Web元件構建庫-Lit
大 廠 技 術 堅 持 周 更 精 選 好 文
認識Lit
抽象與封裝
在《 你真的瞭解Web Component嗎 [1] 》的分享中,我們在介紹web元件前,從理解框架和職責範圍的出發點與角度,探究了框架存在和發展的意義及目標,瞭解到了框架可以使快速開發和基礎效能之間達成平衡,進而讓開發者的開發體驗得到較大的提升。
而一個框架的組成,離不開優秀的設計思想,和對這些設計思想的最終實現。實現的整個過程,其實就是一個抽象與封裝的過程。但是這個過程並非是框架獨屬的,我們可以回憶一下,日常的開發中,可以對某些頻繁、重複使用的邏輯進行函式式封裝;可以將某個到處使用的模版進行元件式封裝;甚至我們會引用一些高質量的庫去支援開發,而這些庫,也是一個抽象與封裝的結果。既然抽象與封裝應用如此廣泛,那麼web component的建立與使用是不是也可以形成一個抽象與封裝的產物呢?
Lit的介紹
Lit是一個輕量的庫,用來快速構建web元件。其核心是LitElement基類,可以提供響應式狀態、作用域樣式以及高效靈活的模版系統。儘管它也是一個基於原生web元件而封裝的庫,但它依然保留了web元件的所有特性。不必依賴框架便可以實現元件化,並且它的使用不受框架的制約,甚至可以沒有框架。
基本特點
-
類jsx/tsx語法。
-
模版語法類似模版字串的寫法。
-
只支援原生css,但預編譯的樣式,需藉助打包器編譯實現後,通過特定方式引入使用。
-
支援ts。
-
程式設計模型:OOP。
-
單向資料流,支援MVVM,但無雙向繫結。
-
元件狀態管理:使用內建的@state與@property實現。
-
職責範圍小,單純處理web元件的建立與使用。
-
跨平臺跨框架。
-
庫的體積較小,據官網說明,gzip壓縮混淆後只有5k左右。
-
Lit的html模版中,可以通過(.[屬性])的方式,進行自定義回撥的傳遞(官網未說明,屬於hack方式)
Lit的應用及基本原理
Demo示例
暫時無法在文件外展示此內容
基本組成及應用
基類
基類(LitElement)是lit最核心的組成,它繼承了原生的HTMLElement類,並在此基礎上進行了豐富的擴充套件。包括響應式狀態、生命週期以及一些諸如控制器和混入等高階用法的提供。
裝飾器
可以理解為一些語法糖,用於修改類、類方法及屬性的特殊函式。
-
@customElement('my-element'),註冊自定義元件,相當於js中的
window.customElements.define('my-element')。
-
@eventOptions({capture: true,passive:true,once:true}),事件監聽配置,相當於js中的
dom.addEventListener(eventName,func,{capture: true,passive:true,once:true})。
-
@property(options?) test:string= 'Somebody',公共屬性狀態的宣告; 相當於js中的
constructor() {
super();
this.test = 'Somebody';
}
static get properties() {
return {
test: {type: String},
}
}
options是一個配置物件,其中包含:
-
attribute:表示宣告的property是否與元件中元素的attribute建立連線,false表示不建立,true表示建立,並且attribute的名字與property同名。為string時,表示建立並且attribute的名字為該字串。(建立連線,表示該property與attribute相互對映。)
-
converter:表示處理property與attribute之間的轉換規則。
-
預設時,attribute=》property,property為宣告時型別,attribute為string;
-
為function時,處理attribute=》property;
-
為object時,fromAttribute處理attribute=》property;toAttribute處理property=》attribute。
-
noAccessor:表示是否監聽該屬性變化並自動更新,預設為false,表示監聽並自動更新。為true時,表示不自動更新,需要開發者呼叫this.requestUpdate(propertyName, oldValue),來進行檢視更新。
-
reflect:表示是否將property的變化同步到attribute,預設false。false時,不會同步,儘管你設定了converter;為true時,會同步,根據converter的設定轉換。
-
type:表示型別宣告。
-
hasChanged:是一個函式,引數為value 與oldValue,分別是屬性的新值與舊值。如果返回false,表示屬性相關的檢視不需更新;返回true則相反。
-
@state(options) protected _active = false, 內部屬性狀態的宣告,本質上也是一個property,同樣會觸發檢視的更新機制;相當於js中的
constructor() {
super();
this._active = false;
}
static get properties() {
return {
_active: {state: true}
}
}
options是一個配置物件,其中包含:
-
hasChanged:同上。
-
@query('todo-list') todoList!: TodoList;獲取單個dom元素,相當於js中的
document.querySelector('todo-list');
-
@queryAll('todo-lists') todoLists!: TodoLists;獲取批量dom元素,相當於js中的
document.querySelectorAll('todo-lists');
-
@queryAssignedNodes(slotName, flatten, selector),用來獲取對應slot元素的。
-
slotName:slot的name,string型別。
-
flatten:是否平鋪,boolean型別。
-
selector:是否過濾出當前選擇器的slot。
-
@queryAsync('todo-list'),同@query一樣,用來獲取單個dom的,只是@query是同步獲取,返回的是dom物件;@queryAsync是非同步獲取,執行時機在dom更新完成以後,執行updateComplete的promise後執行,返回的是一個promise物件。
html模版
我們來看這樣一段關於html模版的程式碼
。。。
render() {
if(this.listItems.filter(item => !item.completed).length === 0) {
return html`<p>anything was done!</p>`;
}
return html`
<ul>
${this.listItems.map((item) => html`
<li
class=${item.completed ? 'completed' : ''}
@click=${() => this.toggleCompleted(item)}>
${item.text}
</li>`,
)}
</ul>
`;
}
。。。
可以看到Lit的html渲染是在render函式中進行的。通過html方法和模版字串的結合,實現渲染。並且語法類似jsx/tsx。可以直接在render函式和html模版中寫js邏輯。非常的靈活與方便。
樣式
-
Lit本身只支援原生css,使用css方法新增樣式,方式如下:
//只有一組style
import {customElement, css} from 'lit-element';
@customElement('my-element')
export class MyElement extends LitElement {
static styles = css`
p {
color: green;
}
`;
。。。
}
//多組style
import {css} from 'lit-element';
static styles = [
css`h1 {
color: green;
} `,
css`h2 {
color: red;
}`
];
//引入樣式檔案
import {css,unsafeCSS} from 'lit-element';
import style from './my-elements.less';//需要使用編譯工具編譯後倒入
static styles = [
css`:host {
width:500px;
}`,
css`${unsafeCSS(style)}`
];
-
Lit允許在css中書寫表示式
這就意味著你可以在某個ts檔案中宣告一組樣式,然後引入到多個檔案中使用:
//樣式中使用表示式
static get styles() {
const mainColor = 'red';
return css`
div { color: ${unsafeCSS(mainColor)} }
`;
}
-
動態樣式
可以想vue或react中一樣,動態的使用class和style。
import {customElement, property,LitElement, html, css} from 'lit-element';
import {classMap} from 'lit/directives/class-map.js';
import {styleMap} from 'lit/directives/style-map.js';
@customElement('my-element')
export class MyElement extends LitElement {
@property()
classes = { someclass: true, anotherclass: true };
@property()
styles = { color: 'lightgreen', fontFamily: 'Roboto' };
protected render() {
return html`
<div class=${classMap(this.classes)} style=${styleMap(this.styles)}>
content
</div>
`;
}
}
slot插槽
-
概念
關於插槽的概念理解,其實可以直接類比vue中的slot插槽,因為lit本身是一個純js庫,所以lit的插槽完全是來源於原生web component技術所提供的規範,而vue的slot功能,參考來源正是原生的slot。
-
注意:warning:
-
預設情況下,如果一個自定義元素有存在shadow dom,那麼它的子元素是不會渲染的。
<my-element test-word="test-word" testWord="testWord">
<div>我是子元素</div>
</my-element>
-
預置slot中的內容,可以在對應子元素渲染前,起到兜底的作用。
-
slot的使用
-
匿名slot,只需要預置一個slot,那麼所有的子元素都可以被安排呈現。
//html
<my-element test-word="test-word" testWord="testWord">
<div>我是子元素1</div>
<div>我是子元素2</div>
<div>我是子元素3</div>
</my-element>
//ts
render() {
return html`
<slot></slot>
`;
}
-
具名slot,只根據對應的name呈現,其他的被忽略
//html
<my-element test-word="test-word" testWord="testWord">
<div slot="child1">我是子元素1</div>
<div>我是子元素2</div>
<div>我是子元素3</div>
</my-element>
//ts
render() {
return html`
<slot name="child1"></slot>
`;
}
-
slot的生命週期
可以通過在slot元素上面繫結slotchange事件,來獲取slot被插入或刪除的時機,以及對應的事件物件。
//html
<my-element test-word="test-word" testWord="testWord">
<div slot="child1">我是子元素1</div>
</my-element>
//ts
handleSlotchange(e:Event) {
console.log(e);
}
render() {
return html`
<slot name="child1" @slotchange=${this.handleSlotchange}></slot>
`;
}
}
事件通訊
在Lit中,也是內建了事件通訊的邏輯。事件通訊主要是兩部分組成,註冊監聽和排程觸發。
-
註冊監聽,採用@[事件名]的方式,在元件上註冊事件;使用e.detail來回去通訊內容。
。。。
private addList(e: CustomEvent){
this.listItems = [...this.listItems,{
text:e.detail,
completed: false,
} as ToDoItem];
this.todoList.requestUpdate();
}
。。。
render() {
return html`
。。。
<controls-area @addList=${this.addList}></controls-area>
。。。
`;
}
。。。
-
排程觸發,使用dispatchEvent進行排程,使用CustomEvent建構函式及固定的結構來例項化通訊傳輸的事件物件。
...
private sendText(){
const options = {
detail: this.inputText,
};
this.dispatchEvent(new CustomEvent('addList', options));
this.inputText = '';
}
...
生命週期及更新流程
Lit採用批量更新的方式來提高效能和效率。一次設定多個屬性只會觸發一次更新,然後在微任務定時非同步中執行。
狀態更新時,只渲染 DOM 中發生改變的部分。由於 Lit 只解析並建立一次靜態 HTML ,並且後續只更新表示式中更改的值,所以更新非常高效。
-
更新流程
-
當屬性被set時,屬性的setter被觸發(屬性的監聽通過defineProperty完成)。
-
然後將觸發元件的requestUpdate。
-
此時若該屬性設定了hasChanged函式,那麼等待該函式返回值來決定是否繼續更新。若沒有設定hasChanged函式,則直接對比新舊值。
-
新舊值不一致時,觸發非同步更新,呼叫元件的update方法。如果發現已經觸發了一次更新,那麼執行最後一個更新。
-
觸發更新後,將更新的property再次對映到attribute中,並渲染html。
-
生命週期
lit中的生命週期分為兩類,一類是原生元件化提供的生命週期,一般不需要開發者主動去使用。另一類是lit的狀態更新提供的生命週期,如下:
-
requestUpdate
執行了requestUpdateInternal方法,並返回了更新結果的promise。requestUpdateInternal方法中主要進行了兩個操作,一個是將更新的屬性存在一個map中,以備後續使用。另一個是進行一些比較判斷,決定是否呼叫_enqueueUpdate方法。而_enqueueUpdate呼叫了performUpdate方法。
-
performUpdate
performUpdate方法中執行了shouldUpdate,shouldUpdate返回false則中斷,若返回true,則依次執行willUpdate、update、firstUpdated和updated。
-
willUpdate
ts中不存在willUpdate,是js的polyfill-support 的覆蓋點,原始碼中函式內容為空。
-
Update
主要執行了_propertyToAttribute函式,將property向attribute對映,覆蓋render渲染出來的html。
-
Render
render函式只執行一次,用來解析並建立靜態 HTML。
-
firstUpdated
是一個覆蓋點,原始碼中為空。元素首次更新完畢觸發,只執行一次。此方法中設定屬性,會在本次更新完畢後再次觸發更新。
-
updated
是一個覆蓋點,原始碼中為空。元素每次更新完畢觸發。此方法中設定屬性,會在本次更新完畢後再次觸發更新。
-
updateComplete
是函式_getUpdateComplete的返回值,本質上是一個promise物件,表示當前更新全部完畢。
protected _getUpdateComplete() {
return this.getUpdateComplete();
}
protected getUpdateComplete() {
return this._updatePromise;
}
高階應用
指令
如上面的動態樣式一樣,classMap和styleMap屬於應用在html模版中的指令。開發者可以直接使用內建指令進行開發;也可以根據自己的需要,進行自定義指令的開發。
-
內建指令
-
classMap - 將類列表設定為基於物件的元素
-
styleMap - 將樣式屬性列表設定為基於物件的元素
-
repeat - 將值從可迭代物件渲染到 DOM 中
-
templageContent- 呈現
<template>
元素的內容 -
unsafeHTML - 將字串呈現為 HTML
-
unsafeSVG - 將字串呈現為 SVG
-
cache - 更改模板時快取呈現的 DOM
-
guard - 僅在其依賴項之一發生變化時重新更新模板
-
ifDefined - 如果值已定義,則設定該屬性,如果未定義,則刪除該屬性
-
live - 採用嚴格的‘===’檢查實時DOM值與表示式的值,不相等便觸發更新。
-
until - 呈現佔位符內容,直到一個或多個Promise解決
-
asyncAppend- 將
AsyncIterable
的promise結果插入到 DOM 中 -
asyncReplace- 將
AsyncIterable
的promise結果替換到 DOM 中 -
ref - 獲取dom節點
-
自定義指令
自定義指令的功能很強大,儘管在使用中看起來只是呼叫了一個函式,但實際上,內部包含了自己的生命週期(constructor、render、update),不僅如此,指令還能獲得與它關聯的底層 DOM 的特殊訪問。
這裡我們實現一個簡單的指令:
-
首先宣告一個繼承了Directive類的自定義指令類。
-
內部定義生命週期的鉤子函式render,將傳入的字串修改後返回。
-
使用directive例項化,並向外暴露。
//自定義指令的檔案
import {Directive, directive} from 'lit/directive.js';
class FormatStr extends Directive {
render(test:string) {
return `${test}!!!`;
}
}
export const formatStr = directive(FormatStr);
//使用自定義指令的檔案
import {formatStr} from '../directives/formatStr';
import {html} from 'lit';
。。。
render(){
html`<div>${formatStr('hellow')}</div>`
}
。。。
混和
混合本身的作用,是為了在類之間共享程式碼。而類混合,本質上是屬於原生類的一種行為,由於Lit是一個原生的js庫,所以它可以拿來直接使用。目的也很簡單,為了封裝抽象。在Lit中使用類混合,我們可以在複用程式碼的同時,做一些定製化的擴充套件。下面我們實現一個混合。
這裡我們宣告一個混合類的方法,使得呼叫這個函式後生成的類,擁有公共的方法,就是在元件連線主文件後進行一次列印,列印的結果,是當前元件自己的name屬性。
/* eslint-disable no-unused-vars */
import {LitElement} from 'lit';
type Constructor<T = {}> = new (...args: any[]) => T;
export const TestMixin = <S extends Constructor<LitElement>>(superClass: S) => {
class MyMixinClass extends superClass {
constructor(...args: any[]) {
super();
this.name = 'TestMixin';
}
name:string;
connectedCallback() {
super.connectedCallback();
setTimeout(()=>{
console.log(this.name);
},3000)
}
}
return MyMixinClass as S;
}
這裡是使用的檔案:
//TodoList.ts
export class TodoList extends TestMixin(LitElement) {
constructor() {
super();
this.name = 'TodoList';
}
name:string;
}
//ControlsArea.ts
export class ControlsArea extends TestMixin(LitElement) {
constructor() {
super();
this.name = 'ControlsArea';
}
name:string;
}
這裡是列印結果,分別會在連線到主文件3s後列印各自的name。
控制器
控制器是Lit中,又一個封裝抽象的概念,它區別於元件和混合。它沒有檢視,也不封裝檢視;它擁有同宿主繫結的生命週期,但是不存在狀態更新的機制。相對於混合的程式碼共用,它更像是實現功能共用。
與宿主互動的相關方法:
-
addController:同宿主繫結。
-
removeController:同宿主解除繫結。
-
requestUpdate:更新宿主檢視。
-
updateComplete:獲取宿主更新完畢的promise。
有四個可以與宿主繫結的生命週期:
-
hostConnected:宿主連線主文件時執行。
-
hostUpdate:宿主將property向attribute對映完畢(update)後,render之前執行。
-
hostUpdated:在元件每次更新完畢(updated)後執行。
-
hostDisconnected:宿主與主文件斷開連線時執行。
實現一個簡單的控制器:
//宣告控制器
import {ReactiveControllerHost} from 'lit';
export class MouseController {
private host: ReactiveControllerHost;
pos = {x: 0, y: 0};
_onMouseMove = ({clientX, clientY}: MouseEvent) => {
this.pos = {x: clientX, y: clientY};
this.host.requestUpdate();
};
constructor(host: ReactiveControllerHost) {
this.host = host;
host.addController(this);
}
hostConnected() {
window.addEventListener('mousemove', this._onMouseMove);
}
hostDisconnected() {
window.removeEventListener('mousemove', this._onMouseMove);
}
}
//使用控制器
import {MouseController} from '../controller/mouseController';
export class ControlsArea extends TestMixin(LitElement) {
constructor() {
super();
}
private mouse = new MouseController(this);
render() {
return html`
<pre>
x: ${this.mouse.pos.x as number}
y: ${this.mouse.pos.y as number}
</pre>
`;
}
}
生態相關
Github star | issue數量(未解決/總數) | npm下載量 | 被依賴數量 | 維護團隊 | 開源協議 | 文件 | 年齡 | 背書公司 |
---|---|---|---|---|---|---|---|---|
8.7k | 171/2084 | lit團隊 | BSD-3-Clause | http://lit.dev/ | 4 | 無 |
路由
Lit官方並未提供路由,但是社群提供了:
lit-element-router傳送門:http://www.npmjs.com/package/lit-element-router
共享狀態管理
Lit官方並未提供共享狀態管理,但是社群提供了:
lit-element-state傳送門:http://www.npmjs.com/package/lit-element-state
開發外掛(vscode)
語法高亮(lit-plugin)
高亮前 高亮後
程式碼片段(LitElement Snippet)
js下程式碼片段提示 ts下程式碼片段提示
SSR
支援ssr,並且官方提供了對應的工具包:
@lit-labs/ssr :http://www.npmjs.com/package/@lit-labs/ssr
github地址
http://github.com/lit/lit/
測試工具
lit是標準的js工具庫,可以使用任何js測試工具。
http://lit.dev/docs/tools/testing/
依賴Lit的組織及專案
同類框架/庫比較
可以看到lit的下載量遙遙領先。當然,這並不意味著lit就是最好的,畢竟不同的場景,有不同的選型。但至少可以確定一點,lit的應用場景相對是比較多的,這也是下載量不斷飆升的原因。
依賴Lit的開源元件庫
名稱 | 官網 | github | 開源協議 | 背書公司 |
---|---|---|---|---|
Spectrum Web Components | http://opensource.adobe.com/spectrum-web-components/ | http://github.com/adobe/spectrum-web-components | Apache License | adobe |
Momentum UI Web Components | http://momentum-design.github.io/momentum-ui/?path=/story/components-accordion--accordion | http://github.com/momentum-design/momentum-ui/tree/master/web-components | MIT | cisco |
material-components | http://github.com/material-components/material-components-web#readme | http://github.com/material-components/material-web | Apache License 2.0 | |
frontend ui | http://github.com/home-assistant/frontend/blob/dev/README.md | http://github.com/home-assistant/frontend | Apache License | home-assistant |
carbon-web-components | http://web-components.carbondesignsystem.com/ | http://github.com/carbon-design-system/carbon-web-components | Apache License 2.0 | IBM |
Lion Web Components | http://lion-web.netlify.app/ | http://github.com/ing-bank/lion | MIT License | ING |
PWA Starter | http://github.com/pwa-builder/pwa-starter/blob/main/README.md | http://github.com/pwa-builder/pwa-starter | MIT License | microsoft |
以上只提供了部分背書公司較知名的元件庫(均為國外元件庫,國內目前暫無知名的相關元件庫)。用以觀察借鑑,規避踩坑。
其他相關參考
lit專案所有者:http://github.com/orgs/lit/people =》http://github.com/e111077
lit專案被依賴關係:http://github.com/lit/lit/network/dependents
Lit-element npm包地址:http://www.npmjs.com/package/lit-element
框架對比網站:http://www.npmtrends.com/[email protected]/core
參考資料
你真的瞭解Web Component嗎: http://juejin.cn/post/7010580819895844878
- END -
:heart: 謝謝支援
以上便是本次分享的全部內容,希望對你有所幫助^_^
喜歡的話別忘了 分享、點贊、收藏 三連哦~。
歡迎關注公眾號 ELab團隊 收貨大廠一手好文章~
我們來自位元組跳動,是旗下大力教育前端部門,負責位元組跳動教育全線產品前端開發工作。
我們圍繞產品品質提升、開發效率、創意與前沿技術等方向沉澱與傳播專業知識及案例,為業界貢獻經驗價值。包括但不限於效能監控、元件庫、多端技術、Serverless、視覺化搭建、音視訊、人工智慧、產品設計與營銷等內容。
歡迎感興趣的同學在評論區或使用內推碼內推到作者部門拍磚哦
位元組跳動校/社招投遞連結: http://job.toutiao.com/s/FwRLWuU
內推碼:QF2RCT8
- 使用 WebAssembly 打造定製 JS Runtime
- 前端也要懂演算法,不會演算法也能微調一個 NLP 預訓練模型
- 聯機遊戲原理入門即入土 -- 入門篇
- Plasmo Framework:次世代的瀏覽器外掛開發框架
- 深入理解 Mocha 測試框架:從零實現一個 Mocha
- Single Source of Truth:XCode SwiftUI 的介面編輯的設計理念
- 深入理解 D3.js 視覺化庫之力導向圖原理與實現
- 淺析神經網路 Neural Networks
- Cutter - Web視訊剪輯工具原理淺析
- 你可能需要一個四捨五入的工具函式
- 淺析eslint原理
- 最小編譯器the-super-tiny-compiler
- Git儲存原理及部分實現
- 淺談短鏈的設計
- Web元件構建庫-Lit
- 使用Svelte開發Chrome Extension
- Web3.0開發入門
- vscode外掛原理淺析與實戰
- 深入淺出 Web Audio API
- 探祕HTTPS