组件是前端的发展方向,现在流行的 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 的高级用法,可以接着学习下面两篇文章。
九、参考链接
- The anatomy of Web Components, Uday Hiwarale
(完)
steven 说:
和vue的模式太像了。。
2019年8月 6日 18:02 | # | 引用
niexia 说:
不知道Vue这些框架后面会不会使用原生的组件
2019年8月 7日 00:58 | # | 引用
南小北 说:
准确的说,是 Vue 的设计一开始就参考了这个官方规范。
2019年8月 7日 04:44 | # | 引用
DannyPei 说:
阮老师有没有空介绍下libra?感觉和之前的数字货币概念有变化,能否讲解一番?
2019年8月 7日 09:36 | # | 引用
sam 说:
所有浏览器都兼容?
2019年8月 7日 11:07 | # | 引用
业余草 说:
2019年8月 7日 13:27 | # | 引用
鲁班七号 说:
目测这些个原生组件如果成熟并且火起来,jq大法又要风靡一时了。哈哈。
2019年8月 7日 14:16 | # | 引用
心斩心 说:
用过google的polymer来写web-component,是一个基于openlayers的地图组件,要求是高度自治,在vue和react都可以用。
写小组件还不错,但是项目的依赖管理和打包挺麻烦,官方的脚手架不支持webpack。组件功能一复杂就很头疼。
遗憾的是polymer已经很久没有更新过了。
2019年8月 7日 15:07 | # | 引用
大羿 说:
第七部分(Shadow DOM),dom在开发者工具里还是可见的,审查元素就能看得到。
难道是我理解有问题么。
2019年8月 7日 15:57 | # | 引用
think 说:
super(); 是什么意思?去掉的话发现出错。
2019年8月 8日 14:54 | # | 引用
Leon.W 说:
https://es6.ruanyifeng.com/#docs/class-extends#super-%E5%85%B3%E9%94%AE%E5%AD%97
2019年8月 8日 15:25 | # | 引用
头文字zhu 说:
感觉不久的将来可以抛弃vue和react了,返璞归真?
2019年8月 9日 21:48 | # | 引用
super 说:
是啊,如果只有Chrome兼容,那意义不太大,在天朝非程序员用Chrome的不多
2019年8月11日 13:58 | # | 引用
rossroma 说:
与其这样,浏览器还不如直接集成Vue和React,以后写代码时,直接声明一下所使用Vue的版本号,不需要引入就能使用。
2019年8月12日 09:56 | # | 引用
Colorless 说:
在constructor里append元素chrome下会报错?
2019年8月12日 16:03 | # | 引用
小白 说:
问一下,跟iframe咋这么像呢?
2019年8月12日 23:11 | # | 引用
redbuck 说:
HTML标签,修改属性,视图直接会更新.
复制代码跑了下,修改属性内容不会变化.说明上述特性并不是HTMLElement这个类实现的.那如何监听属性变化?
实际上vue和react解决的都是数据=>视图这个过程的问题,如果不能解决这个问题,web Component的意义不是很大.
2019年8月14日 17:09 | # | 引用
哈哈哈哈 说:
腾讯视频播放器是这个实现的吗 chrome下
2019年8月16日 18:07 | # | 引用
sooge 说:
你说的是mvvm框架中的另一个核心功能,基于Object.defineProperty实现的数据绑定功能.
2019年8月22日 10:01 | # | 引用
阿凯 说:
阮一峰老师,有没有比较齐全的使用template方式构建web 组件的,组件单独一个文件,引用又在另一个组件的教程
2019年8月29日 13:53 | # | 引用
阿凯 说:
补充说一下,使用web component构建。不用与单文件的template,有没有办法实现,相当于纯前端实现吧
2019年8月29日 13:55 | # | 引用
阿凯 说:
请问web components能用在正式开发吗? 想在移动端中使用可以吗?。 刚刚看到说,兼容性还是有很大问题
2019年8月29日 15:37 | # | 引用
搞的微 说:
我司正在用,如果对兼容性要求比较高,不建议使用。
2019年8月29日 17:26 | # | 引用
阿凯 说:
嗯嗯,好的谢谢啦
2019年8月29日 22:23 | # | 引用
Can 说:
slot 也挺有用(常用)的啊,这个你应该提一下。
我觉得 HTML 可以直接写在一个变量里(可以用模板引用可变的参数),然后直接用 innerHTML 而不是每次都要用 template。
现在 Edge 好像还不支持(更别说IE),所以要排除或提示用户用Chrome(内核)浏览器了。
这个让我有理由不去花时间精力学习 Vue/React 了(很不喜欢 Webpack 的麻烦ness)
(我的个人网址就实践了Web Component,应该会离不开了)
2019年9月11日 00:14 | # | 引用
mel 说:
阮老师讲的通俗易懂~
2019年11月22日 11:50 | # | 引用
XboxYan 说:
个人用Web Component做的一个ui组件库,相比react、vue那些还是有很多优势的,比如纯原生、无需编译等等
https://xy-ui.codelabo.cn/docs
2019年11月30日 17:54 | # | 引用
dbstao 说:
像Vue的模版里<template/>难道不是原生组件的?(我一直以为是原生组件)。如果不是,里面有没有一些功能是用到原生组件的。还是说都是借鉴?
2019年12月 6日 17:05 | # | 引用
赵棱 说:
请问您是怎么跑起来的,我见了个文件夹,里边放一个component的html文件,一个用来渲染网页的html文件,但是丝毫没有效果,感觉没有import进来那个component的html文件
2019年12月17日 16:30 | # | 引用
timi 说:
angular打包出来就是这样的。然后再对组件进行了增强。当然angular还有其他厉害的功能
2019年12月24日 17:23 | # | 引用
大粒盐 说:
应该还是主要是兼容性问题吧,对于企业端应用还不错,毕竟可以指定chrome,对于互联网用户那就不太好了。当然vue已经足够简洁
2020年1月 9日 10:25 | # | 引用
原型 说:
他的意思 少了 MVVM 架构,web components 发展前景不大,因为现在 mvvm 大势所趋,而 需要对 web components 进行更多的丰富改造才行,不过既然改造了,那为什么不用vue 或 react 来得直接呢?
2020年1月15日 17:21 | # | 引用
chen 说:
通俗易懂,清晰明了是阮老师的风格很喜欢 赞~
2020年1月19日 10:54 | # | 引用
WilliamGui 说:
会点vue,这跟vue非常像,像vue实现的功能,而且vue实现起来比文中说的简单易用的多。
2020年1月31日 22:10 | # | 引用
MrDream 说:
vue解决的就是MVVM中(VM)数据驱动视图的驱动问题,如果不对webcomponets做进一步的封装,又回到了MVC时代
2020年4月 4日 23:39 | # | 引用
GM 说:
chrome 浏览器的组件就是用这个,整个和angular很像,但是Google也不更新了,所以国内基本上没有人在用;另外chrome浏览器项目管理很复杂
2020年4月15日 14:19 | # | 引用
zanwu 说:
别丢人了,这是因为vue是基于官方提供的WebComponents来的。
2020年5月 5日 18:25 | # | 引用
初阳 说:
那算倒退吧,还要操作dom也太麻烦了(在有vue的情况下)
2020年5月28日 00:05 | # | 引用
我只是路过 说:
Vue\React 等框架本质还是对原生JS的封装,不需要写复杂的原生js代码,简化了开发流程而已,编译后还是转成标准JS代码的,不然你以为浏览器认识 Vue\React 代码?
2020年6月 5日 17:54 | # | 引用
XDX 说:
五、添加样式 这一节用了:host,:host只在template作为影子元素时生效
参考https://developer.mozilla.org/zh-CN/docs/Web/CSS/:host
在成为影子元素前没有局部的概念,想要定义容器的样式需要按照往常的方式定义。
2020年6月 9日 20:53 | # | 引用
webH 说:
同问:Shadow DOM隐藏了什么?
2020年6月16日 13:57 | # | 引用
Will Xiao 说:
状态没有跟视图分离,直接操作dom很麻烦,vue、react这样的mv框架还是少不了。
样式安全封装在组件内部,不暴露任何class,有一些特殊情况下,需要修改样式,就不方便了。
感觉只是提供了基础的API,很多问题还是需要框架解决!除了上面两点,还要状态管理问题。
2020年6月28日 14:32 | # | 引用
null 说:
这个模板的用法感觉和 Underscore Template 非常相似啊。。
2020年7月11日 19:25 | # | 引用
herry 说:
这个还是有版权问题的,他们的作者不一样,除非协商双方都有利才能达成共识
2020年7月14日 10:41 | # | 引用
金 说:
能发告知如何打包或者 如何用js引入temp 。 就是最后面您说封装成js文件直接引用
2020年7月17日 16:23 | # | 引用
闫柏旭 说:
在js文件前面加上如下代码:
let target_div = document.createElement("div");
target_div.innerHTML = ''; //引号内填写的HTML代码
document.body.append(target_div);
2020年9月 1日 11:10 | # | 引用
和孔孟齐名的骑牛子 说:
2020年9月27日 09:19 | # | 引用
foolseward 说:
感觉w3c事事落后,像css的变量声明,弄个莫名其妙的 -- 来声明,还有方法注解之类的,肯定要出的东西,但是被人抢先,到时候不一定又想出什么奇葩的表现形式
2020年10月13日 17:05 | # | 引用
wenxin667 说:
这个和法律类似吧 在实践中,已经总结得出了一套经验,官方才能据此制定规范。规范提供的 永远是最基础的。
2020年11月 6日 11:59 | # | 引用
Agan 说:
看完文章和评论,尝试封装成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>
就可以了,正常显示~
2021年1月12日 11:47 | # | 引用
avantasia 说:
爸爸长得像儿子?
2021年2月18日 11:25 | # | 引用
叶小鱼 说:
阮老师,我们是公司使用了webcomponnets进行了开发,发现一个问题,在ios手机上,微信里面的img标签无法唤起长按识别的功能,安卓手机可以,请问下您觉得可能和什么有关呢,怎么解决呀~
2021年3月11日 17:48 | # | 引用
郭鹏松 说:
ES6里面新增的,可以看看阮一峰老师的ES6教程
2021年3月24日 21:20 | # | 引用
叶文 说:
2021年4月11日 17:25 | # | 引用
xyz 说:
哈哈,隐形战机你用眼睛能看到不,
2021年6月22日 10:50 | # | 引用
Brannua 说:
阮老师您好,关于您文末所说「用脚本把 <template/> 注入网页」,这样确实可以通过 dom 操作获取到模板结点并进行深克隆,但是既然已经用 js 脚本生成了 <template/> 模板,那为什么不在 js 里面直接使用呢 ?直接使用不是更符合逻辑么 ?为什么还要将 <template/> append 到 html 页面里去呢 ?在 html 页面里面保存模板有什么好处呢 ?是我理解有问题么 ?期待您的回复,谢谢老师
2022年1月11日 10:58 | # | 引用
ahuigo 说:
html直接写的代码是固定死的,不能拿出来复用。
template 是模板 —— 模板是提前准备好的,可以用来无限复用。
你也可以用js createElement 动态生成template, 很多时候没有直接引用template 方便,但是更灵活。
template 模板不用保存在html,工程化时应该放到单独的组件中
2022年9月 6日 16:05 | # | 引用
陶建国 说:
shadow Dom的诱惑力还是挺大的,有时候我们需要制造一个干净的组件。
2023年2月 7日 10:45 | # | 引用
DingSoung 说:
写了10年的iOS UIKit
表示这个跟iOS没差别
都是 new object, add object
看不出有什么优势,非常老套
2023年5月 5日 19:10 | # | 引用