宣告:未經允許,不得轉載。
Web Components 現世很久了,所以你可能聽說過,甚至學習過,非常瞭解了。但是沒關係,可以再重溫一下,溫故知新。
瀏覽器原生能力越來越強。
js
曾經的 JQuery
,是前端入門必學的技能,是前端專案必用的一個庫。它的強大之處在於簡化了 dom 操作
(強大的選擇器) 和 ajax
(非同步) 操作。
現在原生 api querySelector()
、querySelectorAll()
、classList
等的出現已經大大的弱化了 dom 操作, fetch
、基於 promise
的 axios
已經完全替代了 ajax
, 甚至更好用了,async-await
是真的好用。
css
css 前處理器(如 scss
、less
) 是專案工程化處理 css 的不二選擇。它的強大之處是支援變數、樣式規則巢狀、函式。
現在 css 已經支援變數(--var)
了, 樣式規則巢狀也在計劃之中,函式嘛 calc()
也非常強大,還支援 attr()
的使用,還有 css-module
模組化。
以前要製作酷炫複雜的 css 樣式及動畫,必須藉助 css 前處理器的變數、函式或者js才行,現在用 (css-doodle
)[https://css-doodle.com/] 技術,實現的更酷、更炫。
web components 元件化
Web Components
可以建立可複用的元件,未來的某一天拋棄現在所謂的框架和庫,直接使用原生 API 或者是使用基於 Web Components 標準的框架和庫進行開發,你覺得可能嗎?我覺得是可能的。
vue-lit
vue-lit,描述如下:
Proof of concept mini custom elements framework powered by @vue/reactivity and lit-html.
描述用到了 custom elements,而且瀏覽器控制檯 elements 的 DOM 結構中也含有 shadow-root。而 custom element 和 shadow DOM 是 web components 的重要組成。具體看下面 demo,
說明:本文文件示例,都是可以直接複雜到一個 html 文件的 body 中,然後直接在瀏覽中開啟預覽效果的。
<my-component />
<script type="module">
import {
defineComponent,
reactive,
html,
onMounted
} from 'https://unpkg.com/@vue/lit@0.0.2';
defineComponent('my-component', () => {
const state = reactive({
text: 'Hello World',
});
function onClick() {
alert('cliked!');
}
onMounted(() => {
console.log('mounted');
});
return () => html`
<p>
<button @click=${onClick}>Click me</button>
${state.text}
</p>
`;
})
</script>
// lit-html 模板,提供 html 模板(簡單js表示式及事件繫結)、render 渲染能力
import { render } from 'https://unpkg.com/lit-html?module'
// reactivity 是vue3.0的核心,shallowReactive 淺響應,effect 可以理解為 watch,提供屬性響應及部分生命週期處理
import {
shallowReactive,
effect
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
let currentInstance
export function defineComponent(name, propDefs, factory) {
if (typeof propDefs === 'function') {
factory = propDefs
propDefs = []
}
// 自定義元素 custom element,原生 API
customElements.define(
name,
class extends HTMLElement {
// 設定需要監聽的屬性
static get observedAttributes() {
return propDefs
}
constructor() {
super()
// 屬性接入 vue 的響應式
const props = (this._props = shallowReactive({}))
currentInstance = this
// lit-html 的 html 生成的模板
const template = factory.call(this, props)
currentInstance = null
// bm onBeforeMount
this._bm && this._bm.forEach((cb) => cb())
// shadowRoot,closed 表示不可以直接通過 js 獲取到定義的 customElement 操作 shadowRoot
const root = this.attachShadow({ mode: 'closed' })
let isMounted = false
effect(() => {
if (isMounted) {
// _bu, onBeforeUpdate
this._bu && this._bu.forEach((cb) => cb())
}
// 將 template 內容掛載到 shadowRoot 上
render(template(), root)
if (isMounted) {
// _u,onUpdated
this._u && this._u.forEach((cb) => cb())
} else {
isMounted = true
}
})
}
// 首次掛載到 dom 上後的回撥,onMounted
connectedCallback() {
this._m && this._m.forEach((cb) => cb())
}
// 解除安裝, onUnmounted
disconnectedCallback() {
this._um && this._um.forEach((cb) => cb())
}
// 屬性監聽
attributeChangedCallback(name, oldValue, newValue) {
this._props[name] = newValue
}
}
)
}
function createLifecycleMethod(name) {
return (cb) => {
if (currentInstance) {
;(currentInstance[name] || (currentInstance[name] = [])).push(cb)
}
}
}
export const onBeforeMount = createLifecycleMethod('_bm')
export const onMounted = createLifecycleMethod('_m')
export const onBeforeUpdate = createLifecycleMethod('_bu')
export const onUpdated = createLifecycleMethod('_u')
export const onUnmounted = createLifecycleMethod('_um')
export * from 'https://unpkg.com/lit-html?module'
export * from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
shallowReactive 原始碼,函式註釋已經表達的很清楚了,only the root level properties are reactive。物件只有根屬性響應,換言之即,淺響應,和淺拷貝類似。
/**
* Return a shallowly-reactive copy of the original object, where only the root
* level properties are reactive. It also does not auto-unwrap refs (even at the
* root level).
*/
export function shallowReactive<T extends object>(target: T): T {
return createReactiveObject(
target,
false,
shallowReactiveHandlers,
shallowCollectionHandlers
)
}
effect 原始碼,粗略的可以看到裡面有 dep 依賴,還有 oldValue、newValue 處理。
通過分析,vue-lit 應該是將 vue3.0 的響應式和 web components 做的一個嘗試。用 lit-html
的原因時因為支援模板支援簡單js表示式及事件繫結(原生template目前只有slot插槽)
css-doodle
實際上,前面介紹的 css-doodle 也是一個 web component。是瀏覽器原生就支援的。
示例:藝術背景圖。
<script src="https://unpkg.com/css-doodle@0.8.5/css-doodle.min.js"></script>
<css-doodle>
:doodle {
@grid: 1x300 / 100vw 40vmin;
overflow: hidden;
background: linear-gradient(rgba(63, 81, 181, .11), #673AB7);
}
align-self: flex-end;
--h: @r(10, 80, .1);
@random(.1) { --h: @r(85, 102, .1) }
@size: 1px calc(var(--h) * 1%);
background: linear-gradient(transparent, rgba(255, 255, 255, .4), transparent);
background-size: .5px 100%;
transform-origin: center 100%;
transform: translate(@r(-2vmin, 2vmin, .01), 10%) rotate(@r(-2deg, 2deg, .01));
:after {
content: '';
position: absolute;
top: 0;
@size: calc(2px * var(--h));
transform: translateY(-50%) scale(.14);
background: radial-gradient(@p(#ff03929e, #673ab752, #fffa) @r(40%), transparent 50%) 50% 50% / @r(100%) @lr() no-repeat;
}
</css-doodle>
dom 結構:
input、select 等內建 html 元素
input、select 也是 web component。但是是內建的,預設看不到 shadowRoot 結構,需要開啟瀏覽器控制檯的設定,勾選Show user agent shadow DOM
,才可以在控制檯elements
中看到其結構。
設定
dom 結構
web components 元件化由 3 部分組成。
- Custom elements(自定義元素):一組JavaScript API,允許您定義custom elements及其行為,然後可以在您的使用者介面中按照需要使用它們。
- Shadow DOM(影子DOM):一組JavaScript API,用於將封裝的“影子”DOM樹附加到元素(與主文件DOM分開呈現)並控制其關聯的功能。通過這種方式,您可以保持元素的功能私有,這樣它們就可以被指令碼化和樣式化,而不用擔心與文件的其他部分發生衝突。
- HTML templates(HTML模板):
<template>
和<slot>
元素使您可以編寫不在呈現頁面中顯示的標記模板。然後它們可以作為自定義元素結構的基礎被多次重用。
Custom elements
使用者可以使用 customElements.define
自定義 html 元素。
customElements.define(elementName, class[, extendElement]);
- elementName: 名稱不能是單個單詞,必須用短橫線分隔。
- class: 用以定義元素行為的類,包含生命週期。
- extendElement: 可選引數,一個包含
extends
屬性的配置物件,指定建立元素繼承哪個內建 HTML 元素
根據定義,得出有兩種 custom element:
- Autonomous custom elements: 獨立元素,不繼承內建的HTML元素。和 html 元素一樣使用,例如
<custom-info></custom-info>
- Customized built-in elements: 繼承內建的HTML元素。使用先寫出內建html元素便籤,通過 is 屬性指定 custom element 名稱,例如
<p is="custom-info"></p>
還有生命週期:
- connectedCallback:當 custom element首次被插入文件DOM時,被呼叫。
- disconnectedCallback:當 custom element從文件DOM中刪除時,被呼叫。
- adoptedCallback:當 custom element被移動到新的文件時,被呼叫。
- attributeChangedCallback: 當 custom element增加、刪除、修改自身屬性時,被呼叫。
示例:獨立元素。
<button onclick="changeInfo()">更改內容</button>
<custom-info text="hello world"></custom-info>
<script>
// Create a class for the element
class CustomInfo extends HTMLElement {
// 必須加這個屬性監聽,返回需要監聽的屬性,才能觸發 attributeChangedCallback 回撥
static get observedAttributes() {
return ['text'];
}
constructor() {
// Always call super first in constructor
super();
// Create a shadow root
const shadow = this.attachShadow({mode: 'open'});
// Create p
const info = document.createElement('p');
info.setAttribute('class', 'info');
// Create some CSS to apply to the shadow dom
const style = document.createElement('style');
console.log(style.isConnected);
style.textContent = `
.info {
color: red;
}
`;
// Attach the created elements to the shadow dom
shadow.appendChild(style);
console.log(style.isConnected);
shadow.appendChild(info);
}
connectedCallback () {
// 賦值
this.shadowRoot.querySelector('.info').textContent = this.getAttribute('text')
}
attributeChangedCallback(name, oldValue, newValue) {
// TODO
console.log(name, oldValue, newValue)
this.shadowRoot.querySelector('.info').textContent = newValue
}
}
// Define the new element
customElements.define('custom-info', CustomInfo);
function changeInfo() {
document.querySelector('custom-info').setAttribute('text', 'custom element')
}
</script>
示例:繼承元素
<p is="custom-info" text="hello world"></p>
<script>
// Create a class for the element,extend p element
class CustomInfo extends HTMLParagraphElement {
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
const info = document.createElement('span');
info.setAttribute('class', 'info');
const style = document.createElement('style');
console.log(style.isConnected);
style.textContent = `
.info {
color: red;
}
`;
shadow.appendChild(style);
console.log(style.isConnected);
shadow.appendChild(info);
}
connectedCallback () {
this.shadowRoot.querySelector('.info').textContent = this.getAttribute('text')
}
}
// Define the new element, extend p element
customElements.define('custom-info', CustomInfo, {extends: 'p'});
</script>
更多,請參考:Custom elements
Shadow DOM
Web components 的重要功能是封裝——可以將標記結構、樣式和行為隱藏起來,並與頁面上的其他程式碼相隔離,保證不同的部分不會混在一起,使程式碼更加乾淨、整潔。Shadow DOM 介面是關鍵所在,它可以將一個隱藏的、獨立的 DOM 附加到一個元素上。
附加到哪個元素上,和定義 custom element 時有關,如果是獨立元素,附加到 document body 上;如果是繼承元素,則附加到繼承元素上。
可以和操作普通 DOM 一樣,利用 API 操作 Shoadow DOM。
let shadow = elementRef.attachShadow({mode: 'open'});
let shadow = elementRef.attachShadow({mode: 'closed'});
open
表示可以通過頁面內的 JavaScript 方法來獲取 Shadow DOM,如'document.querySelector('custom-info').shadowRoot'。反之,獲取不到。
更多,請參考:Shadow DOM
HTML templates
template 和 slot 元素可以建立出非常靈活的 shadow DOM 模板,來填充 custom element。 對於重複使用的 html 結構,可以起到簡化作用,非常有意義。
示例
<!-- 顯示 default text -->
<custom-info></custom-info>
<!-- 顯示 template info -->
<custom-info>
<span slot="info">template info</span>
</custom-info>
<template id="custom-info">
<style>
p {
color: red;
}
</style>
<p><slot name="info">default text</slot></p>
</template>
<script>
class CustomInfo extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
const customInfoTpCon = document.querySelector('#custom-info').content;
shadowRoot.appendChild(customInfoTpCon.cloneNode(true));
}
}
customElements.define('custom-info', CustomInfo);
</script>
更多,請參考:HTML templates and slots
web components 示例
看圖,結果不言而喻。
總結
瀏覽器原生能力正在變得很強大。web component 值得擁抱一下。雖然 template 還不是很完善(不支援表示式),但這也只是白板上的一個黑點。
參考: