各浏览器支持情况,更多浏览器去caniuse.com于 2011 年面世的 Web Components 是一套功能组件,让开发者可以使用 HTML、CSS 和 JavaScript 创建可复用的组件。这意味着你无需 React 或 Angular 等框架也能创建组件。不仅如此,这些组件还都可以无缝集成到这些框架中。
—— 本文参考地址
Talk is cheap, Show me the code
class MyElement extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
// here the element has been inserted into the DOM
}
}
window.customElements.define('my-element', MyElement);
如上,就完成了一个原生 WebComponents 的定义
HTMLElement为浏览器原生对象,无需引用
connectedCallback为钩子函数,相当于vue中的mounted
const myElement2 = document.createElement('my-element');
document.body.appendChild(myElement2);
// MyElement是一个ES6类, 所以还可以这样
// const el = customElements.get('my-element');
// const myElement2 = new el();
// document.body.appendChild(myElement2);
运行代码,结果如图
钩子(生命周期)函数
- constructor:构造函数,元素创建时(new、createElement)触发
- connectedCallback => mounted(vue)
- disconnectedCallback => destroyed
- adoptCallback:document.adoptNode(element)时触发
- attributeChangedCallback:元素属性变化时触发
原文:每当属性更改已添加到 observedAttributes 数组时都会调用此方法
class MyElement extends HTMLElement {
constructor() {
super();
console.info('constructor')
}
static get observedAttributes() {
return ['foo', 'bar'];
}
connectedCallback() {
console.info('connectedCallback')
// here the element has been inserted into the DOM
}
attributeChangedCallback(attr, oldVal, newVal) {
console.info('attributeChangedCallback')
switch(attr) {
case 'foo':
// do something with 'foo' attribute
case 'bar':
// do something with 'bar' attribute
}
}
}
window.customElements.define('my-element', MyElement);
const myElement2 = document.createElement('my-element');
myElement2.setAttribute('foo', '996')
document.body.appendChild(myElement2);
运行结果
constructor
attributeChangedCallback
connectedCallback
以上即为生命周期方法的执行顺序
- whenDefined: 当html不是由js追加到dom上,而是直接编写在html里时,当代码执行到customElements.define()时触发
重写RadioBox
<style>
my-element { width: 20px; height: 20px; display: block; border-radius: 50%; box-sizing: border-box; border: 1px solid #eaeaea; }
</style>
<my-element></my-element>
<my-element checked="yes"></my-element>
class MyElement extends HTMLElement {
static get observedAttributes() {
return ['checked', 'disabled'];
}
constructor() {
super();
}
attributeChangedCallback(attr, oldVal, newVal) {
switch(attr) {
case 'checked':
newVal == 'yes'
? this.style.border = '7px solid #4ac153'
: this.style.border = '1px solid #eaeaea'
break;
case 'disabled':
break;
}
}
connectedCallback () {
this.onclick = e => {
if (this.getAttribute('checked') == 'yes') this.setAttribute('checked', 'no')
else this.setAttribute('checked', 'yes')
}
}
}
window.customElements.define('my-element', MyElement);
运行效果
setter 和 自定义方法
setter:提供外部可访问的、可修改的属性
自定义方法: 自定义组件中定义自定义方法,可在外部方法
<my-element value=2 title="玩手机"></my-element>
<my-element value=3 title="睡觉" checked="yes" id="my3"></my-element>
class MyElement extends HTMLElement {
...
// 自定义组件外部方法
getValue() {
return this.getAttribute('checked') == 'yes' && this.getAttribute('disabled') != 'yes'
? this.getAttribute('value')
: null
}
// 使用 setter 就能将一个 property 映射到一个 attribute 属性上
set disabled(isDisabled) {
this.setAttribute('disabled', isDisabled);
}
get disabled() {
return this.getAttribute('disabled') == 'yes';
}
}
window.customElements.define('my-element', MyElement);
console.info(document.querySelector('#my3').getValue())
document.querySelector('#my3').disabled = 'yes'
console.info(document.querySelector('#my3').getValue())
> 3
> null
Shadow DOM
使用 Shadow DOM 时,自定义元素的 HTML 和 CSS 会完全封装在组件内部。
其实 Shadow DOM 也用在几个原生 HTML 元素上,如<video><svg>
Shadow DOM 还提供真正的作用域 CSS,可以(设置为)不从周围的 CSS 继承任何值
class RadioBox extends HTMLElement {
// 构造函数
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `<p>Hello world</p>`;
}
}
window.customElements.define('my-popup', MyPopup);
Shadow DOM在Chrome中的显示
特性
一、HTML:自带懒加载
- 在实际插入 DOM 树之前,它将不会被显示或解析,包括js、css
二、JS
- this.attachShadow({mode: 'open'})
open:可在开发工具中检查,并通过查询、配置任何公开的 CSS 属性或监听它抛出的事件来交互
close: 不允许组件的使用者以任何方式与它交互,也无法监听到它抛出的事件 - this.shadowRoot
可以用它来调用document上所有操作DOM的方法,如querySelector等
三、 CSS
- 组件的所有 CSS 都在<style>标签内定义
- 可以使用<link rel =“stylesheet”>调用外部样式
- :host { } 表示组件自身
- :host { all: initial; } 禁止组件使用外围css属性。默认情况下,自定义元素会从周围的 CSS 继承一些属性,例如 color 和 font 等
- #title { color: val(--mycolor) } 无法从外部设置自定义元素内的任何节点的样式,除非主动暴露css变量:--xxx
// 把样式封闭进组件
class RadioBox extends HTMLElement {
// 构造函数
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
:host { cursor: pointer; --pop-color: #4ac153; /*css变量*/ }
:host::after { content: ' '; clear:both; display: block; }
:host i { width: 20px; height: 20px; display: block; border-radius: 50%; box-sizing: border-box; border: 1px solid #eaeaea; float: left; }
:host([disabled=yes]) i { background-color: #eaeaea; border-color: #ccc; }
:host([checked=yes]) i { border: 7px solid var(--pop-color); }
</style>
<i></i>
`;
}
connectedCallback () {
this.onclick = e => {
if (this.getAttribute('disabled') == 'yes') return
this.setAttribute('checked', this.getAttribute('checked') == 'yes'
? 'no'
: 'yes')
}
}
}
window.customElements.define('radio-box', RadioBox);
Slot
为单选按钮添加文本
<radio-box value="phone">玩手机</radio-box>
<radio-box checked="yes" value="sleep">睡觉</radio-box>
class RadioBox extends HTMLElement {
// 构造函数
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
:host { cursor: pointer; --pop-color: #4ac153; /*css变量*/ }
:host::after { content: ' '; clear:both; display: block; }
:host i { width: 20px; height: 20px; display: block; border-radius: 50%; box-sizing: border-box; border: 1px solid #eaeaea; float: left; }
:host([disabled=yes]) i { background-color: #eaeaea; border-color: #ccc; }
:host([checked=yes]) i { border: 7px solid var(--pop-color); }
/* slot 样式 */
::slotted(*) { color: #333; }
slot { line-height: 22px; padding-left: 10px; display: inline; }
</style>
<i></i><slot>选项</slot>
`;
}
// mounted
connectedCallback () {
this.onclick = e => {
if (this.getAttribute('disabled') == 'yes') return
this.setAttribute('checked', this.getAttribute('checked') == 'yes'
? 'no'
: 'yes')
}
}
getValue() {
return this.getAttribute('checked') == 'yes' && this.getAttribute('disabled') != 'yes'
? this.getAttribute('value')
: null
}
// 使用 setter 就能将一个 property 映射到一个 attribute 属性上
set disabled(isDisabled) {
this.setAttribute('disabled', isDisabled);
}
get disabled() {
return this.getAttribute('disabled') == 'yes';
}
set checked(val) {
this.setAttribute('checked', val);
}
get checked() {
return this.getAttribute('checked') == 'yes';
}
}
window.customElements.define('radio-box', RadioBox);
效果
添加 radioGroup
<radio-group>
<radio-box value="phone">玩手机</radio-box>
<radio-box checked="yes" value="sleep">睡觉</radio-box>
</radio-group>
<script>
console.info(document.querySelector('radio-group').values) // -> ["sleep"]
</script>
class radioGroup extends HTMLElement {
constructor () {
super()
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `<slot></slot>`;
}
connectedCallback () {
}
get values () {
let v = []
this.shadowRoot.querySelector('slot').assignedNodes().forEach(item => {
if (item.nodeName == "RADIO-BOX")
item.getValue() && v.push(item.getValue())
})
return v
}
}
window.customElements.define('radio-group', radioGroup)
模板元素
当 Web 组件需要根据不同情况呈现完全不同的标记时,可以使用不同的模板来完成此任务
<template>内的html不渲染、js不执行、不引入外部资源
class ResultTemplate extends HTMLElement {
// 构造函数
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<template id="good">
<style>.good{color:green}</style>
<p class="good">是个好选择</p>
<script>console.info('good good')</script>
</template>
<template id="bad">
<style>.bad{color:red}</style>
<p class="bad">对身体不好</p>
<script>console.info('bad bad')</script>
</template>
<div id="container">
这样做:
</div>
`;
}
connectedCallback() {
setTimeout(() => {
// 不用cloneNode,<template>内容将会被移动,而不是复制
const content = this.shadowRoot.querySelector('#good').content.cloneNode(true);
this.container = this.shadowRoot.querySelector('#container');
this.container.appendChild(content); // 只有在此时才会渲染
}, 2000)
}
}
window.customElements.define('result-template', ResultTemplate);
执行后发现,css被加载了,但js并没有被执行。尝试引入js文件,也并没有被引用。
扩展原生元素 与旧浏览器
扩展原生元素,原文说浏览器支持情况更差,具体不再研究,感兴趣的可以看本文
旧浏览器兼容
npm install --save @webcomponents/webcomponentsjs
网友评论