Web组件代替React-另一尝试

哈Ha!

我最近决定弄清楚,不使用框架就完全用香草Web组件编写的客户端Web应用程序应该是什么样子。事实证明,这是一件很有意义事情,最后,我绘制了移动PWA模板,该模板现在已用于原型中。从以下前提出发:

  • DOM是一种状态。由于我们没有框架,因此我们立即忘记了功能主义,而回到了命令式OOP。Web组件是长期存在的DOM节点,它们封装了它们的状态并具有公共API。它们不是重新创建的,而是已更改。因此,我们不仅应将DOM视为一种表示形式,还应将其视为业务对象的存储库,因此,我们需要考虑到组件交互的便利性来构建组件的层次结构。
  • 组件之间的相互作用。组件可以通过直接调用,回调交换或通过上/下用户DOM事件进行交互。后一种方法是最可取的,因为它减少了相互啮合(耦合),并对键的图进行排序(请参见下面的示例)。
    DOM事件仅在层次结构中起作用-它们可以从祖先链的底部向上弹出,或向下传播给所有后代。在其他情况下,标准的浏览器API用于寻址组件:document.querySelector('page-home'),某些组件可以在窗口中注册自己并在全局范围内使用:APP.route('page-login')。
  • 内部风格。不使用Shadow DOM,因此组件继承全局样式,但也可以拥有自己的样式。由于<style scoped>不太可能在不久的将来实现,因此您必须使用组件名称前缀来声明内部样式,但这是沉默寡言的,并且效果很好(请参见下面的示例)。
  • 与HTML / DOM的交互。由于DOM是一种状态,因此数据源是HTML元素本身的值(值,已检查,contenteditable的innerHTML =“ true”等)。不需要附加的JS变量,为了方便访问表单值-我们只需创建getters / setters并将其添加到祖先对象(为此需要一个小型库)。现在寻址形式值与寻址类变量没有什么不同,例如,this.pass是输入到当前组件的<input>子代中的密码值。因此,既不需要虚拟DOM,也不需要双向绑定,也不需要在重新打开表单时重画表单,并且在导航期间会保存输入表单的数据,除非您专门清除它们。
  • 导航。页面组件位于<main>容器中,一旦创建,它们不会被删除,而只是被隐藏。这使您可以使用location.hash来实现导航,并且标准的浏览器按钮来回可正常使用。导航到现有组件时,将调用onRoute()方法,您可以在其中更新数据。

应用结构


我们的应用程序包括:

  • 可以通过window.APP访问的根组件<app-app>,其中包含页面路由器和全局功能;
  • 具有上下文按钮的面板(我没有将它们放入单独的组件中,而是将它们作为<app-app>布局的一部分以简化事件处理);
  • 下拉菜单(单独的组件);
  • 页面组件将添加到的<main>容器:打开时的<page-home>,<page-login>,<page-work>。

页面堆叠有来回导航。此外,我们演示了自下而上和自上而下的数据流:

  • 授权状态和当前用户名存储在<app-app>组件中,但通过弹出事件来自<page-login>组件。
  • 计时器在<app-app>组件中打勾,该组件通过仅在<page-work>的后代中捕获的广播事件向下发送当前值。

通过弹出事件,还可以在应用程序面板上实现上下文相关按钮的安装-按钮本身以及处理程序在页面组件中创建,然后发送到顶部,在应用程序级别被拦截并插入面板中。

实作


为了使用组件的内部DOM并发送下游事件,使用了微小的WcMixin.js-少于200行代码,其中一半(用户输入事件的统一)也可以扔掉。其他一切都是纯香草。典型的组件(授权页面)如下所示:

import wcmixin from './WcMixin.js'

const me = 'page-login'
customElements.define(me, class extends HTMLElement {
   _but = null

   connectedCallback() {
      this.innerHTML = `
         <style scoped>
            ${me} {
               height: 90%; width: 100%;
               display: flex; flex-direction: column;
               justify-content: center; align-items: center;
            }
            ${me} input { width: 60%; }
         </style>
         <input w-id='userInp/user' placeholder='user'/> 
         <input w-id='passInp/pass' type='password' placeholder='password'/>
      `
      wcmixin(this)

      this.userInp.oninput = (ev) => {
         this.bubbleEvent('login-change', {logged: false, user: this.user})
      }

      this.passInp.onkeypress = (ev) => {
         if (ev.key === 'Enter') this.login()
      }
   }

   onRoute() {
      this.userInp.focus()
      this._but = document.createElement('button')
      this._but.innerHTML = 'Log in<br>⇒'
      this._but.onclick = () => this.login()
      this.bubbleEvent('set-buts', { custom: [this._but] })
   }

   async login() {
      APP.msg = 'Authorization...'
      this._but.disabled = true
      setTimeout(() => {
         this._but.disabled = false
         if (this.user) {
            this.bubbleEvent('login-change', {logged: true, user: this.user})
            APP.route('page-work')
         } else {
            APP.msg = 'Empty user !'
            this.userInp.focus()
         }
      }, 1500)
   }
})

首先,在这里我们看到组件的局部样式。其次,唯一的非标准属性w-id =``userInp / user''已添加到HTML标记中。 wcmixin()函数处理所有带有此属性标记的元素,并将变量添加到当前组件:this.userInp引用<input>元素本身(允许您挂起处理程序),而this.user是元素的值(用户名)。如果不需要访问该元素,则可以指定w-id =``/ user'',并且只会创建该值。

输入用户名时,我们通过一个弹出事件向上发送当前值,在onRoute()方法中创建一个上下文相关按钮,并向上发送它。

重要的是,授权组件对应用程序/面板的高级组件一无所知,也就是说,它不会被钩住。他只是将事件发送到楼上,而拦截事件的人则取决于开发人员。从<page-work>组件中的<app-app>应用程序接收事件的方式相同:

import wcmixin from './WcMixin.js'

const me = 'page-work'
customElements.define(me, class extends HTMLElement {

   connectedCallback() {
      this.innerHTML = `
         <p w-id='/msg'>Enter text:</p>
         <p w-id='textDiv/text' contenteditable='true'>1)<br>2)<br>3)</p>
      `
      wcmixin(this)

      this.addEventListener('notify-timer', (ev) => {
         this.msg = `Enter text (elapsed ${ev.val}s):`
      })
   }

   async onRoute() {
      this.textDiv.focus()
      document.execCommand('selectAll',false,null)
      const but = document.createElement('button')
      but.innerHTML = 'Done<br>⇒'
      but.onclick = () => alert(this.text)
      this.bubbleEvent('set-buts', { custom: [but] })
   }
})

然后在<app-app>组件中编写:

setInterval(() => {
   this._elapsed += 1
   this.drownEvent('notify-timer', this._elapsed)
}, 1000)

<app-app>组件也对想要使用其计数器的页面组件一无所知,也就是说,它没有钩在其后代上。开发人员就事件签名达成共识就足够了。DOM事件是轻量级的,降序事件仅发送到Web组件(而不是简单元素),而升序事件作为标准通过整个祖先链传递。

其实,这就是我要说的。

→  完整的项目代码

异议和建议


我经常反对这种方法将业务逻辑和显示混合在一个“较粗”的组件中,这违反了公认的模式。但是,我们仅在讨论用于显示和验证用户输入的逻辑,其余的业务逻辑可以通过其自己的层次结构和交互方法轻松地移动到单独的JS类甚至服务中。

为什么这是必要的。仍然存在渲染性能问题(垃圾收集不是免费的),与使用JS库和VDOM进行声明式/函数式相比,使用本机工具的强制性方法将始终更快,资源占用更少。如果您愿意的话,如果您承担基准测试功能(我可以做得不好),我愿意与任何框架的代表按照商定的TOR竞争。

谢谢您的关注。

All Articles