尤大 4 天前發(fā)在 GitHub 上的 vue-lit 是啥?
寫在前面

尤大北京時(shí)間 9月18日 下午的時(shí)候發(fā)了一個(gè)微博,人狠話不多??吹竭@個(gè)表情,大家都知道有大事要發(fā)生。果然,在寫這篇文章的時(shí)候,上 GitHub 上看了一眼,剛好碰上發(fā)布:

我們知道,一般開源軟件的 release 就是一個(gè) 最終版本,看一下官方關(guān)于這個(gè) release 版本的介紹:
Today we are proud to announce the official release of Vue.js 3.0 "One Piece".
更多關(guān)于這個(gè) release 版本的信息可以關(guān)注:https://github.com/vuejs/vue-next/releases/tag/v3.0.0[1]
除此之外,我在尤大的 GitHub 上發(fā)現(xiàn)了另一個(gè)東西 vue-lit[2],直覺告訴我這又是一個(gè)啥面向未來的下一代 xxx,所以我就點(diǎn)進(jìn)去看了一眼是啥新玩具。
這篇文章就圍繞?vue-lit 展開說說。
Hello World
Proof of concept mini custom elements framework powered by @vue/reactivity and lit-html.
首先,vue-lit 看上去是尤大的一個(gè)驗(yàn)證性的嘗試,看到 custom element 和 lit-html,盲猜一把,是一個(gè)可以直接在瀏覽器中渲染 vue 寫法的 Web Component 的工具。
這里提到了
lit-html,后面會(huì)專門介紹一下。
按照尤大給的 Demo,我們來試一下 Hello World:
<html?lang="en">
??<head>
????<script?type="module">
??????import?{
????????defineComponent,
????????reactive,
????????html,
????????onMounted
??????}?from?'https://unpkg.com/@vue/[email protected]';
??
??????defineComponent('my-component',?()?=>?{
????????const?state?=?reactive({
??????????text:?'Hello?World',
????????});
????????
????????function?onClick()?{
??????????alert('cliked!');
????????}
??
????????onMounted(()?=>?{
??????????console.log('mounted');
????????});
??
????????return?()?=>?html`
??????????<p>
????????????<button?@click=${onClick}>Click?mebutton>
????????????${state.text}
??????????p>
????????`;
??????})
????script>
??head>
??<body>
????<my-component?/>
??body>
html>
不用任何編譯打包工具,直接打開這個(gè) index.html,看上去沒毛?。?/p>
可以看到,這里渲染出來的是一個(gè) Web Component,并且 mounted 生命周期也觸發(fā)了。
介紹?vue-lit 之前,我們需要先有一些前置知識(shí)。
關(guān)于 lit-html 和 lit-element
看 vue-lit 之前,我們先了解一下 lit-html 和 lit-ement,這兩個(gè)東西其實(shí)已經(jīng)出來很久了,可能并不是所有人都了解。
lit-html
lit-html[3] 可能很多人并不熟悉,甚至沒有見過。

所以是啥?答案是 HTML 模板引擎。
如果沒有體感,我問一個(gè)問題,React 核心的東西有哪些?大家都會(huì)回答:jsx、Virtual-DOM、diff,沒錯(cuò),就是這些東西構(gòu)成了 UI = f(data) 的 React。
來看看 jsx 的語法:
function?App()?{
??const?msg?=?'Hello?World';
??return?<div>${msg}div>;
}
再看看 lit-html 的語法:
function?App()?{
??const?msg?=?'Hello?World';
??return?html`
????<div>${msg}div>
??`;
}
我們知道 jsx 是需要編譯的它的底層最終還是 createElement....。而 lit-html 就不一樣了,它是基于 tagged template 的,使得它不用編譯就可以在瀏覽器上運(yùn)行,并且和 HTML Template 結(jié)合想怎么玩怎么玩,擴(kuò)展能力更強(qiáng),不香嗎?
當(dāng)然,無論是 jsx 還是 lint-html,這個(gè) App 都是需要 render 到真實(shí) DOM 上。
lint-html 實(shí)現(xiàn)一個(gè) Button 組件
直接上代碼(省略樣式代碼):
<html?lang="en">
<head>
??<script?type="module">
????import?{?html,?render?}?from?'https://unpkg.com/lit-html?module';
????const?Button?=?(text,?props?=?{
??????type:?'default',
??????borderRadius:?'2px'
????},?onClick)?=>?{
??????//?點(diǎn)擊事件
??????const?clickHandler?=?{
????????handleEvent(e)?{?
??????????alert('inner?clicked!');
??????????if?(onClick)?{
????????????onClick();
??????????}
????????},
????????capture:?true,
??????};
??????return?html`
????????<div?class="btn?btn-${props.type}"?@click=${clickHandler}>
??????????${text}
????????div>
??????`
????};
????render(Button('Defualt'),?document.getElementById('button1'));
????render(Button('Primary',?{?type:?'primary'?},?()?=>?alert('outer?clicked!')),?document.getElementById('button2'));
????render(Button('Error',?{?type:?'error'?}),?document.getElementById('button3'));
??script>
head>
<body>
??<div?id="button1">div>
??<div?id="button2">div>
??<div?id="button3">div>
body>
html>
效果:

性能
lit-html 會(huì)比 React 性能更好嗎?這里我沒仔細(xì)看過源碼,也沒進(jìn)行過相關(guān)實(shí)驗(yàn),無法下定論。
但是可以大膽猜測(cè)一下,lit-html 沒有使用類 diff 算法而是直接基于相同 template 的更新,看上去這種方式會(huì)更輕量一點(diǎn)。
但是,我們常問的一個(gè)問題 “在渲染列表的時(shí)候,key 有什么用?”,這個(gè)在 lit-html 是不是沒法解決了。我如果刪除了長(zhǎng)列表中的其中一項(xiàng),按照 lit-html 的基于相同 template 的更新,整個(gè)長(zhǎng)列表都會(huì)更新一次,這個(gè)性能就差很多了啊。
// TODO:埋個(gè)坑,以后看
lit-element
lit-element[4] 這又是啥呢?

關(guān)鍵詞:web components。
例子:
import?{?LitElement,?html?}?from?'lit-element';
class?MyElement?extends?LitElement?{
??static?get?properties()?{
????return?{
??????msg:?{?type:?String?},
????};
??}
??constructor()?{
????super();
????this.msg?=?'Hello?World';
??}
??render()?{
????return?html`
??????<p>${this.msg}p>
????`;
??}
}
customElements.define('my-element',?MyElement);
效果:

結(jié)論:可以用類 React 的語法寫 Web Component。
so, lit-element 是一個(gè)可以創(chuàng)建 Web Component 的 base class。分析一下上面的 Demo,lit-element 做了什么事情:
static get properties: 可以 setter的stateconstructor: 初始化 staterender: 通過 lit-html渲染元素,并且會(huì)創(chuàng)建ShadowDOM
總之,lit-element 遵守 Web Components 標(biāo)準(zhǔn),它是一個(gè) class,基于它可以快速創(chuàng)建 Web Component。
更多關(guān)于如何使用 lit-element 進(jìn)行開發(fā),在這里就不展開說了。
Web Components
瀏覽器原生能力香嗎?
說 Web Components 之前我想先問問大家,大家還記得 jQuery 嗎,它方便的選擇器讓人難忘。但是后來 document.querySelector 這個(gè) API 的出現(xiàn)并且廣泛使用,大家似乎就慢慢地淡忘了 jQuery。
瀏覽器原生 API 已經(jīng)足夠好用,我們并不需要為了操作 DOM 而使用 jQuery。
You Dont Need jQuery[5]
再后來,是不是很久沒有直接操作過 DOM 了?
是的,由于 React / Vue 等框架(庫)的出現(xiàn),幫我們做了很多事情,我們可以不用再通過復(fù)雜的 DOM API 來操作 DOM。
我想表達(dá)的是,是不是有一天,如果瀏覽器原生能力足夠好用的時(shí)候,React 等是不是也會(huì)像 jQuery 一樣被瀏覽器原生能力替代?
組件化
像 React / Vue 等框架(庫)都做了同樣的事情,在之前瀏覽器的原生能力是實(shí)現(xiàn)不了的,比如創(chuàng)建一個(gè)可復(fù)用的組件,可以渲染在 DOM 中的任意位置。
現(xiàn)在呢?我們似乎可以不使用任意的框架和庫,甚至不用打包編譯,僅是通過 Web Components 這樣的瀏覽器原生能力就可以創(chuàng)建可復(fù)用的組件,是不是未來的某一天我們就拋棄了現(xiàn)在所謂的框架和庫,直接使用原生 API 或者是使用基于 Web Components 標(biāo)準(zhǔn)的框架和庫來開發(fā)了?
當(dāng)然,未來是不可知的
我不是一個(gè) Web Components 的無腦吹,只不過,我們需要面向未來編程。
來看看 Web Components 的一些主要功能吧。
Custom elements: 自定義元素
自定義元素顧名思義就是用戶可以自定義 HTML 元素,通過 CustomElementRegistry 的 define 來定義,比如:
window.customElements.define('my-element',?MyElement);
然后就可以直接通過 使用了。
根據(jù)規(guī)范,有兩種 Custom elements:
Autonomous custom elements: 獨(dú)立的元素,不繼承任何 HTML元素,使用時(shí)可以直接Customized buld-in elements: 繼承自 HTML元素,比如通過{ extends: 'p' }來標(biāo)識(shí)繼承自p元素,使用時(shí)需要
兩種 Custom elements 在實(shí)現(xiàn)的時(shí)候也有所區(qū)別:
//?Autonomous?custom?elements
class?MyElement?extends?HTMLElement?{
??constructor()?{
????super();
??}
}
// Customized buld-in elements:繼承自 p 元素
class?MyElement?extends?HTMLParagraphElement?{
??constructor()?{
????super();
??}
}
更多關(guān)于 Custom elements[6]
生命周期函數(shù)
在 Custom elements 的構(gòu)造函數(shù)中,可以指定多個(gè)回調(diào)函數(shù),它們將會(huì)在元素的不同生命時(shí)期被調(diào)用。
connectedCallback:元素首次被插入文檔 DOM時(shí)disconnectedCallback:元素從文檔 DOM中刪除時(shí)adoptedCallback:元素被移動(dòng)到新的文檔時(shí) attributeChangedCallback: 元素增加、刪除、修改自身屬性時(shí)
我們這里留意一下 attributeChangedCallback,是每當(dāng)元素的屬性發(fā)生變化時(shí),就會(huì)執(zhí)行這個(gè)回調(diào)函數(shù),并且獲得元素的相關(guān)信息:
attributeChangedCallback(name,?oldValue,?newValue)?{
??//?TODO
}
需要特別注意的是,如果需要在元素某個(gè)屬性變化后,觸發(fā) attributeChangedCallback() 回調(diào)函數(shù),你必須監(jiān)聽這個(gè)屬性:
class?MyElement?extends?HTMLElement?{
??static?get?observedAttributes()?{
????return?['my-name'];
??}
??constructor()?{
????super();
??}
}
元素的 my-name 屬性發(fā)生變化時(shí),就會(huì)觸發(fā)回調(diào)方法。
Shadow DOM
Web Components 一個(gè)非常重要的特性,可以將結(jié)構(gòu)、樣式封裝在組件內(nèi)部,與頁面上其它代碼隔離,這個(gè)特性就是通過 Shadow DOM 實(shí)現(xiàn)。

關(guān)于 Shadow DOM,這里主要想說一下 CSS 樣式隔離的特性。Shadow DOM 里外的 selector 是相互獲取不到的,所以也沒辦法在內(nèi)部使用外部定義的樣式,當(dāng)然外部也沒法獲取到內(nèi)部定義的樣式。
這樣有什么好處呢?劃重點(diǎn),樣式隔離,Shadow DOM 通過局部的 HTML 和 CSS,解決了樣式上的一些問題,類似 vue 的 scope 的感覺,元素內(nèi)部不用關(guān)心 selector 和 CSS rule 會(huì)不會(huì)被別人覆蓋了,會(huì)不會(huì)不小心把別人的樣式給覆蓋了。所以,元素的 selector 非常簡(jiǎn)單:title / item 等,不需要任何的工具或者命名的約束。
更多關(guān)于 Shadow DOM[7]
Templates: 模板
可以通過 來添加一個(gè) Web Component 的 Shadow DOM 里的 HTML 內(nèi)容:
<body>
??<template?id="my-paragraph">
????<style>
??????p?{
????????color:?white;
????????background-color:?#666;
????????padding:?5px;
??????}
????style>
????<p>My?paragraphp>
??template>
??<script>
????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'}).appendChild(templateContent.cloneNode(true));
????????}
??????}
????)
??script>
??<my-paragraph>my-paragraph>
body>
效果:

我們知道, 是不會(huì)直接被渲染的,所以我們是不是可以定義多個(gè) 然后在自定義元素時(shí)根據(jù)不同的條件選擇渲染不同的 ?答案當(dāng)然是:可以。
更多關(guān)于 Templates[8]
vue-lit
介紹了 lit-html/element 和 Web Components,我們回到尤大這個(gè) ?vue-lit。
首先我們看到在 Vue 3.0 的 Release 里有這么一段:
The @vue/reactivity module exports functions that provide direct access to Vue's reactivity system, and can be used as a standalone package. It can be used to pair with other templating solutions (e.g. lit-html) or even in non-UI scenarios.
意思大概就是說 @vue/reactivity 模塊和類似 lit-html 的方案配合,也能設(shè)計(jì)出一個(gè)直接訪問 Vue 響應(yīng)式系統(tǒng)的解決方案。
巧了不是,對(duì)上了,這不就是 vue-lit 嗎?
源碼解析
import?{?render?}?from?'https://unpkg.com/lit-html?module'
import?{
??shallowReactive,
??effect
}?from?'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
lit-html提供核心render能力@vue/reactiity提供Vue響應(yīng)式系統(tǒng)的能力
這里稍帶解釋一下 shallowReactive 和 effect,不展開:
shallowReactive:簡(jiǎn)單理解就是“淺響應(yīng)”,類似于“淺拷貝”,它僅僅是響應(yīng)數(shù)據(jù)的第一層
const?state?=?shallowReactive({
??a:?1,
??b:?{
????c:?2,
??},
})
state.a++?//?響應(yīng)式
state.b.c++?//?非響應(yīng)式
effect:簡(jiǎn)單理解就是 watcher
const?state?=?reactive({
??name:?"前端試煉",
});
console.log(state);?//?這里返回的是Proxy代理后的對(duì)象
effect(()?=>?{
??console.log(state.name);?//?每當(dāng)name數(shù)據(jù)變化將會(huì)導(dǎo)致effect重新執(zhí)行
});
接著往下看:
export?function?defineComponent(name,?propDefs,?factory)?{
??//?propDefs
??//?如果是函數(shù),則直接當(dāng)作工廠函數(shù)
??//?如果是數(shù)組,則監(jiān)聽他們,觸發(fā)?attributeChangedCallback?回調(diào)函數(shù)
??if?(typeof?propDefs?===?'function')?{
????factory?=?propDefs
????propDefs?=?[]
??}
??//?調(diào)用?Web?Components?創(chuàng)建?Custom?Elements?的函數(shù)
??customElements.define(
????name,
????class?extends?HTMLElement?{
??????//?監(jiān)聽?propDefs
??????static?get?observedAttributes()?{
????????return?propDefs
??????}
??????constructor()?{
????????super()
????????//?創(chuàng)建一個(gè)淺響應(yīng)
????????const?props?=?(this._props?=?shallowReactive({}))
????????currentInstance?=?this
????????const?template?=?factory.call(this,?props)
????????currentInstance?=?null
????????//?beforeMount?生命周期
????????this._bm?&&?this._bm.forEach((cb)?=>?cb())
????????//?定義一個(gè)?Shadow?root,并且內(nèi)部實(shí)現(xiàn)無法被?JavaScript?訪問及修改,類似?
????????const?root?=?this.attachShadow({?mode:?'closed'?})
????????let?isMounted?=?false
????????//?watcher
????????effect(()?=>?{
??????????if?(!isMounted)?{
????????????//?beforeUpdate?生命周期
????????????this._bu?&&?this._bu.forEach((cb)?=>?cb())
??????????}
??????????//?調(diào)用?lit-html?的核心渲染能力,參考上文?lit-html?的?Demo
??????????render(template(),?root)
??????????if?(isMounted)?{
????????????//?update?生命周期
????????????this._u?&&?this._u.forEach((cb)?=>?cb())
??????????}?else?{
????????????//?渲染完成,將?isMounted?置為?true
????????????isMounted?=?true
??????????}
????????})
??????}
??????connectedCallback()?{
????????//?mounted?生命周期
????????this._m?&&?this._m.forEach((cb)?=>?cb())
??????}
??????disconnectedCallback()?{
????????//?unMounted?生命周期
????????this._um?&&?this._um.forEach((cb)?=>?cb())
??????}
??????attributeChangedCallback(name,?oldValue,?newValue)?{
????????//?每次修改?propDefs?里的參數(shù)都會(huì)觸發(fā)
????????this._props[name]?=?newValue
??????}
????}
??)
}
//?掛載生命周期
function?createLifecycleMethod(name)?{
??return?(cb)?=>?{
????if?(currentInstance)?{
??????;(currentInstance[name]?||?(currentInstance[name]?=?[])).push(cb)
????}
??}
}
//?導(dǎo)出生命周期
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')
//?導(dǎo)出?lit-hteml?和?@vue/reactivity?的所有?API
export?*?from?'https://unpkg.com/lit-html?module'
export?*?from?'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
簡(jiǎn)化版有助于理解
整體看下來,為了更好地理解,我們不考慮生命周期之后可以簡(jiǎn)化一下:
import?{?render?}?from?'https://unpkg.com/lit-html?module'
import?{
??shallowReactive,
??effect
}?from?'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
export?function?defineComponent(name,?factory)?{
??customElements.define(
????name,
????class?extends?HTMLElement?{
??????constructor()?{
????????super()
????????const?root?=?this.attachShadow({?mode:?'closed'?})
????????effect(()?=>?{
??????????render(factory(),?root)
????????})
??????}
????}
??)
}
也就這幾個(gè)流程:
創(chuàng)建 Web Components的Custom Elements創(chuàng)建一個(gè) Shadow DOM的ShadowRoot節(jié)點(diǎn)將傳入的 factory和內(nèi)部創(chuàng)建的ShadowRoot節(jié)點(diǎn)交給lit-html的render渲染出來
回過頭來看尤大提供的 DEMO:
import?{
??defineComponent,
??reactive,
??html,
}?from?'https://unpkg.com/@vue/lit'
defineComponent('my-component',?()?=>?{
??const?msg?=?'Hello?World'
??const?state?=?reactive({
????show:?true
??})
??const?toggle?=?()?=>?{
????state.show?=?!state.show
??}
??
??return?()?=>?html`
????<button?@click=${toggle}>toggle?childbutton>
????${state.show???html`<my-child?msg=${msg}>my-child>`?:?``}
??`
})
my-component 是傳入的 name,第二個(gè)參數(shù)是一個(gè)函數(shù),也就是傳入的 factory,其實(shí)就是 lit-html 的第一個(gè)參數(shù),只不過引入了 @vue/reactivity 的 reactive 能力,把 state 變成了響應(yīng)式。
沒毛病,和 Vue 3.0 Release 里說的一致,@vue/reactivity 可以和 lit-html 配合,使得 Vue 和 Web Components 結(jié)合到一塊兒了,是不是還挺有意思。
寫在最后
可能尤大只是一時(shí)興起,寫了這個(gè)小玩具,但是可以見得這可能真的是一種大趨勢(shì)。
猜測(cè)不久將來這些關(guān)鍵詞會(huì)突然就爆發(fā):Unbundled / ES Modules / Web components / Custom Element / Shadow DOM...
是不是值得期待一下?
參考資料
https://github.com/vuejs/vue-next/releases/tag/v3.0.0: https://github.com/vuejs/vue-next/releases/tag/v3.0.0
[2]vue-lit: https://github.com/yyx990803/vue-lit
[3]lit-html: https://lit-html.polymer-project.org/
[4]lit-element: https://lit-element.polymer-project.org/
[5]You Dont Need jQuery: https://github.com/nefe/You-Dont-Need-jQuery
[6]更多關(guān)于 Custom elements: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements
[7]更多關(guān)于 Shadow DOM: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM
[8]更多關(guān)于 Templates: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots
掃碼關(guān)注公眾號(hào),訂閱更多精彩內(nèi)容。
“在看和轉(zhuǎn)發(fā)”就是最大的支持
