共计 7157 个字符,预计需要花费 18 分钟才能阅读完成。
当初的前端开发根本离不开 React、Vue 这两个框架的撑持,而这两个框架上面又衍生出了许多的自定义组件库:
- Element(Vue)
- Ant Design(React)
这些组件库的呈现,让咱们能够间接应用曾经封装好的组件,而且在开源社区的帮忙下,呈现了很多的模板我的项目(vue-element-admin、Ant Design Pro),能让咱们疾速的开始一个我的项目。
尽管 React、Vue 为咱们的组件开发提供了便当,然而这两者在组件的开发思路上,一个是借鉴的 JSX 语法,一个是特有的单文件模板的语法,两者的指标都是想提供一种组件的封装办法。毕竟都有其原创的货色在外面,和咱们刚开始接触的 Web 根底的 HTML、CSS、JS 的形式还是有些出入的。明天介绍的就是,通过 HTML、CSS、JS 的形式来实现自定义的组件,也是目前浏览器原生提供的计划:Web Components。
什么是 Web Components?
Web Components 自身不是一个独自的标准,而是由一组 DOM API 和 HTML 标准所组成,用于创立可复用的自定义名字的 HTML 标签,并且能够间接在你的 Web 利用中应用。
代码的复用始终都是咱们谋求的指标,在 JS 中可复用的代码咱们能够封装成一个函数,然而对于简单的 HTML(包含相干的款式及交互逻辑),咱们始终都没有比拟好的方法来进行复用。要么借助后端的模板引擎,要么借助已有框架对 DOM API 的二次封装,而 Web Components 的呈现就是为了补足浏览器在这方面的能力。
如何应用 Web Components?
Web Components 中蕴含的几个标准,都已在 W3C 和 HTML 规范中进行了规范化,次要由三局部组成:
- Custom elements(自定义元素):一组 JavaScript API,用来创立自定义的 HTML 标签,并容许标签创立或销毁时进行一些操作;
- Shadow DOM(影子 DOM):一组 JavaScript API,用于将创立的 DOM Tree 插入到现有的元素中,且 DOM Tree 不能被内部批改,不必放心元素被其余中央影响;
- HTML templates(HTML 模板):通过
<template>
、<slot>
间接在 HTML 文件中编写模板,而后通过 DOM API 获取。
Custom elements(自定义元素)
浏览器提供了一个办法:customElements.define()
,来进行自定义标签的定义。该办法承受三个参数:
- 自定义元素的名称,一个 DOMString 规范的字符串,为了避免自定义元素的抵触,必须是一个带短横线连贯的名称(
e.g. custom-tag
)。 - 定义自定义元素的一些行为,相似于 React、Vue 中的生命周期。
- 扩大参数(可选),该参数类型为一个对象,且须要蕴含
extends
属性,用于指定创立的元素继承自哪一个内置元素(e.g. {extends: 'p'}
)。
上面通过一些例子,演示其用法,残缺代码放到了 JS Bin 上。
创立一个新的 HTML 标签
先看看如何创立一个全新的自定义元素。
class HelloUser extends HTMLElement {constructor() { | |
// 必须调用 super 办法 | |
super(); | |
// 创立一个 div 标签 | |
const $box = document.createElement("p"); | |
let userName = "User Name"; | |
if (this.hasAttribute("name")) { | |
// 如果存在 name 属性,读取 name 属性的值 | |
userName = this.getAttribute("name"); | |
} | |
// 设置 div 标签的文本内容 | |
$box.innerText = `Hello ${userName}`; | |
// 创立一个 shadow 节点,创立的其余元素应附着在该节点上 | |
const shadow = this.attachShadow({mode: "open"}); | |
shadow.appendChild($box); | |
} | |
} | |
// 定义一个名为 <hello-user /> 的元素 | |
customElements.define("hello-user", HelloUser); |
<hello-user name="Shenfq"></hello-user>
这时候页面上就会生成一个 <p>
标签,其文本内容为:Hello Shenfq
。这种模式的自定义元素被称为:Autonomous custom elements
,是一个独立的元素,能够在 HTML 中间接应用。
扩大已有的 HTML 标签
咱们除了能够定义一个全新的 HTML 标签,还能够对已有的 HTML 标签进行扩大,例如,咱们须要封装一个与 <ul>
标签能力相似的组件,就能够应用如下形式:
class SkillList extends HTMLUListElement {constructor() { | |
// 必须调用 super 办法 | |
super(); | |
if (this.hasAttribute("skills") && | |
this.getAttribute("skills").includes(',') | |
) { | |
// 读取 skills 属性的值 | |
const skills = this.getAttribute("skills").split(','); | |
skills.forEach(skill => {const item = document.createElement("li"); | |
item.innerText = skill; | |
this.appendChild(item); | |
}) | |
} | |
} | |
} | |
// 对 <ul> 标签进行扩大 | |
customElements.define("skill-list", SkillList, { extends: "ul"}); |
<ul is="skill-list" skills="js,css,html"></ul>
对已有的标签进行扩大,须要用到 customElements.define
办法的第三个参数,且第二参数的类,也须要继承须要扩大标签的对应的类。应用的时候,只须要在标签加上 is
属性,属性值为第一个参数定义的名称。
生命周期
自定义元素的生命周期比较简单,一共只提供了四个回调办法:
connectedCallback
:当自定义元素被插入到页面的 DOM 文档时调用。disconnectedCallback
:当自定义元素从 DOM 文档中被删除时调用。adoptedCallback
:当自定义元素被挪动时调用。attributeChangedCallback
: 当自定义元素减少、删除、批改本身属性时调用。
上面演示一下应用办法:
class HelloUser extends HTMLElement {constructor() { | |
// 必须调用 super 办法 | |
super(); | |
// 创立一个 div 标签 | |
const $box = document.createElement("p"); | |
let userName = "User Name"; | |
if (this.hasAttribute("name")) { | |
// 如果存在 name 属性,读取 name 属性的值 | |
userName = this.getAttribute("name"); | |
} | |
// 设置 div 标签的文本内容 | |
$box.innerText = `Hello ${userName}`; | |
// 创立一个 shadow 节点,创立的其余元素应附着在该节点上 | |
const shadow = this.attachShadow({mode: "open"}); | |
shadow.appendChild($box); | |
} | |
connectedCallback() {console.log('创立元素') | |
// 5s 后挪动元素到 iframe | |
setTimeout(() => {const iframe = document.getElementsByTagName("iframe")[0] | |
iframe.contentWindow.document.adoptNode(this) | |
}, 5e3) | |
} | |
disconnectedCallback() {console.log('删除元素') | |
} | |
adoptedCallback() {console.log('挪动元素') | |
} | |
} |
<!-- 页面插入一个 iframe,将自定义元素移入其中 --> | |
<iframe width="0" height="0"></iframe> | |
<hello-user name="Shenfq"></hello-user> |
在元素被创立后,期待 5s,而后将自定义元素挪动到 iframe 文档中,这时候能看到控制台会同时呈现 删除元素
、 挪动元素
的 log。
Shadow DOM(影子 DOM)
在后面介绍自定义元素的时候,曾经用到了 Shadow DOM。Shadow DOM 的作用是让外部的元素与内部隔离,让自定义元素的构造、款式、行为不受到内部的影响。
咱们能够看到后面定义的 <hello-user>
标签,在控制台的 Elements 内,会显示一个 shadow-root
,表明外部是一个 Shadow DOM。
其实 Web Components 没有提出之前,浏览器外部就有应用 Shadow DOM 进行一些外部元素的封装,例如 <video>
标签。咱们须要当初控制台的配置中,关上 Show user agent ashdow DOM
开关。
而后在控制台的 Elements 内,就能看到 <video>
标签内其实也有一个 shadow-root
。
创立 Shadow DOM
咱们能够在任意一个节点外部创立一个 Shadow DOM,在获取元素实例后,调用 Element.attachShadow()
办法,就能将一个新的 shadow-root
附加到该元素上。
该办法承受一个对象,且只有一个 mode
属性,值为 open
或 closed
,示意 Shadow DOM 内的节点是否能被内部获取。
<div id="root"></div> | |
<script> | |
// 获取页面的 | |
const $root = document.getElementById('root'); | |
const $p = document.createElement('p'); | |
$p.innerText = '创立一个 shadow 节点'; | |
const shadow = $root.attachShadow({mode: 'open'}); | |
shadow.appendChild($p); | |
</script> |
mode 的差别
后面提到了 mode 值为 open
或 closed
,次要差别就是是否能够应用 Element.shadowRoot
获取到 shadow-root
进行一些操作。
<div id="root"></div> | |
<script> | |
// 获取页面的 | |
const $root = document.getElementById('root'); | |
const $p = document.createElement('p'); | |
$p.innerText = '创立一个 shadow 节点'; | |
const shadow = $root.attachShadow({mode: 'open'}); | |
shadow.appendChild($p); | |
console.log('is open', $div.shadowRoot); | |
</script> |
<div id="root"></div> | |
<script> | |
// 获取页面的 | |
const $root = document.getElementById('root'); | |
const $p = document.createElement('p'); | |
$p.innerText = '创立一个 shadow 节点'; | |
const shadow = $root.attachShadow({mode: 'closed'}); | |
shadow.appendChild($p); | |
console.log('is closed', $div.shadowRoot); | |
</script> |
HTML templates(HTML 模板)
后面的案例中,有个很显著的缺点,那就是操作 DOM 还是得应用 DOM API,相比起 Vue 得模板和 React 的 JSX 效率显著更低,为了解决这个问题,在 HTML 标准中引入了 <tempate>
和 <slot>
标签。
应用模板
模板简略来说就是一个一般的 HTML 标签,能够了解成一个 div
,只是这个元素内的所以内容不会展现到界面上。
<template id="helloUserTpl"> | |
<p class="name">Name</p> | |
<a target="blank" class="blog">##</a> | |
</template> |
在 JS 中,咱们能够间接通过 DOM API 获取到该模板的实例,获取到实例后,个别不能间接对模板内的元素进行批改,要调用 tpl.content.cloneNode
进行一次拷贝,因为页面上的模板并不是一次性的,可能其余的组件也要援用。
// 通过 ID 获取标签 | |
const tplElem = document.getElementById('helloUserTpl'); | |
const content = tplElem.content.cloneNode(true); |
咱们在获取到拷贝的模板后,就能对模板进行一些操作,而后再插入到 Shadow DOM 中。
<hello-user name="Shenfq" blog="http://blog.shenfq.com" /> | |
<script> | |
class HelloUser extends HTMLElement {constructor() { | |
// 必须调用 super 办法 | |
super(); | |
// 通过 ID 获取标签 | |
const tplElem = document.getElementById('helloUserTpl'); | |
const content = tplElem.content.cloneNode(true); | |
if (this.hasAttribute('name')) {const $name = content.querySelector('.name'); | |
$name.innerText = this.getAttribute('name'); | |
} | |
if (this.hasAttribute('blog')) {const $blog = content.querySelector('.blog'); | |
$blog.innerText = this.getAttribute('blog'); | |
$blog.setAttribute('href', this.getAttribute('blog')); | |
} | |
// 创立一个 shadow 节点,创立的其余元素应附着在该节点上 | |
const shadow = this.attachShadow({mode: "closed"}); | |
shadow.appendChild(content); | |
} | |
} | |
// 定义一个名为 <hello-user /> 的元素 | |
customElements.define("hello-user", HelloUser); | |
</script> |
增加款式
<template>
标签中能够直接插入 <style>
标签在,模板外部定义款式。
<template id="helloUserTpl"> | |
<style> | |
:host { | |
display: flex; | |
flex-direction: column; | |
width: 200px; | |
padding: 20px; | |
background-color: #D4D4D4; | |
border-radius: 3px; | |
} | |
.name { | |
font-size: 20px; | |
font-weight: 600; | |
line-height: 1; | |
margin: 0; | |
margin-bottom: 5px; | |
} | |
.email { | |
font-size: 12px; | |
line-height: 1; | |
margin: 0; | |
margin-bottom: 15px; | |
} | |
</style> | |
<p class="name">User Name</p> | |
<a target="blank" class="blog">##</a> | |
</template> |
其中 :host
伪类用来定义 shadow-root
的款式,也就是包裹这个模板的标签的款式。
占位元素
占位元素就是在模板中的某个地位先占据一个地位,而后在元素插入到界面上的时候,在指定这个地位应该显示什么。
<template id="helloUserTpl"> | |
<p class="name">User Name</p> | |
<a target="blank" class="blog">##</a> | |
<!-- 占位符 --> | |
<slot name="desc"></slot> | |
</template> | |
<hello-user name="Shenfq" blog="http://blog.shenfq.com"> | |
<p slot="desc"> 欢送关注公众号:更了不起的前端 </p> | |
</hello-user> |
这里用的用法与 Vue 的 slot 用法统一,不做过多的介绍。
总结
到这里 Web Components 的根本用法就介绍得差不多了,相比于其余的反对组件化计划的框架,应用 Web Components 有如下的长处:
- 浏览器原生反对,不须要引入额定的第三方库;
- 真正的外部私有化的 CSS,不会产生款式的抵触;
- 无需通过编译操作,即可实现的组件化计划,且与内部 DOM 隔离;
Web Components 的次要毛病就是规范可能还不太稳固,例如文章中没有提到的模板的模块化计划,就曾经被破除,当初还没有正式的计划引入模板文件。而且原生的 API 尽管能用,然而就是不好用,要不然也不会呈现 jQuery 这样的库来操作 DOM。好在当初也有很多基于 Web Components 实现的框架,前面还会开篇文章专门讲一讲应用 Web Components 的框架 lit-html
、lit-element
。
好啦,明天的文章就到这里了,心愿大家能有所播种。