Don’t repeat yourself
凡是写过一些代码的程序猿都能够意识到应该避免重复的代码和逻辑。我们通过提取方法,提取抽象类等等措施来达到这一目的, 但是对于前端页面展示来说,如何试用,如何避免一遍有一边的重写相同的HTML和CSS?
有时候我们需要从引入别人写的代码,一般流程就是不断复用js文件,CSS文件,HTML片段,但是我们又需要关注这些引入的代码不会影响到我们已有的代码。
如果使用的是像Vue或像React这样的UI库,那么答案很简单:构建一个组件。 现在组件化在一些框架里越来越普遍,各种各样的框架的库也衍生出一个纷繁复杂、四分五裂的组件生态。这种分裂将一个团队钉死在某个特定的框架上,哪怕是时间和技术的迭代所需要的代价也不会很少。
再者,如果不使用React,Vue或其他最新最好的前端框架怎么办? 如果要编写普通HTML,CSS和JavaScript,该怎么办? 或者,想编写一个与框架无关的组件,并且无论其编写内容如何,都可以在任何Web应用程序中使用,这改如何实现?
上面提出的问题都可以用我们今天的主角 Web Components 来解决。
Web Components 是 W3C 正在向 HTML 和 DOM 规范添加的一套标准,它允许在Web 文档和 Web 应用程序中创建可重用的组件,这样做的目的是将基于组件的软件工程引入 Web 平台。
简单的来说就是通过使用它来以原生的方式实现组件化。创建具有封装功能的自定义元素,这些元素可以在任何web应用中使用。即使是不同团队不同项目不同的框架。
而且其中的每个组件能组织好它自身的 HTML 结构、CSS 样式、javascript 代码,并且不会干扰页面上的其他代码。全局覆盖的CSS防污染封装这也是 Web Components 最根本的特点和核心优势。
但是,就现在而言 Web Components 还是一门新技术,很多浏览器都没有原生实现它。
接下来我们就来介绍 Web Components 的标准以及解决了什么问题。
Custom Elements - 自定义元素
一组JavaScript API,将HTML页面的功能封装为自定义元素,
通过使用 customElementRegistry 来定义。要注册一个元素,通过window.customElements 中的 define 的方法来注册实例。
第一个参数为定义新元素的标签名。(注意这里为了避免和原生标签冲突,强制使用中划线连接)。
第二个参数为自定义元素的构造函数。
第三个参数为可选参数,仅有一个 extends 属性,用来指定继承的已创建的元素(比如HTMLParagraphElement, HTMLUListElement etc.)
- **Autonomous custom elements
**
我们先来定义一个自主定制元素,它不继承其他内建的HTML元素。你可以直接把它们写成HTML标签的形式,来在页面上使用
window.customElement.define('my-component', MyComponent)
<my-component></my-component>
- Customized built-in elements
它继承自基本的HTML元素。在创建时,用 extends 的值来指定所需扩展的元素,使用时,需要先写出基本的元素标签,并通过 is
属性指定自定义元素的名称
window.customElement.define('my-component', MyComponent, { 'extends': 'p' })
<p is="my-component"></p>
这个组件继承 HTMLParagraphElement接口,拥有 p 元素所有的特性,我们在此基础上可以拓展出自定义特性。
这里我们自定义的一个 的标签,同时用 MyComponent 类来修饰这个标签
下面我们来定义这个类
class MyComponent extends HTMLElement {
constructor() {
super()
// 下面可以写功能代码
}
}
这个自定义的 MyComponent 是继承自 nativeHTML 元素的类。
生命周期
在构造函数中,我们可以设定一些生命周期的回调函数,在特定的时间,这些回调函数将会被调用。
-
connectedCallback : 当 custom element首次被插入文档DOM时,被调用。
-
disconnectedCallback : 当 custom element从文档DOM中删除时,被调用。
-
adoptedCallback : 当 custom element被移动到新的文档时,被调用。
-
attributeChangeCallback : 当 custom element增加、删除、修改自身属性时,被调用
class MyComponent extends HTMLElement { constructor() { super() // 下面可以写功能代码 } connectedCallback() {} disconnectedCallback() {} adoptedCallback() {} attributeChangedCallback() {} }
生命周期的调用顺序如下
constructor -> attributeCHangedCallback -> connectedCallback -> disconnectedCallback
这里我们可能好奇为什么attributeChangedCallback在connectedCallback之前执行。
因为组件上的属性是用来初始化配置,当组件被插入DOM时候,这些配置就需要可以被访问。所以attributeChangedCallback要在connectedCallback之前执行。
每当元素的属性变化时, attributeChangeCallback() 回调函数会执行。我们可以查看属性的名称、旧值与新值,以此来对元素属性做单独的操作。
class MyComponent extends HTMLElement {
constructor() {
super()
// 下面可以写功能代码
}
static get observedAttributes() {
return ['w']
}
attributeChangeCallback(name, oldVal, newVal) {
if (name === 'w') {
// 功能代码
}
}
}
除了生命周期的一些方法,我们还可以定义一些方法供外部调用
class MyComponent extends HTMLElement {
constructor() {
super()
// 下面可以写功能代码
}
connectedCallback() {}
doSomething() {}
}
// 在外部使用
const element = document,querySelector('my-component')
element.doSomething()
Shadow DOM - 影子DOM
Shadow DOM可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。
- Shadow host:一个常规 DOM节点,Shadow DOM 会被附加到这个节点上。
- Shadow tree:Shadow DOM内部的DOM树。
- Shadow boundary:Shadow DOM结束的地方,也是常规 DOM开始的地方。
- Shadow root: Shadow tree的根节点。
使用同样的方式来操作 Shadow DOM,就和操作常规 DOM 一样——例如添加子节点、设置属性,以及为节点添加自己的样式(例如通过 element.style
属性),或者为整个 Shadow DOM 添加样式(例如在 <style>
元素内添加样式)。不同的是,Shadow DOM 内部的元素始终不会影响到它外部的元素(除了 :focus-within
),这为封装提供了便利
我们可以使用 Element.attachShadow()
方法来将一个 shadow root 附加到任何一个元素上。它接受一个配置对象作为参数,该对象有一个 mode
属性,值可以是 open
或者 closed
:
open
表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM
let shadow = this.attachShadow({mode: 'open'});
如果你将一个 Shadow root 附加到一个 Custom element 上,并且将 mode
设置为 closed
,那么就不可以从外部获取 Shadow DOM 了——myCustomElem.shadowRoot
将会返回 null
。浏览器中的某些内置元素就是如此,例如 ,包含了不可访问的 Shadow DOM
video作为一个独立的标签来展示,显示播放和暂停视频的空间,但是当我们在浏览器中查看该标签的时候,是看不到这些控件,这些控件实际上是video元素的Shadow DOM一部分,默认是隐藏。
将 Shadow DOM 附加到一个元素之后,就可以使用 DOM APIs对它进行操作,就和处理常规 DOM 一样。
HTML templates - HTML模板
使用