说明
浏览器工作原理与实践专栏学习笔记
什么是组件化?
特点:对内高内聚,对外低耦合。
对内各个元素彼此紧密结合、相互依赖,对外和其他组件的联系最少且接口简单。
阻碍前端组件化的因素
比如:在页面中嵌入第三方内容时,需要确保第三方的内容样式不会影响到当前内容,同样也要确保当前的 DOM 不会影响到第三方的内容。
<style> p { background-color: brown; color: cornsilk } </style> <p>time.geekbang.org</p>
<style> p { background-color: red; color: blue } </style> <p>time.geekbang</p>
显然,CSS 的全局属性会阻碍组件化。
DOM 也是阻碍组件化的一个因素,因为在页面中任何地方都可以直接读取和修改 DOM。
WebComponent 组件化开发
上面我们了解到了:CSS 和 DOM 是阻碍组件化的两个因素,WebComponent 给出的解决思路
WebComponent 提供了对局部视图封装能力,可以让 DOM、CSSOM 和 JavaScript 运行在局部环境中,这样就使得局部的 CSS 和 DOM 不会影响到全局。
WebComponent 是怎么实现组件化的
Web Components 概念
Web Components旨在解决这些问题 — 它由三项主要技术组成,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。
Custom elements(自定义元素):一组JavaScript API,允许您定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们。
Shadow DOM(影子DOM):一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
HTML templates(HTML模板): <template> 和 <slot> 元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
Web Components 使用
实现 web component 的基本方法通常如下所示:
创建一个类或函数来指定web组件的功能,如果使用类,请使用 ECMAScript 2015 的类语法。
使用 CustomElementRegistry.define() 方法注册您的新自定义元素 ,并向其传递要定义的元素名称、指定元素功能的类、以及可选的其所继承自的元素。
如果需要的话,使用 Element.attachShadow() 方法将一个 shadow DOM 附加到自定义元素上。使用通常的 DOM 方法向 shadow DOM 中添加子元素、事件监听器等等。
如果需要的话,使用 <template> 和 <slot> 定义一个HTML模板。再次使用常规DOM方法克隆模板并将其附加到您的shadow DOM中。
在页面任何您喜欢的位置使用自定义元素,就像使用常规HTML元素那样。
Web Components 例子
要使用 WebComponent 需要实现下面三步:
1.使用 template 属性来创建模板。
利用 DOM 可以查找到模板的内容,但是模板元素是不会被渲染到页面上的,在模板的内部定义样式信息。
<!-- 定义模板样式 --> <template id="kxm-template"> <style> p { background-color: green; color: white; } div { width: 200px; background-color: blue; border: 3px solid red; border-radius: 10px; } </style> <div> <p>kxm</p> <p>kaimo313</p> </div> <script> function foo() { console.log('template log'); } </script> </template>
2.创建一个类
- 查找模板内容;
- 创建影子 DOM;
- 再将模板添加到影子 DOM 上。
可以把影子 DOM 看成是一个作用域,其内部的样式和元素是不会影响到全局的样式和元素的,而在全局环境下,要访问影子 DOM 内部的样式或者元素也是需要通过约定好的接口的。
<!-- 创建一个类 --> <script> class Kaimo313 extends HTMLElement { constructor() { super() // 获取组件模板 const content = document.querySelector('#kxm-template').content; // 创建影子DOM节点 const shadowDOM = this.attachShadow({ mode: 'open' }); // 将模板添加到影子DOM上 shadowDOM.appendChild(content.cloneNode(true)); } } // 使用 customElements.define 来自定义元素 customElements.define('kaimo-313', Kaimo313); </script>
3.使用该元素
<!-- 使用该元素 --> <kaimo-313></kaimo-313> <div> <p>kxm</p> <p>kaimo313</p> </div> <kaimo-313></kaimo-313>
完整的代码展示:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>12.Web Components 例子</title> </head> <body> <h1>kxm 测试 Web Components 例子</h1> <!-- 定义模板样式 --> <template id="kxm-template"> <style> p { background-color: green; color: white; } div { width: 200px; background-color: blue; border: 3px solid red; border-radius: 10px; } </style> <div> <p>kxm</p> <p>kaimo313</p> </div> <script> function foo() { console.log('template log'); } </script> </template> <!-- 创建一个类 --> <script> class Kaimo313 extends HTMLElement { constructor() { super() // 获取组件模板 const content = document.querySelector('#kxm-template').content; // 创建影子DOM节点 const shadowDOM = this.attachShadow({ mode: 'open' }); // 将模板添加到影子DOM上 shadowDOM.appendChild(content.cloneNode(true)); } } // 使用 customElements.define 来自定义元素 customElements.define('kaimo-313', Kaimo313); </script> <!-- 使用该元素 --> <kaimo-313></kaimo-313> <div> <p>kxm</p> <p>kaimo313</p> </div> <kaimo-313></kaimo-313> </body> </html>
使用影子 DOM 的输出效果
浏览器如何实现影子 DOM
影子 DOM 的作用
- 影子 DOM 中的元素对于整个网页是不可见的;
- 影子 DOM 的 CSS 不会影响到整个网页的 CSSOM,影子 DOM 内部的 CSS 只对内部的元素起作用。
影子 DOM 示意图
影子 DOM 都有一个 shadow root 的根节点
浏览器怎么实现 DOM API 无法直接查询到影子 DOM 的内部元素?
当通过 DOM 接口去查找元素时,渲染引擎会去判断 kaimo-313 属性下面的 shadow-root 元素是否是影子 DOM,如果是,那么就直接跳过 shadow-root 元素的查询操作。
浏览器怎么实现渲染出来的效果就是影子 DOM 内部定义的样式?
当生成布局树的时候,渲染引擎也会判断 kaimo-313 属性下面的 shadow-root 元素是否是影子 DOM,如果是,那么在影子 DOM 内部元素的节点选择 CSS 样式的时候,会直接使用影子 DOM 内部的 CSS 属性。