Web Components 简述

1,203 阅读11分钟
原文链接: zhuanlan.zhihu.com

要说最近几年来,前端开发最火的一个趋势或最火的前端开发框架是什么,第一想到的是,组件及推崇组件化开发的React框架。本文将介绍Web Components规范并就组件的几大特性进行讨论。

前言

在开启本篇的阅读之前,先问一个问题,组件是什么?

组件,是数据和方法的一个封装,其定义了一个可重用的软件元素的功能,展示和使用,通常表现为一个或一组可重用的元素。

组件的特性是什么?通常可以总结为以下几点:

  1. 可拓展性:既然组件是针对某一特定功能或需求开发的,那它就必须易于开发和拓展;
  2. 封装性:组件作为一个独立整体供使用,应该是对内修改,对外封闭,只供使用,而不对使用环境产生副作用;
  3. 易用性:组件的目的是产生可重用的独立部件,那就必须提供一种简单快捷的方式供使用。

组件化,给前端开发带来了极大的效率提升,是近几年以来web开发发展的趋势,各种组件化的用户界面库,框架也层出不穷,如,React,Vue,Ionic等,这些框架关于组件化都有各自的实现,推崇理念,与编程规范,各大框架的支持者之间的争论也是向来不断,而若想在不同框架间切换,成本还是挺高的,因为毕竟谁都希望自己能占主流,占据绝对优势地位,就像当前IE与网景浏览器之争,延续到现在,各类浏览器标准兼容差异万千,近年来w3c不断在为web标准规范做努力,Web Components就是推出的关于组件化的一个标准,希望它能将组件化更好的带进web开发,同时尽量保证标准规范,开发者可以更好的关注于开发,而不是框架选择与争论之上。

Web Components特征

Web Components将一系列特性加入HTML和DOM规范,使得开发者可以自由创建在web应用或文档可重用的元素或部件,其由四部分组成:

  • 自定义元素(Custom Elements):定义新HTML元素的一系列API;
  • 影子DOM(Shadow DOM):组合对DOM和样式的封装;
  • HTML导入(HTML Imports):定义在文档中导入其他HTML文档的方式;
  • HTML模板(HTML Templates):HTML内的DOM模板,在<template>元素内声明。

自定义元素(Custom Elements)

自定义元素支持开发者定义一类新HTML元素,声明其行为和样式,自定义元素分两类:

  • 自定义标签元素(Autonomous custom elements):完全独立于原始HTML元素标签的新标签元素,其所有行为需要开发者定义;
  • 自定义内置元素(Customized built-in):基于HTML原始元素标签的自定义元素,以便于使用原始元素的特性,开发者只需要定义拓展行为;

支持创建自定义元素,Web Components比较好的实现了组件开发的可拓展性。

创建自定义标签元素

为了创建一个自定义标签元素,我们需要继承HTMLELement类, 如在很多页面我们经常会有一键回到页面顶部功能,我们创建一个返回顶部的组件:

class GoTop extends HTMLElement {
        constructor() {
            super();
        }
    }
    customElements.define('go-top', GoTop);

在需要使用该组件的页面只需像使用正常HTML元素一样:

<go-top>Top</go-top>

当然,该元素的一切样式,行为,事件监听,默认行为均需要开发者自行定义,无法期待它有像<button>一样的默认行为详细参考创建自定义标签元素

创建自定义内置元素

很多时候我们并不需要完全创建一个新元素,而只是需要在某些内置元素基础上进行拓展,创建自定义内置元素,需要继承该类元素类,如HTMLButtonElement 或HTMLDivElement:

class MenuButton extends HTMLButtonElement {
        ...
    }
    customElements.define('menu-button', MenuButton);

使用也很简单,和内置元素一样的语法;不同的是,在需要使用自定义内置元素时,为内置元素添加is特性,该特性值对应创建的自定义内置元素名称:

<button is="menu-button">menu</button>

该元素默认行为继承自<button>元素,但是我们可以为其设置拓展功能或性质。

对比

通过上面实例可知,自定义标签元素与内置元素主要表现在两点不同:

  • 标签:自定义标签元素是完全独立的一个新元素,新标签,而自定义内置标签,使用的依然是已有内置标签;
  • 行为与样式:自定义内置元素,继承内置元素的默认行为,样式及语义,可以进行拓展,而自定义标签元素,完全需要开发者定义相关声明。

影子DOM(Shadow DOM)

DOM,即文档对象模型,是HTML文档的一个结构表示,以树形结构表示一个文档,文档中元素间关系按照父子,兄弟关系排列;DOM规范提供一系列API支持我们操作文档节点,即通常所说的DOM API。

前面提到Web Components指封装DOM和样式,以组件的形式在文档中使用,而不同于JavaScript中函数会形成一个单独作用域,文档DOM树的层次结构中是不存在局部作用域概念的,也就是说文档内所有定义的样式都对整个文档产生影响,文档中的样式也会影响组件内的声明样式,而不限定于元素所处位置,这样显然极大阻碍了组件的独立性和可重用性,是必须要解决的问题,不过不用担心,这都已经解决了,解决方案就是下文介绍的attachShadow()方法。

影子DOM API提供了attachShadow()方法,创建一个影子DOM,支持将封装的内容或组件作为一个独立DOM子树附加进一个HTML文档,组件内与外部隔离,样式互不影响,这也印证了组件开发的封装性需求。

创建

要创建一个影子DOM,很简单,使用attachShadow()方法即可,而需要注意的是所有影子DOM必须和一个文档中存在的元素(HTML内置元素或自定义元素)绑定,才能使用:

var frag = document.createElement('div');
    var shadowRoot = frag.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = '<p>Shadow DOM Content</p>';

影子树(SHADOW TREE)与影子主体(SHADOW HOST)

上文使用attachShadow()方法创建的元素就是一个影子DOM,而其子内容就构成一棵影子树(shadow tree),而和影子DOM绑定,也就是包含该树的文档内元素通常称为影子主体(shadow host)。

槽位(SLOT)

如上,当一个元素(即影子主体)内存在影子DOM,浏览器默认只会渲染该影子DOM的影子树,而不渲染影子主体的其他子内容,如,现有某元素<div class="menus">,在文档中使用如下:

<div class="menus">
        <h2>Menus</h2>
    </div>

给该元素绑定影子DOM:

var menus = document.querySelector('.menus');
    var shadowRoot = menus.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = '<ul>\
        <li>Home</li>\
        <li>About</li>\
    </ul>';

其影子树内容为:

<ul>
        <li>Home</li>
        <li>About</li>
    </ul>

最后渲染结果如下:

<div class="menus">
        <ul>
            <li>Home</li>
            <li>About</li>
        </ul>
    </div>

你好发现影子主体原本的子元素内容没有被渲染,那么是不是没办法了?当然不是,如果要保存子内容,需要使用<slot>槽位元素,相当于做一个占位符,只需要把前文影子主体内容修改为如下:

<div class="menus">
        <h2>Menus</h2>
        <slot></slot>
    </div>

渲染结果如下, 一切符合需求:

<div class="menus">
        <h2>Menus</h2>
        <ul>
            <li>Home</li>
            <li>About</li>
        </ul>
    </div>
命名槽(named slots)

上文显示的是只有一个槽位的实例,假如需要有多个分组怎么办呢?Web Components也有解决方案,那就是使用命名槽,即给槽位添加name属性,依然使用如上实例,修改影子主体内容:

<div class="menus">
        <slot></slot>
        <slot name="top"></slot>
        <slot name="right"></slot>
    </div>

假如影子树内容如下:

<h2>Menus</h2>
    <ul slot="top">
        <li>Home</li>
        <li>About</li>
    </ul>
    <ul slot="right">
        <li>Home</li>
        <li>Top</li>
    </ul>

渲染结果如下:

<div class="menus">
        <h2>Menus</h2>
        <ul>
            <li>Home</li>
            <li>About</li>
        </ul>
        <ul>
        <li>Home</li>
        <li>Top</li>
    </ul>
    </div>

如上,可以发现拥有name属性的槽位由对应slot属性值相同的影子子树替换,而剩下的内容默认替换空名槽位,若不存在空名槽位,则剩余内容将被抛弃。

样式

前文已经提到,Web Components定义的组件内的样式与外部环境的样式是互不影响的,那么如何为组件设置样式呢,依然使用<style>标签:

<head>
        <style>
            .top {margin-top: 30px;}
        </style>
    </head>
    ...

    <div class="top">
        ...
    </div>
    ...

    <div class="menus">
        #shadow-root
        <style>
            .top {margin-top: 10px;}
        </style>
        <div class="top">
            ...
        </div>
    </div>

如上实例,在组件内部top类元素margin-top值为10px,而外部top类元素margin-top值为30px,两者是独立的。

渲染

关于影子DOM树的渲染,其方式与web文档DOM树的渲染方式并无区别,均由浏览器渲染引擎进行渲染,需要注意的是,影子树的DOM渲染过程和文档DOM树的渲染是独立分别进行的。

HTML引入(HTML Imports)

如何在HTML文档中引入另一个web文档或web组件呢?像JSP或PHP语言都对HTML语法进行了拓展,我们可以使用诸如<include>标签直接引入另一个文档,然而在这之前,原生HTML规范并不支持直接引入另一文档,通常都得通过ajax请求另一文档内容,然后通过JavaScript使用DOM API将内容插入,对于组件化开发和使用,这样显然不是我们期望的结果,这与组件的易用性是背离的,所以,HTML imports定义了如何在文档内引入和重用另一文档。

在文档内直接引入外链资源的文档或web组件,语法如下,使用<link>标签:

<link rel="import" href="components.html">

假如在components.html中定义了got-top自定义元素,则在本文档内可以直接使用:

<go-top>GoTop</go-top>

如上,仅仅将<link>标签的rel属性设置成import即可,另外值得注意的是:为了避免重复执行引入文档内的脚本,对于已加载文档,import方式将跳过其加载和执行过程。

HTML模板(HTML Templates)

为了更友好的处理组件模板,Web Components规范,支持<template>模板标签,HTML模板定义了使用<template>标签声明可以通过脚本操作插入文档的HTML模板片段:

<template id="menusTemplate">
        <ul>
            <li>Home</li>
            <li>About</li>
        </ul>
    </template>

使用脚本操作,该元素content属性可访问模板内容:

var menusTemplate = document.querySelector('#menusTemplate');
    var frag = document.importNode(menusTemplate.content, true);
    document.querySelector('.menus').appendChild(frag);

TEMPLATE标签

<template>标签本质上与其他HTML内置标签一样,可以使用DOM API进行操作,但是需要明白,在将模板激活(生成DOM或插入文档)前:

  1. <template>标签内的内容不会被渲染;
  2. 标签内的图片,等媒体资源不会被加载;
  3. 标签不会出现在DOM树,审查元素看不到;

关于兼容性

对于Web Components规范的兼容性,目前还是需要使用webcomponentsjs polyfills的方式支持开发,总的来说,目前Safari 10, Google Chrome (53)兼容的更好;虽然兼容性并不好,还在推进过程中,但是对其进行学习还是很有必要的。

类库

推荐几个常见的Web组件类库:

  • Polymer: Google推出的web组件库,支持数据的单向和双向绑定,兼容性较好,跨浏览器性能也较好;
  • X-Tag: 微软推出的开源库,支持Web Components规范,兼容Web ComponentsAPI;
  • Slim.js:轻量级的web组件库,专注于帮助开发者更好的编写原生web组件,而不依赖于其他框架,但是也提供了良好的拓展性,开发者可以自由拓展。

参考

ComponentWeb Components SpecificationsWeb Components WiKiWeb Components MDN