Web Components 入门实例教程

作者: 阮一峰

日期: 2019年8月 6日

组件是前端的发展方向,现在流行的 React 和 Vue 都是组件框架。

谷歌公司由于掌握了 Chrome 浏览器,一直在推动浏览器的原生组件,即 Web Components API。相比第三方框架,原生组件简单直接,符合直觉,不用加载任何外部模块,代码量小。目前,它还在不断发展,但已经可用于生产环境。

Web Components API 内容很多,本文不是全面的教程,只是一个简单演示,让大家看一下怎么用它开发组件。

一、自定义元素

下图是一个用户卡片。

本文演示如何把这个卡片,写成 Web Components 组件,这里是最后的完整代码

网页只要插入下面的代码,就会显示用户卡片。


<user-card></user-card>

这种自定义的 HTML 标签,称为自定义元素(custom element)。根据规范,自定义元素的名称必须包含连词线,用与区别原生的 HTML 元素。所以,<user-card>不能写成<usercard>

二、customElements.define()

自定义元素需要使用 JavaScript 定义一个类,所有<user-card>都会是这个类的实例。


class UserCard extends HTMLElement {
  constructor() {
    super();
  }
}

上面代码中,UserCard就是自定义元素的类。注意,这个类的父类是HTMLElement,因此继承了 HTML 元素的特性。

接着,使用浏览器原生的customElements.define()方法,告诉浏览器<user-card>元素与这个类关联。


window.customElements.define('user-card', UserCard);

三、自定义元素的内容

自定义元素<user-card>目前还是空的,下面在类里面给出这个元素的内容。


class UserCard extends HTMLElement {
  constructor() {
    super();

    var image = document.createElement('img');
    image.src = 'https://semantic-ui.com/images/avatar2/large/kristy.png';
    image.classList.add('image');

    var container = document.createElement('div');
    container.classList.add('container');

    var name = document.createElement('p');
    name.classList.add('name');
    name.innerText = 'User Name';

    var email = document.createElement('p');
    email.classList.add('email');
    email.innerText = '[email protected]';

    var button = document.createElement('button');
    button.classList.add('button');
    button.innerText = 'Follow';

    container.append(name, email, button);
    this.append(image, container);
  }
}

上面代码最后一行,this.append()this表示自定义元素实例。

完成这一步以后,自定义元素内部的 DOM 结构就已经生成了。

四、<template>标签

使用 JavaScript 写上一节的 DOM 结构很麻烦,Web Components API 提供了<template>标签,可以在它里面使用 HTML 定义 DOM。


<template id="userCardTemplate">
  <img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image">
  <div class="container">
    <p class="name">User Name</p>
    <p class="email">[email protected]</p>
    <button class="button">Follow</button>
  </div>
</template>

然后,改写一下自定义元素的类,为自定义元素加载<template>


class UserCard extends HTMLElement {
  constructor() {
    super();

    var templateElem = document.getElementById('userCardTemplate');
    var content = templateElem.content.cloneNode(true);
    this.appendChild(content);
  }
}  

上面代码中,获取<template>节点以后,克隆了它的所有子元素,这是因为可能有多个自定义元素的实例,这个模板还要留给其他实例使用,所以不能直接移动它的子元素。

到这一步为止,完整的代码如下。


<body>
  <user-card></user-card>
  <template>...</template>

  <script>
    class UserCard extends HTMLElement {
      constructor() {
        super();

        var templateElem = document.getElementById('userCardTemplate');
        var content = templateElem.content.cloneNode(true);
        this.appendChild(content);
      }
    }
    window.customElements.define('user-card', UserCard);    
  </script>
</body>

五、添加样式

自定义元素还没有样式,可以给它指定全局样式,比如下面这样。


user-card {
  /* ... */
}

但是,组件的样式应该与代码封装在一起,只对自定义元素生效,不影响外部的全局样式。所以,可以把样式写在<template>里面。


<template id="userCardTemplate">
  <style>
   :host {
     display: flex;
     align-items: center;
     width: 450px;
     height: 180px;
     background-color: #d4d4d4;
     border: 1px solid #d5d5d5;
     box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
     border-radius: 3px;
     overflow: hidden;
     padding: 10px;
     box-sizing: border-box;
     font-family: 'Poppins', sans-serif;
   }
   .image {
     flex: 0 0 auto;
     width: 160px;
     height: 160px;
     vertical-align: middle;
     border-radius: 5px;
   }
   .container {
     box-sizing: border-box;
     padding: 20px;
     height: 160px;
   }
   .container > .name {
     font-size: 20px;
     font-weight: 600;
     line-height: 1;
     margin: 0;
     margin-bottom: 5px;
   }
   .container > .email {
     font-size: 12px;
     opacity: 0.75;
     line-height: 1;
     margin: 0;
     margin-bottom: 15px;
   }
   .container > .button {
     padding: 10px 25px;
     font-size: 12px;
     border-radius: 5px;
     text-transform: uppercase;
   }
  </style>

  <img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image">
  <div class="container">
    <p class="name">User Name</p>
    <p class="email">[email protected]</p>
    <button class="button">Follow</button>
  </div>
</template>

上面代码中,<template>样式里面的:host伪类,指代自定义元素本身。

六、自定义元素的参数

<user-card>内容现在是在<template>里面设定的,为了方便使用,把它改成参数。


<user-card
  image="https://semantic-ui.com/images/avatar2/large/kristy.png"
  name="User Name"
  email="[email protected]"
></user-card>

<template>代码也相应改造。


<template id="userCardTemplate">
  <style>...</style>

  <img class="image">
  <div class="container">
    <p class="name"></p>
    <p class="email"></p>
    <button class="button">Follow John</button>
  </div>
</template>

最后,改一下类的代码,把参数加到自定义元素里面。


class UserCard extends HTMLElement {
  constructor() {
    super();

    var templateElem = document.getElementById('userCardTemplate');
    var content = templateElem.content.cloneNode(true);
    content.querySelector('img').setAttribute('src', this.getAttribute('image'));
    content.querySelector('.container>.name').innerText = this.getAttribute('name');
    content.querySelector('.container>.email').innerText = this.getAttribute('email');
    this.appendChild(content);
  }
}
window.customElements.define('user-card', UserCard);    

七、Shadow DOM

我们不希望用户能够看到<user-card>的内部代码,Web Component 允许内部代码隐藏起来,这叫做 Shadow DOM,即这部分 DOM 默认与外部 DOM 隔离,内部任何代码都无法影响外部。

自定义元素的this.attachShadow()方法开启 Shadow DOM,详见下面的代码。


class UserCard extends HTMLElement {
  constructor() {
    super();
    var shadow = this.attachShadow( { mode: 'closed' } );

    var templateElem = document.getElementById('userCardTemplate');
    var content = templateElem.content.cloneNode(true);
    content.querySelector('img').setAttribute('src', this.getAttribute('image'));
    content.querySelector('.container>.name').innerText = this.getAttribute('name');
    content.querySelector('.container>.email').innerText = this.getAttribute('email');

    shadow.appendChild(content);
  }
}
window.customElements.define('user-card', UserCard);

上面代码中,this.attachShadow()方法的参数{ mode: 'closed' },表示 Shadow DOM 是封闭的,不允许外部访问。

至此,这个 Web Component 组件就完成了,完整代码可以访问这里。可以看到,整个过程还是很简单的,不像第三方框架那样有复杂的 API。

八、组件的扩展

在前面的基础上,可以对组件进行扩展。

(1)与用户互动

用户卡片是一个静态组件,如果要与用户互动,也很简单,就是在类里面监听各种事件。


this.$button = shadow.querySelector('button');
this.$button.addEventListener('click', () => {
  // do something
});

(2)组件的封装

上面的例子中,<template>与网页代码放在一起,其实可以用脚本把<template>注入网页。这样的话,JavaScript 脚本跟<template>就能封装成一个 JS 文件,成为独立的组件文件。网页只要加载这个脚本,就能使用<user-card>组件。

这里就不展开了,更多 Web Components 的高级用法,可以接着学习下面两篇文章。

九、参考链接

(完)

留言(59条)

和vue的模式太像了。。

不知道Vue这些框架后面会不会使用原生的组件

引用steven的发言:

和vue的模式太像了。。

准确的说,是 Vue 的设计一开始就参考了这个官方规范。

阮老师有没有空介绍下libra?感觉和之前的数字货币概念有变化,能否讲解一番?

所有浏览器都兼容?

引用DannyPei的发言:
阮老师有没有空介绍下libra?感觉和之前的数字货币概念有变化,能否讲解一番?
libra 是一种趋势!王小川曾经说过,libra 开启了互联网 3.0

目测这些个原生组件如果成熟并且火起来,jq大法又要风靡一时了。哈哈。

用过google的polymer来写web-component,是一个基于openlayers的地图组件,要求是高度自治,在vue和react都可以用。
写小组件还不错,但是项目的依赖管理和打包挺麻烦,官方的脚手架不支持webpack。组件功能一复杂就很头疼。
遗憾的是polymer已经很久没有更新过了。

第七部分(Shadow DOM),dom在开发者工具里还是可见的,审查元素就能看得到。

难道是我理解有问题么。

super(); 是什么意思?去掉的话发现出错。

引用think的发言:

super(); 是什么意思?去掉的话发现出错。

https://es6.ruanyifeng.com/#docs/class-extends#super-%E5%85%B3%E9%94%AE%E5%AD%97

感觉不久的将来可以抛弃vue和react了,返璞归真?

引用sam的发言:

所有浏览器都兼容?

是啊,如果只有Chrome兼容,那意义不太大,在天朝非程序员用Chrome的不多

与其这样,浏览器还不如直接集成Vue和React,以后写代码时,直接声明一下所使用Vue的版本号,不需要引入就能使用。

在constructor里append元素chrome下会报错?

问一下,跟iframe咋这么像呢?

HTML标签,修改属性,视图直接会更新.

复制代码跑了下,修改属性内容不会变化.说明上述特性并不是HTMLElement这个类实现的.那如何监听属性变化?

实际上vue和react解决的都是数据=>视图这个过程的问题,如果不能解决这个问题,web Component的意义不是很大.

腾讯视频播放器是这个实现的吗 chrome下

引用redbuck的发言:

HTML标签,修改属性,视图直接会更新.

复制代码跑了下,修改属性内容不会变化.说明上述特性并不是HTMLElement这个类实现的.那如何监听属性变化?

实际上vue和react解决的都是数据=>视图这个过程的问题,如果不能解决这个问题,web Component的意义不是很大.

你说的是mvvm框架中的另一个核心功能,基于Object.defineProperty实现的数据绑定功能.

阮一峰老师,有没有比较齐全的使用template方式构建web 组件的,组件单独一个文件,引用又在另一个组件的教程

引用阿凯的发言:

阮一峰老师,有没有比较齐全的使用template方式构建web 组件的,组件单独一个文件,引用又在另一个组件的教程

补充说一下,使用web component构建。不用与单文件的template,有没有办法实现,相当于纯前端实现吧

请问web components能用在正式开发吗? 想在移动端中使用可以吗?。 刚刚看到说,兼容性还是有很大问题

引用阿凯的发言:

请问web components能用在正式开发吗?想在移动端中使用可以吗?。刚刚看到说,兼容性还是有很大问题

我司正在用,如果对兼容性要求比较高,不建议使用。

引用搞的微的发言:

我司正在用,如果对兼容性要求比较高,不建议使用。

嗯嗯,好的谢谢啦

slot 也挺有用(常用)的啊,这个你应该提一下。
我觉得 HTML 可以直接写在一个变量里(可以用模板引用可变的参数),然后直接用 innerHTML 而不是每次都要用 template。
现在 Edge 好像还不支持(更别说IE),所以要排除或提示用户用Chrome(内核)浏览器了。

这个让我有理由不去花时间精力学习 Vue/React 了(很不喜欢 Webpack 的麻烦ness)
(我的个人网址就实践了Web Component,应该会离不开了)

阮老师讲的通俗易懂~

个人用Web Component做的一个ui组件库,相比react、vue那些还是有很多优势的,比如纯原生、无需编译等等

https://xy-ui.codelabo.cn/docs

像Vue的模版里<template/>难道不是原生组件的?(我一直以为是原生组件)。如果不是,里面有没有一些功能是用到原生组件的。还是说都是借鉴?

请问您是怎么跑起来的,我见了个文件夹,里边放一个component的html文件,一个用来渲染网页的html文件,但是丝毫没有效果,感觉没有import进来那个component的html文件

angular打包出来就是这样的。然后再对组件进行了增强。当然angular还有其他厉害的功能

应该还是主要是兼容性问题吧,对于企业端应用还不错,毕竟可以指定chrome,对于互联网用户那就不太好了。当然vue已经足够简洁

引用sooge的发言:

你说的是mvvm框架中的另一个核心功能,基于Object.defineProperty实现的数据绑定功能.

他的意思 少了 MVVM 架构,web components 发展前景不大,因为现在 mvvm 大势所趋,而 需要对 web components 进行更多的丰富改造才行,不过既然改造了,那为什么不用vue 或 react 来得直接呢?

通俗易懂,清晰明了是阮老师的风格很喜欢 赞~

会点vue,这跟vue非常像,像vue实现的功能,而且vue实现起来比文中说的简单易用的多。

引用sooge的发言:

你说的是mvvm框架中的另一个核心功能,基于Object.defineProperty实现的数据绑定功能.

vue解决的就是MVVM中(VM)数据驱动视图的驱动问题,如果不对webcomponets做进一步的封装,又回到了MVC时代

chrome 浏览器的组件就是用这个,整个和angular很像,但是Google也不更新了,所以国内基本上没有人在用;另外chrome浏览器项目管理很复杂

引用MrDream的发言:

vue解决的就是MVVM中(VM)数据驱动视图的驱动问题,如果不对webcomponets做进一步的封装,又回到了MVC时代

引用WilliamGui的发言:

会点vue,这跟vue非常像,像vue实现的功能,而且vue实现起来比文中说的简单易用的多。

别丢人了,这是因为vue是基于官方提供的WebComponents来的。

引用鲁班七号的发言:

目测这些个原生组件如果成熟并且火起来,jq大法又要风靡一时了。哈哈。

那算倒退吧,还要操作dom也太麻烦了(在有vue的情况下)

引用rossroma的发言:

与其这样,浏览器还不如直接集成Vue和React,以后写代码时,直接声明一下所使用Vue的版本号,不需要引入就能使用。

Vue\React 等框架本质还是对原生JS的封装,不需要写复杂的原生js代码,简化了开发流程而已,编译后还是转成标准JS代码的,不然你以为浏览器认识 Vue\React 代码?

五、添加样式 这一节用了:host,:host只在template作为影子元素时生效
参考https://developer.mozilla.org/zh-CN/docs/Web/CSS/:host

在成为影子元素前没有局部的概念,想要定义容器的样式需要按照往常的方式定义。

引用大羿的发言:

第七部分(Shadow DOM),dom在开发者工具里还是可见的,审查元素就能看得到。

难道是我理解有问题么。

同问:Shadow DOM隐藏了什么?

状态没有跟视图分离,直接操作dom很麻烦,vue、react这样的mv框架还是少不了。
样式安全封装在组件内部,不暴露任何class,有一些特殊情况下,需要修改样式,就不方便了。
感觉只是提供了基础的API,很多问题还是需要框架解决!除了上面两点,还要状态管理问题。

这个模板的用法感觉和 Underscore Template 非常相似啊。。

引用rossroma的发言:

与其这样,浏览器还不如直接集成Vue和React,以后写代码时,直接声明一下所使用Vue的版本号,不需要引入就能使用。

这个还是有版权问题的,他们的作者不一样,除非协商双方都有利才能达成共识

能发告知如何打包或者 如何用js引入temp 。 就是最后面您说封装成js文件直接引用

引用金的发言:

能发告知如何打包或者 如何用js引入temp 。 就是最后面您说封装成js文件直接引用

在js文件前面加上如下代码:
let target_div = document.createElement("div");
target_div.innerHTML = ''; //引号内填写的HTML代码
document.body.append(target_div);

引用大羿的发言:

第七部分(Shadow DOM),dom在开发者工具里还是可见的,审查元素就能看得到。

难道是我理解有问题么。

说的很清楚,【Shadow DOM 是封闭的,不允许外部访问】。不允许访问不是不允许查看。是不允许js去访问。

感觉w3c事事落后,像css的变量声明,弄个莫名其妙的 -- 来声明,还有方法注解之类的,肯定要出的东西,但是被人抢先,到时候不一定又想出什么奇葩的表现形式

引用foolseward的发言:

感觉w3c事事落后,像css的变量声明,弄个莫名其妙的 -- 来声明,还有方法注解之类的,肯定要出的东西,但是被人抢先,到时候不一定又想出什么奇葩的表现形式

这个和法律类似吧 在实践中,已经总结得出了一套经验,官方才能据此制定规范。规范提供的 永远是最基础的。

看完文章和评论,尝试封装成js:

let target_template = document.createElement("template");
target_template.setAttribute("id", "userCardTemplate");
target_template.innerHTML = `
<style>
//内容太长了,自己去复制样式
</style>

<img class="image">
<div class="container">
<p class="name"></p>
<p class="email"></p>
<button class="button">Follow John</button>
</div>
`; //引号内填写的HTML代码
document.body.append(target_template);

class UserCard extends HTMLElement {
//太长了,自己复制内容
}
window.customElements.define("user-card", UserCard);


然后index.html里使用:
<body>
<user-card
image="https://semantic-ui.com/images/avatar2/large/kristy.png"
name="User Name"
email="[email protected]"
></user-card>
<script src="components/userCard.js"></script>
</body>

就可以了,正常显示~

引用steven的发言:

和vue的模式太像了。。

爸爸长得像儿子?

阮老师,我们是公司使用了webcomponnets进行了开发,发现一个问题,在ios手机上,微信里面的img标签无法唤起长按识别的功能,安卓手机可以,请问下您觉得可能和什么有关呢,怎么解决呀~

引用think的发言:

super(); 是什么意思?去掉的话发现出错。

ES6里面新增的,可以看看阮一峰老师的ES6教程

引用大羿的发言:

第七部分(Shadow DOM),dom在开发者工具里还是可见的,审查元素就能看得到。

难道是我理解有问题么。

那是因为在生成的时候设置了可见,如果设置了不可见,你在dev工具中看到的dom就只是外层的组件名称

引用和孔孟齐名的骑牛子的发言:


说的很清楚,【Shadow DOM 是封闭的,不允许外部访问】。不允许访问不是不允许查看。是不允许js去访问。


哈哈,隐形战机你用眼睛能看到不,

阮老师您好,关于您文末所说「用脚本把 <template/> 注入网页」,这样确实可以通过 dom 操作获取到模板结点并进行深克隆,但是既然已经用 js 脚本生成了 <template/> 模板,那为什么不在 js 里面直接使用呢 ?直接使用不是更符合逻辑么 ?为什么还要将 <template/> append 到 html 页面里去呢 ?在 html 页面里面保存模板有什么好处呢 ?是我理解有问题么 ?期待您的回复,谢谢老师

引用Brannua的发言:

阮老师您好,关于您文末所说「用脚本把 <template/> 注入网页」,这样确实可以通过 dom 操作获取到模板结点并进行深克隆,但是既然已经用 js 脚本生成了 <template/> 模板,那为什么不在 js 里面直接使用呢 ?直接使用不是更符合逻辑么 ?为什么还要将 <template/> append 到 html 页面里去呢 ?在 html 页面里面保存模板有什么好处呢 ?是我理解有问题么 ?期待您的回复,谢谢老师

html直接写的代码是固定死的,不能拿出来复用。

template 是模板 —— 模板是提前准备好的,可以用来无限复用。
你也可以用js createElement 动态生成template, 很多时候没有直接引用template 方便,但是更灵活。

template 模板不用保存在html,工程化时应该放到单独的组件中

shadow Dom的诱惑力还是挺大的,有时候我们需要制造一个干净的组件。

写了10年的iOS UIKit

表示这个跟iOS没差别

都是 new object, add object

看不出有什么优势,非常老套

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接