fe_webComponent

13 min read

web component

背景

前端或者说web的演进过程大概就是,

从php这种服务端渲染+简单的页面布局,

发展到jquery/lodash等工具框架简化前端的日益复杂的场景,

再到移动互联网时代bootstrap/h5/css3响应式占据主流,

然后前端变得更重更复杂出现了前后端分离,或者说客户端渲染模式/单页应用/组件化开发。

组件化是一个必然的趋势,因为前端业务越来越繁杂,能将复杂的dom/js/css封装成一些成熟的组件,就能大大提高代码的复用,提高开发效率。所以react/vue/angular的核心概念都是组件,只要你学会了他们的组件怎么使用,你就学会了这个框架。然而不同的框架有着自己的组件声明语法和背后的实现逻辑,各大框架显然都是各自为战,组件不能互用,意味着你必须站队其中一个。比如你是react的坚定支持者,或者vue的忠实粉丝,也有可能你对两者都很熟悉,但是最终写项目的时候还是要做出选择。另一个令人头疼的问题是组件库,如果你是公司项目,那可能已经有内部的组件库,也就固定了必须用哪个框架。但是如果是自由项目,你发现一个很好看的组件库是基于react的,比如官方的antd组件库。但是你又是vue的粉丝,这时候就比较尴尬。(当然这是个例子,其实antd有民间的vue版本)

不过现在Web Component将要终结一切纷争,迎来最终的大一统,所有的第三方(非w3c)组织的框架,都要在Web Component面前黯然失色。他是W3C官方和谷歌微软等等公司一起拟定了新的组件规范。

愿景

原来写react的时候时常会想,我自己的Table组件用jsx写的是<Table/>,但是实际编译成浏览器支持的js后就会变成一系列dom支持的标签比如<table>等等的组合。

如果能真的在浏览器中自定义dom标签(组件)该有多好!

原来写前端项目的时候,都会经常使用import/export来引入自己的组件或者依赖中的组件,但是浏览器并不支持这个语法,编译js的过程变得非常复杂。

如果浏览器能原生支持import/export该有多好!

特大好消息,上述都已经在现代浏览器获得支持啦!WebComponent使得我们可以直接声明自己的标签贴到html代码中辣,esm的支持使得也可以用import直接引入第三方库啦。

esm

先来说下esm的支持,例如我们可以这样引入第三方的库,比如jquery lodash d3的引入如下,注意script的type必须是module才支持import语法,然后是必须是esmodule版本的cdn库才可以。

<div id="root"></div>
<script type="module">
  import $ from 'https://unpkg.com/jquery-es';
  import _ from 'https://unpkg.com/lodash-es'
  import * as d3 from 'https://unpkg.com/d3?module'
  d3.select("#root").style('color', 'red')
  $('#root').text('I am jquery and here is lodash ' + _.join([1,2,3]))
</script>

HTMLElement与shadow DOM

WebComponent的规范主要是通过js对象HTMLElement和dom中的shadow dom来实现的。

<script>
  // 通过extends HTMLElement,并在构造方法中,开启shadowDom,并追加自定义的标签即可实现,自定义的标签
  class MyC extends HTMLElement {
    constructor(){
        super();
        this.attachShadow({mode: 'open'});
        var d = document.createElement('div');
        d.innerHTML =`<p style='color:red'>This is content from custom component</p>`;
        this.shadowRoot.appendChild(d);
    }
  }
  // 将MyC这个组件注册到自定义的元素中,并命名为my-c标签,注意标签规范是小写,且必须有横线。
  customElements.define('my-c', MyC)
</script>
<my-c/>

可以在开发者模式中看到这个组件,确实使用的是my-c这个标签,并且内部有一个shadow root,其他元素都是挂在一个shadow root下面的。

image

shadow dom就像是一个黑盒,shadow内部任何变量、css样式都不与全局dom共享,这是很重要的一点,因为大多数情况下,例如我们声明一个.my-style样式,那么他的作用域默认就是全局的。shadow却能进行隔离,其实这也不是什么特别新的技术,其实有个远古的标签iframe就有类似功能,他能引入其他页面,并保持其他页面内部的变量样式等独立,也有个类似的#document。

image

react/vue好像也能限定css样式的作用域,他们也是用了shadow吗?并不是,他们的实现原理本质上都是将css在编译的时候改了名字,改成一个具有唯一标识的乱码的名字来实现的。

image

Lit

上面的web component声明方式比较麻烦。一方面是各种公逻辑包括html排布要写到constructor内部调理不太清晰,另一方面是dom状态改变重新渲染等逻辑需要自己重新绘制,所以就有了对web component的继承实现和简化的lit,这里需要注意的是lit与react等不同,他只是在原生webcomponent提供一些语法糖和逻辑优化,并不是创建只有自己这个框架才能用的组件!!这一点很重要。

<script type="module">
  import {LitElement, html} from 'https://cdn.skypack.dev/lit-element';

  class MyElement extends LitElement {
    static properties = {
      name: {type: String},
    };

    constructor() {
      super();
      this.name = 'World';
    }

    render() {
      return html`
      <h1>Hello, ${this.name}</h1>
      `;
    }
  }
  customElements.define('my-element', MyElement);
</script>
<my-element name="张三"></my-element>

properties & attributes

Attributes are key value pairs defined in HTML on an element.

<div id="myDiv" foo="bar"></div>
const myDiv = document.getElementById('myDiv');
console.log(myDiv.attributes);
console.log(myDiv.getAttribute('foo'));

Properties are key-value pairs defined on a javascript object.

const myDiv = document.getElementById('myDiv');

myDiv.foo = 'bar';

dom中的attribute必须是数字、字符或者bool值,这使得如果传输json对象或者数组数据的时候受阻,lit的解决方案是:

  • JSON.stringify(*)内部自动会将attribute转换为property,并且如果是json字符串的会转回js对象/数组,根据properties的声明。
  • 如果是在另一个元素中,也可以使用在属性前面加个.,例如.person=${this.obj}.onClick=${this.func}

还是上面hello word的例子,如果穿的是对象,则可以声明为Object类型,并传递jsonstring即可,此时会做自动的转为json对象操作。

<script type="module">
  import {LitElement, html} from 'https://cdn.skypack.dev/lit-element';

  class MyElement extends LitElement {
    static properties = {
      person: {type: Object},
    };

    constructor() {
      super();
      this.person = {name: 'World', age: 0};
    }

    render() {
      console.log(this.person)
      return html`
      <h1>Hello, ${this.person.age} years old ${this.person.name}</h1>
      `;
    }
  }
  customElements.define('my-element', MyElement);
</script>
<my-element person='{"name":"张三","age":11}'></my-element>

re-rend

初次渲染之后,经常还有异步的再次渲染,比如点了个按钮之类的事件,要触发页面变动,在lit中只需要修改properties中定义的key的值,就可以自动触发页面重新渲染,且不会触发整个页面的渲染,自动只触发最小范围的dom的渲染。

这是一个例子:连接

生命周期

钩子函数:

  • constructor构造函数,创建组件对象的时候会调用。
  • connectedCallback创建shadow并挂载完成了时会调用,可以用来添加事件注册,比如整个组件的对外的focus、click等事件。
  • disconnectedCallback从dom中删除的时候调用,可以用来删除事件的注册。防止内存泄漏。
  • attributeChangedCallback属性发生变化的时候调用,该方法默认逻辑就是properties发生变化就会re-rend。You rarely need to implement this callback.

slot与template

<slot></slot>标签用在render的html里,用于表示里面的其他元素,和this.children功能类似。 <template></template>标签用于声明一个模板的dom,不会被渲染,但是可以通过该dom的.content方法获取内部的html文本,作用没有slot大。

好用的组件库的使用方法

像react有antd等组件库一样,webcomponent现在也有一些组件库,将来肯定越来越多,希望antd也能出一版webcomponent版本的组件库。

web component的组件库,基本上不需要使用npm来管理包,可以回归到jquery年代的直接html引入cdn库即可,然后在html中直接使用自定义组件的标签即可。

当然也可以使用npm安装依赖后组织项目和本地打包,这个我们后面再说。

a.微软的fluent

第一位选手就是来自微软的fluentrepo。顺带一提fluent也有react的版本。29个组件,常用的form、button、input代码和样式如下: image

b.IBM的carbon-web-components

第二位选手是来自IBM的carbonrepo。风格非常"欧美捞逼",各种组件都很硬朗,棱角分明的。组将数44个,粗略看了下很多组件没有像antd那样的深度逻辑,比如table组件,就只是样式。

image

c.material-components-web

material-ui不多说了,算是和bootstrap齐名的框架了。repo,也没有table组件。

image

d.shoelace

repo来自民间开源组织,文档比较详细,也给了和react vue的解决方案,样式比较舒服。组件高达50+。但是比较坑的是没有table相关的组件,搞不懂为啥,其他的样式还都挺好看的,也很不错,而且一直在更新中。

image

e.ui5-webcomponents

ui5-webcomponent扁平化风格,有点像jquery-easy-ui

image

f.adobe的spectrum-web-components

官网adobe软件内的风格样式的web组件。

image

g.kor

官网

image

h.wired components

官网手绘风格的线条ui,缺点是组件不是很多,适合简单网页。

image

i.clever components

官网

image

j.vscode-webview-elements

官网 vscode风格的组件

image

image