深入理解Web Components
1. 什么是Web Components?它的出现原因?为什么要学习它?Web Components 是一个浏览器原生支持的组件化方案,允许你创建新的自定义、可封装、可重用的HTML 标记。不用加载任何外部模块,直接就可以在浏览器中跑。它的出现原因?因为早期组件生态很乱,有各种各样的框架和库,都有各自的规范,导致一个团队切换框架很困难 。为了解决这种分化的形式,让 Web 组件模型统一化,所以才有
1. 什么是Web Components?它的出现原因?为什么要学习它?
Web Components 是一个浏览器原生支持的组件化方案,允许你创建新的自定义、可封装、可重用的HTML 标记。不用加载任何外部模块,直接就可以在浏览器中跑。
它的出现原因?因为早期组件生态很乱,有各种各样的框架和库,都有各自的规范,导致一个团队切换框架很困难 。为了解决这种分化的形式,让 Web 组件模型统一化,所以才有了Web Components规范的出现。目标是提供一种权威的、浏览器能理解的方式来创建组件。
为什么要学习它?用一句话总结就是回顾历史展望未来。
2011年提出Web Components概念,React诞生
2013年 Chrome 和 Opera 又联合提出了推出的 V0 版本的 Web Components 规范,React开源
2014年Vue诞生
2016年Web Components 推进到了 V1 版本
由于浏览器兼容性、和主流框架开发效率等等问题导致现在几乎使用不到它,但我们可以学习它的思想,也许未来就会变的有用?
2. Web Components 由三种技术组成
Custom Elements
可以创建一个自定义标签。根据规范,自定义元素的名称必须包含连词线”-“,用与区别原生的 HTML 元素。
<body>
<user-card></user-card>
<script>
class UserCard extends HTMLElement {
constructor() {
super();
var el = document.createElement('p');
el.classList.add('name');
el.innerText = 'User Name';
this.append(el);
}
}
window.customElements.define('user-card', UserCard);
</script>
</body>
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k1Av4lYZ-1621597650365)(/Users/mybells/Desktop/%E5%85%AC%E5%8F%B8%E5%89%8D%E7%AB%AF%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%B7%B2%E5%AE%8C%E6%88%90%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%9B%BE%E7%89%87/webcomponents%E5%AD%A6%E4%B9%A0/image-20210504213119362.png)]](https://i-blog.csdnimg.cn/blog_migrate/b5d5fe46ebf5e5bbdfc253c3ad5c20dd.png)
class UserButton extends HTMLButtonElement {
constructor() {
super();
}
}
customElements.define('user-button', UserButton, { extends: "button" });
<button is="user-button">
</button>
使用生命周期回调函数
在custom element的构造函数中,可以指定多个不同的回调函数,它们将会在元素的不同生命时期被调用:
connectedCallback:当 custom element首次被插入文档DOM时,被调用。disconnectedCallback:当 custom element从文档DOM中删除时,被调用。adoptedCallback:当 custom element被移动到新的文档时,被调用。attributeChangedCallback: 当 custom element增加、删除、修改自身属性时,被调用。
Shadow DOM
Shadow DOM是对DOM的一个封装。可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。
<style>
p {
color: blueviolet;
}
</style>
<p>不会影响shadow样式</p>
<script>
const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'closed'});// header.shadowRoot === null
// const shadowRoot = header.attachShadow({mode: 'open'});// header.shadowRoot === shadowRoot #document-fragment
shadowRoot.innerHTML = '<p>Hello Shadow DOM</p>';
document.querySelector('body').append(header);// shadowRoot.host === header
</script>
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QMU9AZAI-1621597589953)(/Users/mybells/Desktop/%E5%85%AC%E5%8F%B8%E5%89%8D%E7%AB%AF%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%B7%B2%E5%AE%8C%E6%88%90%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%9B%BE%E7%89%87/%E5%AD%A6%E4%B9%A0%E4%BD%BF%E7%94%A8webcomponents/image-20210506170009819.png)]](https://i-blog.csdnimg.cn/blog_migrate/daca3706ffe54a414ad08e970be40149.png)
修改shadow dom内部样式:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-roe3zfOO-1621597589954)(/Users/mybells/Desktop/%E5%85%AC%E5%8F%B8%E5%89%8D%E7%AB%AF%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%B7%B2%E5%AE%8C%E6%88%90%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%9B%BE%E7%89%87/%E5%AD%A6%E4%B9%A0%E4%BD%BF%E7%94%A8webcomponents/image-20210507114101311.png)]](https://i-blog.csdnimg.cn/blog_migrate/cb0b58ac0837aad02c1080be45a86ae2.png)
templates and slots
可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
- 被使用前不会被渲染。
- 被使用前对页面其他部分没有影响,脚本不会运行,图像不会加载,音频不会播放。
<body>
<p>会影响外部样式</p>
<template id="my-paragraph">
<style>
p{color: red;}
</style>
<p>My paragraph</p>
</template>
<my-paragraph></my-paragraph>
<script>
customElements.define('my-paragraph',
class extends HTMLElement {
constructor() {
super();
let template = document.getElementById('my-paragraph');
let templateContent = template.content.cloneNode(true);
this.appendChild(templateContent);
}
})
</script>
</body>
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6UQBWgIy-1621597589956)(/Users/mybells/Desktop/%E5%85%AC%E5%8F%B8%E5%89%8D%E7%AB%AF%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%B7%B2%E5%AE%8C%E6%88%90%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%9B%BE%E7%89%87/%E5%AD%A6%E4%B9%A0%E4%BD%BF%E7%94%A8webcomponents/image-20210506170311517.png)]](https://i-blog.csdnimg.cn/blog_migrate/7145d24206d12d1864b18a77de358e59.png)
slot的使用:
<body>
<style>
p{color: blueviolet;}
</style>
<p>会影响外部样式</p>
<template id="my-paragraph">
<style>
p{color: red;}
</style>
<p>My paragraph</p>
<slot name="my-text">My default text</slot>
</template>
<my-paragraph>
<p slot="my-text">slot text</p>
</my-paragraph>
<script>
customElements.define('my-paragraph',
class extends HTMLElement {
constructor() {
super();
let template = document.getElementById('my-paragraph');
let templateContent = template.content.cloneNode(true);
this.attachShadow({mode: 'open'}).appendChild(templateContent);
}
})
</script>
</body>
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ff0KSkdH-1621597589957)(/Users/mybells/Desktop/%E5%85%AC%E5%8F%B8%E5%89%8D%E7%AB%AF%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%B7%B2%E5%AE%8C%E6%88%90%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%9B%BE%E7%89%87/%E5%AD%A6%E4%B9%A0%E4%BD%BF%E7%94%A8webcomponents/image-20210506172255519.png)]](https://i-blog.csdnimg.cn/blog_migrate/7fdc54d6626ce808adc704db02329873.png)
3. 实现一个计数器组件
<body>
<template id="button-counter">
<style>
p{color: blueviolet;}
</style>
<span id="counter"></span>
<button id="button"></button>
</template>
<button-counter counter="0" label="增加1"></button-counter>
<script>
document.querySelector('button-counter').buttonCounter=(val)=>{}
// document.querySelector('button-counter').addEventListener('test', (val)=>{
// console.log(val.detail.counter);
// })
class ButtonCounter extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({mode: 'open'});
let el = document.querySelector('#button-counter').content.cloneNode(true);
shadowRoot.appendChild(el);
this.$counter = shadowRoot.querySelector('#counter');
this.$button = shadowRoot.querySelector('#button');
this.$button.addEventListener('click', () => {
this.counter++;
this.buttonCounter(this.counter);
// this.dispatchEvent(
// new CustomEvent('test', {
// detail: {counter: this.counter},
// })
// );
});
}
get label() {
return this.getAttribute('label');
}
set label(value) {
this.setAttribute('label', value);
}
get counter() {
return this.getAttribute('counter');
}
set counter(value) {
this.setAttribute('counter', value);
}
static get observedAttributes() {
return ['label', 'counter'];
}
attributeChangedCallback(name, oldVal, newVal) {
console.log('attributeChangedCallback');
this.render();
}
render() {
this.$button.innerHTML = this.label;
this.$counter.innerHTML = `You clicked me ${this.counter} times`;
}
connectedCallback() {
console.log('connectedCallback');
}
disconnectedCallback() {
console.log('disconnectedCallback');
}
adoptedCallback() {
console.log('adoptedCallback');
}
}
customElements.define('button-counter', ButtonCounter)
</script>
</body>
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5mJFsmI0-1621597589958)(/Users/mybells/Desktop/%E5%85%AC%E5%8F%B8%E5%89%8D%E7%AB%AF%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%B7%B2%E5%AE%8C%E6%88%90%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%9B%BE%E7%89%87/webcomponents%E5%AD%A6%E4%B9%A0/image-20210505130801797.png)]](https://i-blog.csdnimg.cn/blog_migrate/f721ca26b0ff36af77d2b90e556df490.png)
4. Web Components vs Vue Components
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BkQAeKfI-1621597589958)(/Users/mybells/Desktop/%E5%85%AC%E5%8F%B8%E5%89%8D%E7%AB%AF%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%B7%B2%E5%AE%8C%E6%88%90%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%9B%BE%E7%89%87/%E5%AD%A6%E4%B9%A0%E4%BD%BF%E7%94%A8webcomponents/image-20210507111257968.png)]](https://i-blog.csdnimg.cn/blog_migrate/69e8563d53b86284622a8a56cf4cb8e6.png)
| Vue | Web Component |
|---|---|
| data | 实例属性 |
| props | attributes |
| watch | observedAttributes attributeChangedCallback |
| computed | getters |
| methods | class methods |
| mounted | connectedCallback |
| destroyed | disconnectedCallback |
| style scoped | template中的style |
| template | template |
computed: {
name() {
return this.planet.name || '';
},
}
get name() {
return this.planet.name || '';
}
components: {
"planet-summary": PlanetSummary
}
customElements.define("planet-summary", PlanetSummary);
watch: {
altitude(newValue, oldValue) {
...
}
}
static get observedAttributes() {
return ['altitude'];
}
attributeChangedCallback(name, oldValue, newValue) {
}
性能比较
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GmIHvlpM-1621597589959)(/Users/mybells/Desktop/%E5%85%AC%E5%8F%B8%E5%89%8D%E7%AB%AF%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%B7%B2%E5%AE%8C%E6%88%90%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%9B%BE%E7%89%87/%E5%AD%A6%E4%B9%A0%E4%BD%BF%E7%94%A8webcomponents/line-stack%20(2)].png)](https://i-blog.csdnimg.cn/blog_migrate/fc3fa46c5d7890e8206a2c59e78f40d2.png)
从上图可以看出在浏览器中组件越多,vue的js计算时间比web components越少,性能越好。原因大概是因为
vue是通过 模板->虚拟DOM->真实DOM 来实现自定义组件的。
web components是在浏览器中通过 自定义组件->Shadow Dom 实现自定义组件的。
导致了浏览器中vue的性能比web components性能优秀。
5. 优点与缺点
- 浏览器原生支持,不需要引入额外的第三方库
- 语义化
- 复用性,移植性高
- 不同团队不同项目可以共用组件
缺点
- 需要操作DOM
- 目前浏览器兼容性、性能方面不够友好
- 和外部css交互比较难
6. 浏览器兼容
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rv5ZU0Z0-1621597589960)(/Users/mybells/Desktop/%E5%85%AC%E5%8F%B8%E5%89%8D%E7%AB%AF%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%B7%B2%E5%AE%8C%E6%88%90%E6%96%87%E6%A1%A3%E6%95%B4%E7%90%86/%E5%9B%BE%E7%89%87/%E5%AD%A6%E4%B9%A0%E4%BD%BF%E7%94%A8webcomponents/image-20210510101144361.png)]](https://i-blog.csdnimg.cn/blog_migrate/faebb8317636228d713df6559cf3cd57.png)
兼容性不好的浏览器可以通过引入webcomponentsjs来polyfills。
<!-- load webcomponents bundle, which includes all the necessary polyfills -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
<!-- load the element -->
<script type="module" src="my-element.js"></script>
<!-- use the element -->
<my-element></my-element>
7. 基于web components的框架
LitElement
LitElement是一个快速、轻量级的 Web UI 框架。使用 lit-html 来渲染元素。
Polymer
Polymer是一款实用、基于事件驱动、封装性和交互性强的 Web UI 框架。
Omi
Omi是基于 Web 组件的跨框架跨平台框架 。移动端&桌面&小程序。
更多推荐



所有评论(0)