Web元件構建庫-Lit

語言: CN / TW / HK

       

認識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 https://lit.dev/ 4

路由

Lit官方並未提供路由,但是社群提供了:

lit-element-router傳送門:https://www.npmjs.com/package/lit-element-router

共享狀態管理

Lit官方並未提供共享狀態管理,但是社群提供了:

lit-element-state傳送門:https://www.npmjs.com/package/lit-element-state

開發外掛(vscode)

語法高亮(lit-plugin)

高亮前 高亮後

程式碼片段(LitElement Snippet)

js下程式碼片段提示 ts下程式碼片段提示

SSR

支援ssr,並且官方提供了對應的工具包:

@lit-labs/ssr :https://www.npmjs.com/package/@lit-labs/ssr

github地址

https://github.com/lit/lit/

測試工具

lit是標準的js工具庫,可以使用任何js測試工具。

https://lit.dev/docs/tools/testing/

依賴Lit的組織及專案

同類框架/庫比較

可以看到lit的下載量遙遙領先。當然,這並不意味著lit就是最好的,畢竟不同的場景,有不同的選型。但至少可以確定一點,lit的應用場景相對是比較多的,這也是下載量不斷飆升的原因。

依賴Lit的開源元件庫

名稱 官網 github 開源協議 背書公司
Spectrum Web Components https://opensource.adobe.com/spectrum-web-components/ https://github.com/adobe/spectrum-web-components Apache License adobe
Momentum UI Web Components https://momentum-design.github.io/momentum-ui/?path=/story/components-accordion--accordion https://github.com/momentum-design/momentum-ui/tree/master/web-components MIT cisco
material-components https://github.com/material-components/material-components-web#readme https://github.com/material-components/material-web Apache License 2.0 google
frontend ui https://github.com/home-assistant/frontend/blob/dev/README.md https://github.com/home-assistant/frontend Apache License home-assistant
carbon-web-components https://web-components.carbondesignsystem.com/ https://github.com/carbon-design-system/carbon-web-components Apache License 2.0 IBM
Lion Web Components https://lion-web.netlify.app/ https://github.com/ing-bank/lion MIT License ING
PWA Starter https://github.com/pwa-builder/pwa-starter/blob/main/README.md https://github.com/pwa-builder/pwa-starter MIT License microsoft

以上只提供了部分背書公司較知名的元件庫(均為國外元件庫,國內目前暫無知名的相關元件庫)。用以觀察借鑑,規避踩坑。

其他相關參考

lit專案所有者:https://github.com/orgs/lit/people =》https://github.com/e111077

lit專案被依賴關係:https://github.com/lit/lit/network/dependents

Lit-element npm包地址:https://www.npmjs.com/package/lit-element

框架對比網站:https://www.npmtrends.com/lit-element-vs-svelte-vs-@stencil/core

參考資料

[1]

你真的瞭解Web Component嗎: https://juejin.cn/post/7010580819895844878

- END -

:heart: 謝謝支援

以上便是本次分享的全部內容,希望對你有所幫助^_^

喜歡的話別忘了 分享、點贊、收藏 三連哦~。

歡迎關注公眾號 ELab團隊 收貨大廠一手好文章~

我們來自位元組跳動,是旗下大力教育前端部門,負責位元組跳動教育全線產品前端開發工作。

我們圍繞產品品質提升、開發效率、創意與前沿技術等方向沉澱與傳播專業知識及案例,為業界貢獻經驗價值。包括但不限於效能監控、元件庫、多端技術、Serverless、視覺化搭建、音視訊、人工智慧、產品設計與營銷等內容。

歡迎感興趣的同學在評論區或使用內推碼內推到作者部門拍磚哦

位元組跳動校/社招投遞連結: https://job.toutiao.com/s/FwRLWuU

內推碼:QF2RCT8