Web 組件構建庫 - Lit

認識 Lit

抽象與封裝

在《你真的瞭解 Web Component 嗎 [1]》的分享中,我們在介紹 web 組件前,從理解框架和職責範圍的出發點與角度,探究了框架存在和發展的意義及目標,瞭解到了框架可以使快速開發和基礎性能之間達成平衡,進而讓開發者的開發體驗得到較大的提升。

而一個框架的組成,離不開優秀的設計思想,和對這些設計思想的最終實現。實現的整個過程,其實就是一個抽象與封裝的過程。但是這個過程並非是框架獨屬的,我們可以回憶一下,日常的開發中,可以對某些頻繁、重複使用的邏輯進行函數式封裝;可以將某個到處使用的模版進行組件式封裝;甚至我們會引用一些高質量的庫去支持開發,而這些庫,也是一個抽象與封裝的結果。既然抽象與封裝應用如此廣泛,那麼 web component 的創建與使用是不是也可以形成一個抽象與封裝的產物呢?

Lit 的介紹

Lit 是一個輕量的庫,用來快速構建 web 組件。其核心是 LitElement 基類,可以提供響應式狀態、作用域樣式以及高效靈活的模版系統。儘管它也是一個基於原生 web 組件而封裝的庫,但它依然保留了 web 組件的所有特性。不必依賴框架便可以實現組件化,並且它的使用不受框架的制約,甚至可以沒有框架。

基本特點

Lit 的應用及基本原理

Demo 示例

暫時無法在文檔外展示此內容

基本組成及應用

基類

基類 (LitElement) 是 lit 最核心的組成,它繼承了原生的 HTMLElement 類,並在此基礎上進行了豐富的擴展。包括響應式狀態、生命週期以及一些諸如控制器和混入等高級用法的提供。

裝飾器

可以理解爲一些語法糖,用於修改類、類方法及屬性的特殊函數。

window.customElements.define('my-element')
dom.addEventListener(eventName,func,{capture: truepassivetrueoncetrue})
  constructor() {

    super();

    this.test = 'Somebody'; 

  }

  

  static get properties() {

    return {

      test: {type: String},

    }

  }

options 是一個配置對象,其中包含:

constructor() {

    super();

    this._active = false;

  }

  

  static get properties() {

    return {

      _active: {state: true}

    }

  }

options 是一個配置對象,其中包含:

document.querySelector('todo-list');
document.querySelectorAll('todo-lists');

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 邏輯。非常的靈活與方便。

樣式

//只有一組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 {cssunsafeCSS} from 'lit-element';

import style from './my-elements.less';//需要使用編譯工具編譯後倒入

  static styles = [

    css`:host {

    width:500px;

  }`,

    css`${unsafeCSS(style)}`

    ];

這就意味着你可以在某個 ts 文件中聲明一組樣式,然後引入到多個文件中使用:

//樣式中使用表達式

static get styles() {

  const mainColor = 'red';

  return css`

    div { color: ${unsafeCSS(mainColor)} }

  `;

}

可以想 vue 或 react 中一樣,動態的使用 class 和 style。

import {customElement, propertyLitElement, 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。

  <my-element test-word="test-word" testWord="testWord">

    <div>我是子元素</div>

  </my-element>

  //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>

    `;

  }

  //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 ></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   @slotchange=${this.handleSlotchange}></slot>

    `;

  }

}

事件通信

在 Lit 中,也是內置了事件通信的邏輯。事件通信主要是兩部分組成,註冊監聽和調度觸發。

。。。

  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>

    。。。

    `;

  }

  。。。
...

  private sendText(){

    const options = {

      detail: this.inputText,

    };

    this.dispatchEvent(new CustomEvent('addList', options));

    this.inputText = '';

  }

...

生命週期及更新流程

Lit 採用批量更新的方式來提高性能和效率。一次設置多個屬性只會觸發一次更新,然後在微任務定時異步中執行。

狀態更新時,只渲染 DOM 中發生改變的部分。由於 Lit 只解析並創建一次靜態 HTML ,並且後續只更新表達式中更改的值,所以更新非常高效。

lit 中的生命週期分爲兩類,一類是原生組件化提供的生命週期,一般不需要開發者主動去使用。另一類是 lit 的狀態更新提供的生命週期,如下:

執行了 requestUpdateInternal 方法,並返回了更新結果的 promise。requestUpdateInternal 方法中主要進行了兩個操作,一個是將更新的屬性存在一個 map 中,以備後續使用。另一個是進行一些比較判斷,決定是否調用_enqueueUpdate 方法。而_enqueueUpdate 調用了 performUpdate 方法。

performUpdate 方法中執行了 shouldUpdate,shouldUpdate 返回 false 則中斷,若返回 true,則依次執行 willUpdate、update、firstUpdated 和 updated。

ts 中不存在 willUpdate,是 js 的 polyfill-support 的覆蓋點,源碼中函數內容爲空。

主要執行了_propertyToAttribute 函數,將 property 向 attribute 映射,覆蓋 render 渲染出來的 html。

render 函數只執行一次, 用來解析並創建靜態 HTML。

是一個覆蓋點,源碼中爲空。元素首次更新完畢觸發,只執行一次。此方法中設置屬性,會在本次更新完畢後再次觸發更新。

是一個覆蓋點,源碼中爲空。元素每次更新完畢觸發。此方法中設置屬性,會在本次更新完畢後再次觸發更新。

是函數_getUpdateComplete 的返回值,本質上是一個 promise 對象,表示當前更新全部完畢。

 protected _getUpdateComplete() {

    return this.getUpdateComplete();

  }

 protected getUpdateComplete() {

    return this._updatePromise;

 }

高階應用

指令

如上面的動態樣式一樣,classMap 和 styleMap 屬於應用在 html 模版中的指令。開發者可以直接使用內置指令進行開發;也可以根據自己的需要,進行自定義指令的開發。

自定義指令的功能很強大,儘管在使用中看起來只是調用了一個函數,但實際上,內部包含了自己的生命週期 (constructor、render、update),不僅如此,指令還能獲得與它關聯的底層 DOM 的特殊訪問。

這裏我們實現一個簡單的指令:

//自定義指令的文件

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 中,又一個封裝抽象的概念,它區別於組件和混合。它沒有視圖,也不封裝視圖;它擁有同宿主綁定的生命週期,但是不存在狀態更新的機制。相對於混合的代碼共用,它更像是實現功能共用。

與宿主交互的相關方法:

有四個可以與宿主綁定的生命週期:

實現一個簡單的控制器:

//聲明控制器

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>

    `;

  }

}

生態相關

foRidk

路由

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 的開源組件庫

jOEaek

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

其他相關參考

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

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/FIPCzKfxDZrs_g97AFBpgg