前端开发中组件化开发是一种趋势,方便界面中各种自定义组件的管理与复用。现在流行的 React 和 Vue 等框架都是组件化开发的。目前前端原生也支持一定程度上的组件化开发,这个称为Web Components,这篇文章将对相关内容做个说明。
Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。
原生的HTML标准中提供很多的组件,比如 p 、div 、button 这些,这些元素的默认样式和功能都朴素简单了,通常使用时我们需要根据功能需求再编写很多样式和脚本,大多数时候我们甚至需要将一些空间嵌套组合使用。这样的开发对于这些自己设计的组件的管理、维护、复用等工作都是比较麻烦的。
比较方便的一种方式是所有组件相关的html和css代码都封装到js中,这样每个组件就是一份js代码了,使用时只要调用相关函数将传教组件插入到DOM中就行。目前前端原生的组件化开发最根本的也是用js,再此基础上针对组件化需求添加了一些新特性。这整一个被称为Web Components。
Web Components主要有 四部分 三部分 组成:
Web Components 中涉及的一些技术可以在MDN上找到说明:
https://developer.mozilla.org/zh-CN/docs/Web/Web_Components
这东西就是组件化开发的核心了,用来创建自定义组件。这个其实很简答,创建一个继承自HTMLElement的类就行:
class YourComponent extends HTMLElement {
constructor() {
super();
// 组件的功能代码写在这里
}
}
接下来我们需要使用 CustomElementRegistry.define()
方法将上面的类和一个自定义的标签绑定,这样你就可以在页面中直接使用这里的标签来使用自定义的组件了:
customElements.define('your-component', YourComponent);
// 官方规定自定义标签必须大于一个单词,单词间用-连接
将两者结合一下就是下面的样子:
customElements.define('your-component',
class extends HTMLElement {
constructor() {
super();
// 组件的功能代码写在这里
}
}
);
知道上面两点内容后就可以创建自己的组件来使用了:
上图演示中用上了自定义的标签元素,这个内部元素内部其实就是在声明的类中用js代码创建的。上面演示中自定义组件内部的内容是在类中写死的,实际上我们希望属性是可以在使用时自由设置的。可以通过下面方式来自由设置属性:
除了上面的基础用法Custom elements还有一些生命周期相关的回调函数可用:
customElements.define('your-component',
class extends HTMLElement {
static get observedAttributes() { return ['要监听的属性列表'] } // 配合下面attributeChangedCallback()使用
constructor() {
super();
}
connectedCallback() {} // 当组件首次被插入文档DOM时被调用
disconnectedCallback() {} // 当组件从文档DOM中删除时被调用
adoptedCallback() {} // 当组件被移动到新的文档时被调用
attributeChangedCallback(name, oldValue, newValue) {} // 当组件增加、删除、修改自身属性时被调用
}
);
这东西主要用于让组件内的各个元素和DOM隔离,这样自定义的组件才能真正成为独立的组件,不会被页面中其它样式等的污染。
在上一节其实我们已经实现了自定义组件,但是这里还存在一个比较大的问题。上面的自定义组件内部的各种元素对外都是可见的,这会引起很多问题,比如内部的元素会被外部样式所修改。这时候就要用到Shadow DOM了:
上面的 attachShadow()
中mode参数可以设置为 closed
或 open
,区别在于页面中JS对shadow DOM内部元素的访问性:
前面内容中创建组件用的都是JS,但只用JS来创建组件的话组件的结构和样式稍微复杂点就变得很麻烦了,这个时候要用上HTML templates了,它可以让你用原生html、css、js语言来编写组件。HTML templates结合前面的内容使用时差不多是下面这样的结构:
<template id='your-component-template'>
template>
<script>
customElements.define('your-component',
class extends HTMLElement {
constructor() {
super();
let template = document.getElementById('your-component-template');
let templateContent = template.content;
const shadowRoot = this.attachShadow({ mode: 'closed' });
shadowRoot.appendChild(templateContent.cloneNode(true)); // 克隆template内部内容添加到ShadowDOM中
}
}
);
script>
上面的演示中可以看到有了HTML templates之后编写复杂的自定义组件就方便多了。另外template中的样式中可以使用 :defined
:host
:host()
:host-context()
,比如下面这样:
:defined {
/* 匹配任何已定义的元素,包括内置元素和使用CustomElementRegistry.define()定义的自定义元素 */
}
:host {
/* 选择 shadow DOM 的 shadow host ,内容是它内部使用的 CSS( containing the CSS it is used inside ) */
}
:host(xxx) {
/* 选择 shadow DOM 的 shadow host ,内容是它内部使用的 CSS (这样您可以从 shadow DOM 内部选择自定义元素)— 但只匹配给定方法的选择器的 shadow host 元素 */
}
:host-context(xxx) {
/* 选择 shadow DOM 的 shadow host ,内容是它内部使用的 CSS (这样您可以从 shadow DOM 内部选择自定义元素)— 但只匹配给定方法的选择器匹配元素的子 shadow host 元素 */
}
除了上面的基础使用外HTML templates还提供了一个更进一步的功能slot。这东西可以让你在templates的html结构中插入一个插槽,这样你在使用自定义组件的时候可以向这个插槽中插入各种各样的东西:
如果templates中只有一个slot,那就可以不用指定name,你在网页上使用自定义标签之间的所有内容都会放到这个slot间。
上面演示中组件和页面都是在同一个文件中的,实际使用中我们通常是希望组件可以封装在独立的文件中的。
在HTML Imports弃用之前我们可以把组件相关的代码(比如template、script)这些写到一个html文件中,然后在真实页面头部中使用 方式引用组件。
在HTML Imports已经被废弃并且ES Module还无法import html文件的现在我们只能把template部分代码作为字符串嵌入到js代码中来使用了。最终组件就是一个独立的js文件,比如下面这样:
需要注意的是下面演示中JS使用了ES module的方式,该方式必须使用服务器,最简单的比如VS Code中安装Live Server扩展来处理。
export default class YourComponent extends HTMLElement {
static get observedAttributes() { return ['color'] }
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
`;
}
get color() {
return this.getAttribute('color') || 'blue'; // 如果没有设置颜色则返回bule作为默认颜色
}
set color(value) {
this.setAttribute('color', value);
}
connectedCallback() {
this.text = this.shadowRoot.getElementById('text');
}
attributeChangedCallback(name, oldValue, newValue) {
if (name == 'color' && this.text) {
this.text.style.color = newValue;
}
}
}
if (!customElements.get('your-component')) {
customElements.define('your-component', YourComponent);
}
上面是个简单的组件,仅仅只是控制了下文字的颜色,不过基本上展示出了原生单文件组件的一些用法,真正应用的时候更多的只是拓展组件结构,处理更多的属性。
上面演示中对于color这个属性,可以通过组件标签中添加属性来设置,也可以在页面上用CSS进行设置,另外虽然上面没有演示,其实也可以通过js使用 color(value) 或 setAttribute(name,value) 方法来设置。
export default class YourComponent extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
`;
}
}
if (!customElements.get('your-component')) {
customElements.define('your-component', YourComponent);
}
上面的示例主要使用了 :host() 和 :host-context() 这两个选择器,很多时候纯css也可以实现很多功能。
上面把模板的代码当作字符串来处理最终用是能用了,但是代码编写、修改的时候就比较纠结了。好在在VScode中可以安装es6-string-html扩展在一定程度上改善这个问题:
安装该插件后只要在字符串前面加上 /*html*/
就可以以Html形式渲染字符串内容,编写和修改上也一般的代码编写差不多。目前来说唯一没法实现的就是格式化操作。
Web Components 使用总体来说并不复杂。就我个人而言比起Vue、React这类需要编译的框架,我更加喜欢浏览器原生就能解析使用的开发方式。
更多内容可以参考MDN相关的例程:
https://github.com/mdn/web-components-examples
另外也可以参考第三方的原生组件库:
https://github.com/XboxYan/xy-ui