Web Component -- 即將爆發(fā)的原生的 UI 組件化標(biāo)準(zhǔn)
Web Component 概述
Web Component 是一種用于構(gòu)建可復(fù)用用戶(hù)界面組件的技術(shù),開(kāi)發(fā)者可以創(chuàng)建自定義的 HTML 標(biāo)簽,并將其封裝為包含邏輯和樣式的獨(dú)立組件,從而在任何 Web 應(yīng)用中重復(fù)使用。
每個(gè) Web Component 都具有自己的 DOM 和樣式隔離,避免了全局 CSS 和 JavaScript 的沖突問(wèn)題。它還支持自定義事件和屬性,可以與其他組件進(jìn)行通信和交互。
不同于 Vue/React 等社區(qū)或廠商的組件化開(kāi)發(fā)方案,Web Component 被定義在標(biāo)準(zhǔn)的 HTML 和 DOM 標(biāo)準(zhǔn)中。它由一組相關(guān)的 Web 平臺(tái) API 組成,也可以與現(xiàn)有的前端框架和庫(kù)配合使用。
Web Component 的兼容性良好,可以在現(xiàn)代瀏覽器中直接使用,也可以通過(guò) polyfill 兼容到舊版瀏覽器(IE11 理論上可以兼容,出于初步調(diào)研的考慮,本文不對(duì)兼容性作過(guò)多探討)。
同類(lèi)組件化方案比較
| Pros | 技術(shù) | Cons |
|---|---|---|
| 可以異構(gòu) | Micro Frontend | 需要主應(yīng)用、對(duì)子應(yīng)用有侵入、樣式統(tǒng)一困難 |
| 模塊級(jí)的多項(xiàng)目在運(yùn)行時(shí)共享 | Module Federation | 主要依賴(lài)webpack5,既有項(xiàng)目改造成本未知;實(shí)現(xiàn)異構(gòu)引用需要借助其他插件 |
| 模塊級(jí)動(dòng)態(tài)共享 | Vue :is + 動(dòng)態(tài)import |
依賴(lài)vue技術(shù)棧 |
| 可以異構(gòu)、完全解耦、對(duì)原有開(kāi)發(fā)方法改造極小 | Web Compnent | IE兼容性?xún)H11可通過(guò)Polyfill支持 |
TL;DR
實(shí)例:用異構(gòu)系統(tǒng)共建 web components
https://gitee.com/tonylua/web-component-test1/tree/master
Web Component 關(guān)鍵特性
Custom Elements(自定義元素)
是 Web 標(biāo)準(zhǔn)中的一項(xiàng)功能,它允許開(kāi)發(fā)者自定義新的 HTML 元素,開(kāi)發(fā)者可以使用 JavaScript 和 DOM API,使新元素具有自定義的行為和功能
4.13.1.1 Creating an autonomous custom element
This section is non-normative.
For the purposes of illustrating how to create an autonomous custom element, let's define a custom element that encapsulates rendering a small icon for a country flag. Our goal is to be able to use it like so:
<flag-icon country="nl"></flag-icon>To do this, we first declare a class for the custom element, extending
HTMLElement:class FlagIcon extends HTMLElement {
constructor() {
super();
this._countryCode = null;
}
static observedAttributes = ["country"];
attributeChangedCallback(name, oldValue, newValue) {
// name will always be "country" due to observedAttributes
this._countryCode = newValue;
this._updateRendering();
}
connectedCallback() {
this._updateRendering();
}
get country() {
return this._countryCode;
}
set country(v) {
this.setAttribute("country", v);
}
_updateRendering() {
...
}
}We then need to use this class to define the element:
customElements.define("flag-icon", FlagIcon);
- 繼承自基類(lèi) HTMLElement
- 自定義的元素名稱(chēng)需符合 DOMString 標(biāo)準(zhǔn),簡(jiǎn)單來(lái)說(shuō)就是必須帶短橫線
- 其中 observedAttributes 聲明的屬性才能被 attributeChangedCallback() 監(jiān)聽(tīng)
- 完整生命周期方法說(shuō)明為:
class MyCustomElement extends HTMLElement {
constructor() {
super();
// 在構(gòu)造函數(shù)中進(jìn)行初始化操作
// 用 this.appendChild(...) 等掛載到dom中
// 用 addEventListener() 綁定事件到 this.xxx 上
}
connectedCallback() {
// 元素被插入到文檔時(shí)觸發(fā),等價(jià)于 vue 的 mounted
}
disconnectedCallback() {
// 元素從文檔中移除時(shí)觸發(fā),等價(jià)于 vue 的 beforeDestory / destoyed
}
attributeChangedCallback(attributeName, oldValue, newValue) {
// 元素的屬性被添加、移除或更改時(shí)觸發(fā),等價(jià)于 vue 的 beforeUpdate / updated
}
}
除了繼承 HTMLElement,也可以繼承其既有子類(lèi),并在使用是采用原生標(biāo)簽(被繼承類(lèi)) + is 語(yǔ)法,如:
// Create a class for the element
class WordCount extends HTMLParagraphElement {
constructor() {
// Always call super first in constructor
super();
// Constructor contents omitted for brevity
// …
}
}
// Define the new element
customElements.define("word-count", WordCount, { extends: "p" });
<p is="word-count"></p>
Shadow DOM
DOM 編程模型令人詬病的一個(gè)方面就是缺乏封裝,不同組件之間的邏輯和樣式很容易互相污染。
鑒于這個(gè)原因,Web components 的一個(gè)重要屬性就是封裝——可以將標(biāo)記結(jié)構(gòu)、樣式和行為隱藏起來(lái),并與頁(yè)面上的其他代碼相隔離。其中,Shadow DOM 接口是關(guān)鍵所在,它可以將一個(gè)隱藏的、獨(dú)立的 DOM 附加到一個(gè)元素上
Shadow DOM 是 DOM nodes 的附屬樹(shù)。這種 Shadow DOM 子樹(shù)可以與某宿主元素相關(guān)聯(lián),但并不作為該元素的普通子節(jié)點(diǎn),而是會(huì)形成其自有的作用域;Shadow DOM 中的根及其子節(jié)點(diǎn)也不可見(jiàn)。
相比于以前為了實(shí)現(xiàn)封裝而只能使用 <iframe> 實(shí)現(xiàn)的情況,Shadow DOM 無(wú)疑是一種更優(yōu)雅的創(chuàng)建隔離 DOM 樹(shù)的方法。
Shadow DOM 允許將隱藏的 DOM 樹(shù)附加到常規(guī)的 DOM 樹(shù)中——它以 shadow root 節(jié)點(diǎn)為起始根節(jié)點(diǎn),在這個(gè)根節(jié)點(diǎn)的下方,可以是任意元素,和普通的 DOM 元素一樣。
![]()
這里,有一些 Shadow DOM 特有的術(shù)語(yǔ)需要我們了解:
- Shadow host:一個(gè)常規(guī) DOM 節(jié)點(diǎn),Shadow DOM 會(huì)被附加到這個(gè)節(jié)點(diǎn)上。
- Shadow tree:Shadow DOM 內(nèi)部的 DOM 樹(shù)。
- Shadow boundary:Shadow DOM 結(jié)束的地方,也是常規(guī) DOM 開(kāi)始的地方。
- Shadow root: Shadow tree 的根節(jié)點(diǎn)。
你可以使用同樣的方式來(lái)操作 Shadow DOM,就和操作常規(guī) DOM 一樣——例如添加子節(jié)點(diǎn)、設(shè)置屬性,以及為節(jié)點(diǎn)添加自己的樣式(例如通過(guò)
element.style屬性),或者為整個(gè) Shadow DOM 添加樣式(例如在<style>元素內(nèi)添加樣式)。不同的是,Shadow DOM 內(nèi)部的元素始終不會(huì)影響到它外部的元素(除了:focus-within),這為封裝提供了便利。注意,不管從哪個(gè)方面來(lái)看,Shadow DOM 都不是一個(gè)新事物——在過(guò)去的很長(zhǎng)一段時(shí)間里,瀏覽器用它來(lái)封裝一些元素的內(nèi)部結(jié)構(gòu)。以一個(gè)有著默認(rèn)播放控制按鈕的
<video>元素為例。你所能看到的只是一個(gè)<video>標(biāo)簽,實(shí)際上,在它的 Shadow DOM 中,包含了一系列的按鈕和其他控制器。Shadow DOM 標(biāo)準(zhǔn)允許你為你自己的元素(custom element)維護(hù)一組 Shadow DOM。基本用法
可以使用
Element.attachShadow()方法來(lái)將一個(gè) shadow root 附加到任何一個(gè)元素上。它接受一個(gè)配置對(duì)象作為參數(shù),該對(duì)象有一個(gè)mode屬性,值可以是open或者closed:let shadow = elementRef.attachShadow({ mode: "open" });
let shadow = elementRef.attachShadow({ mode: "closed" });
open表示可以通過(guò)頁(yè)面內(nèi)的 JavaScript 方法來(lái)獲取 Shadow DOM,例如使用Element.shadowRoot屬性:let myShadowDom = myCustomElem.shadowRoot;如果你將一個(gè) Shadow root 附加到一個(gè) Custom element 上,并且將
mode設(shè)置為closed,那么就不可以從外部獲取 Shadow DOM 了——myCustomElem.shadowRoot將會(huì)返回null。瀏覽器中的某些內(nèi)置元素就是如此,例如<video>,包含了不可訪問(wèn)的 Shadow DOM。如果你想將一個(gè) Shadow DOM 附加到 custom element 上,可以在 custom element 的構(gòu)造函數(shù)中添加如下實(shí)現(xiàn)(目前,這是 shadow DOM 最實(shí)用的用法):
let shadow = this.attachShadow({ mode: "open" });將 Shadow DOM 附加到一個(gè)元素之后,就可以使用 DOM APIs 對(duì)它進(jìn)行操作,就和處理常規(guī) DOM 一樣。
var para = document.createElement('p');
shadow.appendChild(para);
etc.
注意:
- 要使用 Chrome 調(diào)試器檢查 Shadow DOM,需要選中調(diào)試器的
Preferences/Elmenets下的show user agent shadow DOM框*;比如對(duì)于上文提到的<video>,在打開(kāi)該調(diào)試選項(xiàng)后,就能在元素面板中看到<video>下掛載的 shadow tree - 一些比較舊的資料中會(huì)出現(xiàn) attachShadow() 的前身 createShadowRoot(),語(yǔ)義基本相同;createShadowRoot()已經(jīng)被廢棄,它是在 Shadow DOM v0 規(guī)范中引入的。Shadow DOM 的最新版本是 v1,是 Web 標(biāo)準(zhǔn)的一部分。
HTML templates 和 slot
<template> 元素允許開(kāi)發(fā)者在 HTML 中定義一個(gè)模板,其中可以包含任意的 HTML 結(jié)構(gòu)、文本和變量占位符。此元素及其內(nèi)容不會(huì)在 DOM 中呈現(xiàn),但仍可使用 JavaScript 去引用它。
<template id="my-paragraph">
<p>My paragraph</p>
</template>上面的代碼不會(huì)展示在你的頁(yè)面中,直到你用 JavaScript 獲取它的引用,然后添加到 DOM 中,如下面的代碼:
let template = document.getElementById("my-paragraph");
let templateContent = template.content;
document.body.appendChild(templateContent);模板(Template)本身就是有用的,而與 web 組件(web component)一起使用效果更好。我們定義一個(gè) web 組件使用模板作為陰影(shadow)DOM 的內(nèi)容,叫它
<my-paragraph>:customElements.define(
"my-paragraph",
class extends HTMLElement {
constructor() {
super();
let template = document.getElementById("my-paragraph");
let templateContent = template.content;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.appendChild(templateContent.cloneNode(true));
}
},
);
使用 <slot> 則能進(jìn)一步展示不同的自定義內(nèi)容:
<template id="my-paragraph">
<p><slot name="my-text">My default text</slot></p>
</template>...
<my-paragraph>
<ul slot="my-text">
<li>Let's have some different text!</li>
<li>In a list!</li>
</ul>
</my-paragraph>
CSS Scoping(局部作用域的 CSS)
The CSS scoping module defines the CSS scoping and encapsulation mechanisms, focusing on the Shadow DOM scoping mechanism.
根據(jù) Shadow DOM 作用域機(jī)制,CSS scoping 模塊定義了 CSS 作用域和封裝機(jī)制
CSS styles are either global in scope or scoped to a shadow tree. Globally scoped styles apply to all the elements in the node tree that match the selector, including custom elements in that tree, but not to the shadow trees composing each custom element. Selectors and their associated style definitions don't bleed between scopes.
CSS 樣式分為全局和 shadow tree 局部?jī)煞N。全局樣式應(yīng)用于節(jié)點(diǎn)樹(shù)中與選擇器匹配的所有元素,包括該樹(shù)中的自定義元素,但不應(yīng)用于組成每個(gè)自定義元素的shadow tree。選擇器及其關(guān)聯(lián)的樣式定義也不會(huì)在作用域之間流通。
Within the CSS of a shadow tree, selectors don't select elements outside the tree, either in the global scope or in other shadow trees. Each custom element has its own shadow tree, which contains all the components that make up the custom element (but not the custom element, or "host", itself).
在 shadow tree 的 CSS 中,選擇器不會(huì)影響樹(shù)外部的元素 -- 無(wú)論是全局作用域還是其他 shadow tree。每個(gè)自定義元素都有自己的 shadow tree,它包含組成自定義元素的所有組件(但不包含自定義元素或“宿主”本身)。
:host 偽類(lèi)
在 shadow DOM 內(nèi)部,要想為“宿主” shadow host 本身添加樣式,可以用 CSS 選擇器 :host:
:host {
/* ... */
}
:host 選擇器還有一種函數(shù)式的用法,接收一個(gè)選擇器參數(shù),該參數(shù)表示 shadow host 本身具備特定的狀態(tài)或樣式時(shí)才生效,如:
:host(:hover) {
background-color: #ccc;
}
:host(.active) {
color: red;
}
:host(.footer) { // 宿主元素包含footer樣式名時(shí)
color : red;
}
:host-context 偽類(lèi)
與 :host(selector) 用法類(lèi)似的還有 :host-context() 偽類(lèi),但所謂 context 的語(yǔ)意指的是,作為其參數(shù)的選擇器指向的是 shadow host 宿主元素的上下文環(huán)境,也就是其作為哪個(gè)祖先元素的后代時(shí)才生效,如:
// 當(dāng)宿主是 h1 后代時(shí)
:host-context(h1) {
font-weight: bold;
}
// 當(dāng) .dark-theme 類(lèi)應(yīng)用于主文檔 body 時(shí)
:host-context(body.dark-theme) p {
color: #fff;
}
::part 偽元素
用于在父頁(yè)面指定 shadow DOM 內(nèi)部使用了對(duì)應(yīng) part 屬性元素的樣式:
<html>
<head>
<template id="template">
My host element!
<span part="sp">xxx</span>
</template>
<style>
#host::part(sp) {
background-color: aqua;
}
</style>
</head>
<body>
<div id="host"></div>
<script type="text/javascript">
var template = document.querySelector('#template')
var root = document.querySelector('#host').attachShadow({ mode: "open" });
root.appendChild(template.content);
</script>
</body>
</html>
::part() 在遵循 Shadow DOM 封裝性的同時(shí),提供了一個(gè)安全指定內(nèi)部樣式的途徑。
但這不是唯一的手段,另一種“穿透”方法是通過(guò) CSS 自定義變量:
<html>
<head>
<template id="template">
<style>
span {
background-color: var(--sp-color, red);
}
</style>
My host element will have a blue border!
<span part="sp">xxx</span>
</template>
<style>
#host {
--sp-color: blue; // 生效
}
</style>
</head>
<body>
<div id="host"></div>
<script type="text/javascript">
var template = document.querySelector('#template')
var root = document.querySelector('#host').attachShadow({ mode: "open" });
root.appendChild(template.content);
</script>
</body>
</html>
::slotted 偽元素
在自定義組件內(nèi)部指定該樣式后,僅有 被外部成功填充的slot 才會(huì)被匹配到,使用默認(rèn)值的 slot 上則不會(huì)生效。
優(yōu)先級(jí)
- 對(duì)于“宿主”元素,外部樣式優(yōu)先級(jí)高于內(nèi)部的
:host - 如果要覆蓋父頁(yè)中設(shè)置的樣式,則必須在宿主元素上內(nèi)聯(lián)完成
- 外部
::part樣式優(yōu)先級(jí)高于內(nèi)部定義
觀察以下例子,優(yōu)先級(jí) blur > green > red:
<head>
<template id="template">
<style>
:host {
border: 1px solid red;
padding: 10px;
line-height: 50px;
}
</style>
My host element will have a blue border!
</template>
<style>
#host {
border-color: green;
}
</style>
</head>
<body>
<div id="host" style="border-color: blue;"></div>
<script type="text/javascript">
var template = document.querySelector('#template')
var root = document.querySelector('#host').attachShadow({ mode: "open" });
root.appendChild(template.content);
</script>
</body>
Event retargeting(事件的重定向)
當(dāng) shadow DOM 中發(fā)生的事件在外部被捕獲時(shí),將會(huì)以其 host 元素作為目標(biāo)。
<user-card></user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<p>
<button>Click me</button>
</p>`;
this.shadowRoot.firstElementChild.onclick =
e => alert("Inner target: " + e.target.tagName);
}
});
document.onclick =
e => alert("Outer target: " + e.target.tagName);
</script>
打印出:
Inner target: BUTTON
Outer target: USER-CARD
外部文檔并不需要知道自定義組件的內(nèi)部情況 -- 從它的角度來(lái)看,事件總是發(fā)生在自定義組件上,除非事件發(fā)生在 slot 的元素上。
<user-card id="userCard">
<span slot="username">John Smith</span>
</user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<div>
<b>Name:</b> <slot name="username"></slot>
</div>`;
this.shadowRoot.firstElementChild.onclick =
e => alert("Inner target: " + e.target.tagName);
}
});
userCard.onclick = e => alert(`Outer target: ${e.target.tagName}`);
</script>
打印出:
Inner target: BUTTON
Outer target: SPAN
從 Shadow DOM 內(nèi)部觸發(fā)事件
如果要發(fā)送自定義事件,可以使用 CustomEvent,注意要設(shè)置冒泡和 composed
this._shadowRoot.dispatchEvent(
new CustomEvent("weather-fetched", {
bubbles: true,
composed: true,
detail: json,
})
);
HTML imports
Web Component 標(biāo)準(zhǔn)中被廢棄的一個(gè)草案(有開(kāi)源替代方案),用于引入自定義組件的結(jié)構(gòu)和完整定義,從而可以直接在主頁(yè)面 html 中引用:
<link rel="import" href="module-my-comp.html">
<my-comp />
Web Component 開(kāi)發(fā)框架
除了原生開(kāi)發(fā)方法,社區(qū)中大量既有/特有開(kāi)發(fā)語(yǔ)言,都可以轉(zhuǎn)譯為 Web Component
Polymer
Google 推出的 Web Components 庫(kù),支持?jǐn)?shù)據(jù)的單向和雙向綁定,兼容性較好,跨瀏覽器性能也較好;在語(yǔ)法層面,Polymer 也最接近 Web Components 的原生語(yǔ)法。
import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';
import '@polymer/iron-icon/iron-icon.js'; // 一個(gè)圖標(biāo)庫(kù)
class IconToggle extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: inline-block;
}
iron-icon {
fill: var(--icon-toggle-color, rgba(0,0,0,0));
stroke: var(--icon-toggle-outline-color, currentcolor);
}
:host([pressed]) iron-icon {
fill: var(--icon-toggle-pressed-color, currentcolor);
}
</style>
<!-- shadow DOM goes here -->
<iron-icon icon="[[toggleIcon]]"></iron-icon>
`;
}
static get properties () {
return {
toggleIcon: {
type: String
},
pressed: {
type: Boolean,
notify: true,
reflectToAttribute: true,
value: false
}
};
}
constructor() {
super();
this.addEventListener('click', this.toggle.bind(this));
}
toggle() {
this.pressed = !this.pressed;
}
}
customElements.define('icon-toggle', IconToggle);
Lit
Google 在 2019 年宣布停止對(duì) Polymer 的進(jìn)一步開(kāi)發(fā),轉(zhuǎn)向支持 Web Components 規(guī)范更好的 Lit;這也是目前社區(qū)中被推薦較多的一個(gè)
The Polymer library is in maintenance mode. For new development, we recommend Lit. -- Google
import {html, css, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
static styles = css`p { color: blue }`;
@property()
name = 'Somebody';
render() {
return html`<p>Hello, ${this.name}!</p>`;
}
}
<simple-greeting name="World"></simple-greeting>
React
react 在 v17 版本之后,增加了對(duì)于在 React 組件中使用 web component 的支持:
If you render a tag with a dash, like, React will assume you want to render a custom HTML element. In React, rendering custom elements works differently from rendering built-in browser tags:
All custom element props are serialized to strings and are always set using attributes. Custom elements accept class rather than className, and for rather than htmlFor. If you render a built-in browser HTML element with an is attribute, it will also be treated as a custom element.
import React, { useState } from 'react';
import './alert.js';
export default function App() {
const [show, setShow] = useState(true);
return (
<div>
<button onClick={() => setShow(!show)}>toggle alert</button>
<x-alert hidden={show} status="success" closable oncloseChange={() => setShow(!show)}>
This is a Web Component in React
</x-alert>
</div>
);
}
而如果想將標(biāo)準(zhǔn) react 組件包裝為 web component,可以在 react 工程中直接結(jié)合 web component 原生語(yǔ)法、使用 React 完成節(jié)點(diǎn)渲染,并導(dǎo)出成獨(dú)立組件。
比如 Github上這個(gè)例子:
import * as React from "react";
import * as ReactDom from "react-dom";
import { FetchData } from "./fetch-data";
class StandaloneComponent extends HTMLElement {
mountPoint!: HTMLSpanElement;
name!: string;
connectedCallback() {
const mountPoint = document.createElement("span");
this.attachShadow({ mode: "open" }).appendChild(mountPoint);
const name = this.getAttribute("name");
if (name) {
ReactDom.render(<FetchData name={name} />, mountPoint);
} else {
console.error("You must declare a name!");
}
}
}
export default StandaloneComponent;
window.customElements.get("standalone-component") ||
window.customElements.define("standalone-component", StandaloneComponent);
另一種更方便的方式是依靠 react 社區(qū)中的工具,常見(jiàn)的如:
- react-web-component
- direflow
- react-shadow-root
- react-to-web-component
import r2wc from '@r2wc/react-to-web-component';
import Checklist from './components/checklist/Checklist';
const wcChecklist = r2wc(Checklist, { props: { items: "json" } });
customElements.define("r2w-checklist", wcChecklist);
Vue3
Polymer 是另一個(gè)由谷歌贊助的項(xiàng)目,事實(shí)上也是 Vue 的一個(gè)靈感來(lái)源。Vue 的組件可以粗略的類(lèi)比于 Polymer 的自定義元素,并且兩者具有相似的開(kāi)發(fā)風(fēng)格。最大的不同之處在于,Polymer 是基于最新版的 Web Components 標(biāo)準(zhǔn)之上,并且需要重量級(jí)的 polyfills 來(lái)幫助工作 (性能下降),瀏覽器本身并不支持這些功能。相比而言,Vue 在支持到 IE9 的情況下并不需要依賴(lài) polyfills 來(lái)工作。
...
Vue implements a content distribution API inspired by the Web Components spec draft, using the
<slot>element to serve as distribution outlets for content.-- vue2官方文檔
源自 Vue 2.x 時(shí)代對(duì) Web Components 的關(guān)注,Vue 3 更進(jìn)一步,原生支持了將 Vue 3 組件導(dǎo)出為 Web Components:
Vue 提供了一個(gè)和定義一般 Vue 組件幾乎完全一致的
defineCustomElement方法來(lái)支持創(chuàng)建自定義元素。這個(gè)方法接收的參數(shù)和defineComponent完全相同。但它會(huì)返回一個(gè)繼承自HTMLElement的自定義元素構(gòu)造器:<my-vue-element></my-vue-element>import { defineCustomElement } from 'vue'
const MyVueElement = defineCustomElement({
// 這里是同平常一樣的 Vue 組件選項(xiàng)
props: {},
emits: {},
template: `...`,
// defineCustomElement 特有的:注入進(jìn) shadow root 的 CSS
styles: [`/* inlined css */`]
})
// 注冊(cè)自定義元素
// 注冊(cè)之后,所有此頁(yè)面中的 `<my-vue-element>` 標(biāo)簽
// 都會(huì)被升級(jí)
customElements.define('my-vue-element', MyVueElement)
// 你也可以編程式地實(shí)例化元素:
// (必須在注冊(cè)之后)
document.body.appendChild(
new MyVueElement({
// 初始化 props(可選)
})
)...
官方的 SFC 工具鏈支持以“自定義元素模式”導(dǎo)入 SFC (需要
@vitejs/plugin-vue@^1.4.0或vue-loader@^16.5.0)。一個(gè)以自定義元素模式加載的 SFC 將會(huì)內(nèi)聯(lián)其<style>標(biāo)簽為 CSS 字符串,并將其暴露為組件的styles選項(xiàng)。這會(huì)被defineCustomElement提取使用,并在初始化時(shí)注入到元素的 shadow root 上。要開(kāi)啟這個(gè)模式,只需要將你的組件文件以
.ce.vue結(jié)尾即可:import { defineCustomElement } from 'vue'
import Example from './Example.ce.vue'
console.log(Example.styles) // ["/* 內(nèi)聯(lián) css */"]
// 轉(zhuǎn)換為自定義元素構(gòu)造器
const ExampleElement = defineCustomElement(Example)
// 注冊(cè)
customElements.define('my-example', ExampleElement)
在 Vue 3 中使用其他 Web Component 同樣簡(jiǎn)單,根據(jù)編譯環(huán)境是瀏覽器、vite 或是 vue cli 等,設(shè)置其 isCustomElement 配置函數(shù)為 (tag) => tag.includes('-') 后基本就能正常使用了;詳見(jiàn)官方文檔。
Vue 2
Vue 2 中并不具備 Vue 3 中 defineCustomElement 那樣的方法。
webpack
對(duì)于大部分基于原生 webpack 的 Vue 2 項(xiàng)目,可以用開(kāi)源插件 vue-custom-element 達(dá)到和 defineCustomElement 類(lèi)似的效果,如:
Vue.customElement('widget-vue', MyVueComponent, {
shadow: true,
beforeCreateVueInstance(root) {
const rootNode = root.el.getRootNode();
if (rootNode instanceof ShadowRoot) {
root.shadowRoot = rootNode;
} else {
root.shadowRoot = document.head;
}
return root;
},
});
Vue CLI
而在由 Vue CLI 構(gòu)建的 Vue 項(xiàng)目中,可以通過(guò)為構(gòu)建命令指定 --target wc 參數(shù),從而將一個(gè)單獨(dú)的入口構(gòu)建為一個(gè) Web Components 組件:
vue-cli-service build --target wc --name my-element [entry]
-
entry應(yīng)該是一個(gè) *.vue 文件。Vue CLI 將會(huì)把這個(gè)組件自動(dòng)包裹并注冊(cè)為 Web Components 組件,無(wú)需在 main.js 里自行注冊(cè) - 在 Web Components 模式中,Vue 是外置的。這意味著包中不會(huì)有 Vue,即便你在代碼中導(dǎo)入了 Vue。這里的包會(huì)假設(shè)在頁(yè)面中已經(jīng)有一個(gè)可用的全局變量 Vue
- 該構(gòu)建將會(huì)產(chǎn)生一個(gè)單獨(dú)的 JavaScript 文件 (及其壓縮后的版本) 將所有的東西都內(nèi)聯(lián)起來(lái)
- 當(dāng)這個(gè)腳本被引入網(wǎng)頁(yè)時(shí),會(huì)注冊(cè)自定義組件,其使用 @vue/web-component-wrapper 包裹目標(biāo) Vue 組件,并自動(dòng)代理屬性、特性、事件和插槽
- 也可以設(shè)置構(gòu)建命令打包多個(gè)組件或異步組件
<script src="https://unpkg.com/vue"></script>
<script src="path/to/my-element.js"></script>
<!-- 可在普通 HTML 中或者其它任何框架中使用 -->
<my-element></my-element>
??實(shí)例:用異構(gòu)系統(tǒng)共建 web components
https://gitee.com/tonylua/web-component-test1/tree/master
總結(jié)
正如以 Flash 為代表的 RIA 技術(shù)浪潮極大地刺激了瀏覽器廠商,從而加速了瀏覽器的進(jìn)步并催生了 ES5/ES6 的落地;同樣,Angular/React/Vue 等前端組件化開(kāi)發(fā)框架的普及,也讓原生的 Web Components 標(biāo)準(zhǔn)不斷發(fā)展。
Web Components 搭配的 shadow DOM 封裝等實(shí)用特性,讓一直困擾開(kāi)發(fā)者們的樣式局部化和事件隔離等問(wèn)題迎刃而解。
隨著現(xiàn)代瀏覽器兼容性的不斷改善和各種開(kāi)發(fā)框架對(duì) Web Components 的主動(dòng)擁抱,也勢(shì)必會(huì)在不久的將來(lái)打破前端開(kāi)發(fā)技術(shù)棧之間的壁壘,讓整個(gè)社區(qū)沉淀的服務(wù)和能力迎來(lái)一次大的整合。
參考資料
- http://w3c-html-ig-zh.github.io/webcomponents/spec-zh/shadow/
- https://javascript.info/webcomponents-intro
- https://www.webcomponents.org/introduction
- https://juejin.cn/post/7072715334519619598
- https://juejin.cn/post/7148974524795453476
- https://juejin.cn/post/7107856163361783816
- https://www.zhihu.com/question/321832109
- https://juejin.cn/post/7181088227531915322
- https://www.jitendrazaa.com/blog/salesforce/introduction-to-html-web-components/
- https://juejin.cn/post/7168630364246638606
- https://juejin.cn/post/6976557762377416718
- https://cn.vuejs.org/guide/extras/web-components.html
- https://web.dev/custom-elements-best-practices/
- https://github.com/stcruy/building-a-reusable-vue-web-component
- https://www.oreilly.com/library/view/modern-javascript/9781491971420/ch05.html
- https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components
- https://deepinout.com/css/css-questions/417_css_what_is_the_different_between_host_host_hostcontext_selectors.html
- https://www.zhangxinxu.com/wordpress/2021/02/css-part-shadow-dom/
- https://juejin.cn/post/6923957212075261966
- https://web.dev/custom-elements-best-practices/
- https://www.abeautifulsite.net/tags/web%20components/
- https://juejin.cn/post/7010595352550047752
- https://dev.to/nurlan_tl/tips-to-create-web-components-using-vue-3-ts-vite-3a7a
- https://itnext.io/react-and-web-components-3e0fca98a593
- https://www.bitovi.com/blog/how-to-create-a-web-component-with-create-react-app
