WebComponent

WebComponent

前言

 最近加入到新项目组负责前端技术预研和选型,一直偏向于以Polymer为代表的WebComponent技术线,于是查阅各类资料想说服老大向这方面靠,最后得到的结果是:"资料99%是英语无所谓,最重要是UI/UX上符合要求,技术的事你说了算。",于是我只好乖乖地去学UI/UX设计的事,木有设计师撑腰的前端是苦逼的:(嘈吐一地后,还是挤点时间总结一下WebComponent的内容吧,为以后作培训材料作点准备。

浮在水面上的痛

组件噪音太多了!

 在使用Bootstrap的Modal组件时,我们不免要Ctrl+c然后Ctrl+v下面一堆代码

<div class="modal fade" tabindex="-1" role="dialog">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <h4 class="modal-title">Modal title</h4>
      </div>
      <div class="modal-body">
        <p>One fine body&hellip;</p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
      </div>
    </div><!-- /.modal-content -->
  </div><!-- /.modal-dialog -->
</div><!-- /.modal -->

 一个不留神误删了一个结束标签,或拼错了某个class或属性那就悲催了,此时一个语法高亮、提供语法检查的编辑器是如此重要啊!但是我其实只想配置个Modal而已。
 由于元素信息由标签标识符,元素特性树层级结构组成,所以排除噪音后提取的核心配置信息应该如下(YAML语法描述):

dialog:
  modal: true
  children:
    header:
      title: Modal title
      closable: true
    body:
      children:
        p:
          textContent: One fine body&hellip;
    footer
      children:
        button:
          type: close
          textContent: Close
        button:
          type: submit
          textContent: Save changes

转换成HTML就是

<dialog modal>
  <dialog-header title="Modal title" closable></dialog-header>
  <dialog-body>
    <p>One fine body&hellip;</p>
  </dialog-body>
  <dialog-footer>
    <dialog-btn type="close">Close</dialog-btn>
    <dialog-btn type="submit">Save changes</dialog-btn>
  </dialog-footer>
</dialog>

而像Alert甚至可以极致到这样

<alert>是不是很简单啊?</alert>

可惜浏览器木有提供<alert></alert>,那怎么办呢?

手打牛丸模式1

既然浏览器木有提供,那我们自己手写一个吧!

<script>
‘use strict‘
class Alert{
  constructor(el = document.createElement(‘ALERT‘)){
    this.el = el
    const raw = el.innerHTML
    el.dataset.resolved = ‘‘
    el.innerHTML = `<div class="alert alert-warning alert-dismissible fade in">
                      <button type="button" class="close" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                      </button>
                      ${raw}
                    </div>`
    el.querySelector(‘button.close‘).addEventListener(‘click‘, _ => this.close())
  }
  close(){
    this.el.style.display = ‘none‘
  }
  show(){
    this.el.style.display = ‘block‘
  }
}

function registerElement(tagName, ctorFactory){
  [...document.querySelectorAll(`${tagName}:not([data-resolved])`)].forEach(ctorFactory)
}
function registerElements(ctorFactories){
  for(let k in ctorFactories){
    registerElement(k, ctorFactories[k])
  }
}

清爽一下!

<alert>舒爽多了!</alert>
<script>
registerElements({alert: el => new Alert(el)})
</script>

复盘找问题

 虽然表面上实现了需求,但存在2个明显的缺陷

  1. 不完整的元素实例化方式
    原生元素有2种实例化方式

a. 声明式

<!-- 由浏览器自动完成 元素实例化 和 添加到DOM树 两个步骤 -->
<input type="text">

b. 命令式

// 元素实例化
const input = new HTMLInputElement() // 或者 document.createElement(‘INPUT‘)
input.type = ‘text‘
// 添加到DOM树
document.querySelector(‘#mount-node‘).appendChild(input)

 由于声明式注重What to do,而命令式注重How to do,并且我们操作的是DOM,所以采用声明式的HTML标签比命令式的JavaScript会来得简洁平滑。但当我们需要动态实例化元素时,命令式则是最佳的选择。于是我们勉强可以这样

// 元素实例化
const myAlert = new Alert()
// 添加到DOM树
document.querySelector(‘#mount-node‘).appendChild(myAlert.el)
/*
由于Alert无法正常实现HTMLElement和Node接口,因此无法实现
document.querySelector(‘#mount-node‘).appendChild(myAlert)
myAlert和myAlert.el的差别在于前者的myAlert是元素本身,而后者则是元素句柄,其实没有明确哪种更好,只是原生方法都是支持操作元素本身,一下来个不一致的句柄不蒙才怪了
*/

 即使你能忍受上述的代码,那通过innerHTML实现半声明式的动态元素实例化,那又怎么玩呢?是再手动调用一下registerElement(‘alert‘, el => new Alert(el))吗?
 更别想通过document.createElement来创建自定义元素了。

  1. 有生命无周期
     元素的生命从实例化那刻开始,然后经历如添加到DOM树、从DOM树移除等阶段,而想要更全面有效地管理元素的话,那么捕获各阶段并完成相应的处理则是唯一有效的途径了。

生命周期很重要

 当定义一个新元素时,有3件事件是必须考虑的:

  1. 元素自闭合: 元素自身信息的自包含,并且不受外部上下文环境的影响;
  2. 元素的生命周期: 通过监控元素的生命周期,从而实现不同阶段完成不同任务的目录;
  3. 元素间的数据交换: 采用property in, event out的方式与外部上下文环境通信,从而与其他元素进行通信。
     元素自闭合貌似无望了,下面我们试试监听元素的生命周期吧!

手打牛丸模式2

 通过constructor我们能监听元素的创建阶段,但后续的各个阶段呢?可幸的是可以通过MutationObserver监听document.body来实现:)
最终得到的如下版本:

‘use strict‘
class Alert{
  constructor(el = document.createElement(‘ALERT‘)){
    this.el = el
    this.el.fireConnected = () => { this.connectedCallback && this.connectedCallback() }
    this.el.fireDisconnected = () => { this.disconnectedCallback && this.disconnectedCallback() }
    this.el.fireAttributeChanged = (attrName, oldVal, newVal) => { this.attributeChangedCallback && this.attributeChangedCallback(attrName, oldVal, newVal) } 

    const raw = el.innerHTML
    el.dataset.resolved = ‘‘
    el.innerHTML = `<div class="alert alert-warning alert-dismissible fade in">
                      <button type="button" class="close" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                      </button>
                      ${raw}
                    </div>`
    el.querySelector(‘button.close‘).addEventListener(‘click‘, _ => this.close())
  }
  close(){
    this.el.style.display = ‘none‘
  }
  show(){
    this.el.style.display = ‘block‘
  }
  connectedCallback(){
    console.log(‘connectedCallback‘)
  }
  disconnectedCallback(){
    console.log(‘disconnectedCallback‘)
  }
  attributeChangedCallback(attrName, oldVal, newVal){
    console.log(‘attributeChangedCallback‘)
  }
}

function registerElement(tagName, ctorFactory){
  [...document.querySelectorAll(`${tagName}:not([data-resolved])`)].forEach(ctorFactory)
}
function registerElements(ctorFactories){
  for(let k in ctorFactories){
    registerElement(k, ctorFactories[k])
  }
}

const observer = new MutationObserver(records => {
  records.forEach(record => {
    if (record.addedNodes.length && record.target.hasAttribute(‘data-resolved‘)){
      // connected
      record.target.fireConnected()
    }
    else if (record.removedNodes.length){
      // disconnected
      const node = [...record.removedNodes].find(node => node.hasAttribute(‘data-resolved‘))
      node && node.fireDisconnected()
    }
    else if (‘attributes‘ === record.type && record.target.hasAttribute(‘data-resolved‘)){
      // attribute changed
      record.target.fireAttributeChanged(record.attributeName, record.oldValue, record.target.getAttribute(record.attributeName))
    }
  })
})
observer.observe(document.body, {attributes: true, childList: true, subtree: true})

registerElement(‘alert‘, el => new Alert(el))

总结

 千辛万苦撸了个基本不可用的自定义元素模式,但通过代码我们进一步了解到对于自定义元素我们需要以下基本特性:

  1. 自定义元素可通过原有的方式实例化(<custom-element></custom-element>,new CustomElement()document.createElement(‘CUSTOM-ELEMENT‘))
  2. 可通过原有的方法操作自定义元素实例(如document.body.appendChild等)
  3. 能监听元素的生命周期
    下一篇《WebComponent魔法堂:深究Custom Element 之 标准构建》中,我们将一同探究H5标准中Custom Element API,并利用它来实现满足上述特性的自定义元素:)
     尊重原创,转载请注明来自: http://www.cnblogs.com/fsjohnhuang/p/5918677.html ^_^肥仔John

感谢

Custom ELement
Custom ELement v1
MutationObserver

时间: 2024-12-10 22:47:09

WebComponent的相关文章

JavaScript实现绑定DOM的定时器插件

问题 使用原生的setTimeout和setInterval仅仅能够实现, 定时执行事件处理函数, 在网页开发中, 往往会出现一种情况,定时器用于定时更新某个页面区域的数据, 往往在页面加载之后, 就启动这个定时器, 往后则间隔执行此定时器. 页面上定时刷新的区域可能会动态消失, 特别是在ajax被广泛使用的今天, 如果定时刷新的区域被删除了, 则定时器材也需要自动清除掉. 此二个接口,如果实现这种效果需要, 自己维护定时器句柄, 并且在处理定时器事件函数的时候, 首先判断 指定的刷新区域是否还

浅谈angular2与angularJS的区别

简介 大家好,今天给大家介绍一下angular,相信做过前端的小伙伴们都知道angular的大名,angularJS自2012年发布起就受到了大家的广泛关注.他首次提出了双向绑定概念让所有人都耳目一新,2016年angular2正式被发布,那么angular2到底有什么值得期待的地方呢,接下来讲一下angular2吸引人的地方. 1.1.1  angularJS的困境以及angular2的新特性 首先呢我们讨论一下angularJS的一些不足之处: 1.饱受诟病的性能问题 通过检查进行数据更新,

vue.js学习之组件数据流详解

本文和大家分享的主要是vue.js组件数据流相关内容,一起来看看吧,希望对大家学习vue.js有所帮助. 一.组件 组件,可以说是现代前端框架中必不可少的组成部分.使用组件,不仅能极大地提高代码的复用率和开发者的开发效率,对于代码后期的维护也有着非常重要的意义.前端开发,由于历史遗留原因,WebComponent 虽然好用,但其发展情况却受到极大地限制,和很多新兴的前端技术一样,可望而不可即.基于这样的情况,聪明的开发者们尝试通过框架内部集成相应的功能来完成组件化,各种现代前端框架基本上都有各自

Spring Boot通过命令行启动发生FileNotFoundException

Spring Boot + Jersey 通过命令行启动会发生错误FileNotFoundException异常 异常信息如下: ERROR o.a.c.c.C.[Tomcat].[localhost].[/] - StandardWrapper.Throwable org.glassfish.jersey.internal.ServiceConfigurationError: org.glassfish.jersey.internal.spi.AutoDiscoverable: : java.

Windchill10.0升级到10.2版本导致Jersey-1.17无法正常工作的解决办法

先给出异常详细信息吧: 2015-06-01 15:06:28,694 ERROR [localhost-startStop-1] org.apache.catalina.core.StandardContext.loadOnStartup(StandardContext.java:5233) - Servlet /Windchill threw load() exception java.lang.IncompatibleClassChangeError: Implementing class

又一例 select for update 的悲观锁使用所引发的血案

1. 事故描述 昨日上午十点多,我们的基础应用发生生产事故.具体表象为系统出现假死无响应. 2. 硬件 LB 查看硬件负载路由情况:ARRAY-3(config)#sh stati sl r tcp JiChuYinYong_a_8001Real service JiChuYinYong_a_8001 192.168.1.137 8001 UP ACTIVE        Main health check: 192.168.1.137 8001 tcp UP        Max Conn C

React.js再探(三)

很多时候,组件实例的外观和行为我们通过props进行定制就可以了.因为任何时候,组件实例的表现只跟 传过来的props属性 相关. 我们称这种为 无状态/ stateless 组件 即它自身是 无记忆的 比如切换开关,它可以响应用户的点击事件,如果当前状态是关,那么它就 切换到开的状态(显示开状态的图片):而如果当前状态是开,那么它就切换到关的 状态(显示关状态的图片) 状态记忆:state React引入了状态机的概念,让组件具有不同的状态,使得组件具有记忆功能 具体如下: state ——组

手机Web定位 解决国行安卓手机没有GMS包

最近在做手机打开一个网址,然后定位具体的位置,本来觉得很简单的,HTML5就能解决这问题了,后来发现问题还是很多. 首先是很多安卓手机不支持HTML5,判断navigator.geolocation是true,但是就是不返回经纬度信息,查资料发现很多国行很多手机都是这样,GMS(Google Mobile Service)被阉割了.解决这种方法可以恢复网络定位.但是咱们这手机网页是对所有用户开放的,显然不合理. 后来查资料发现安卓浏览器有些是可以的,然后下载了安卓所有常见的浏览器测试,确定四款浏

Spring Boot + Jersey发生FileNotFoundException (No such file or directory)

我在使用Spring Boot + Jersey 项目,解决了上一篇随笔中的FileNotFoundException,然后又报了一个FileNotFoundException,不过报错信息不一样了 报错信息如下: ERROR o.a.c.c.C.[Tomcat].[localhost].[/] - StandardWrapper.Throwable org.glassfish.hk2.api.MultiException: A MultiException has 1 exceptions.