JavaScript Library – Lit

前言

我寫過一篇關於 Lit 的文章,Material Design, Angular Material, MDC, MWC, Lit 的關係

如今 material-web MWC 已經發布 1.0 了,估計 Angular 也會在不遠的將來從 material-components-web MDC 遷移到 MWC。

以後,我們要想深入理解 Angular Material 就必須對 MWC 有一定了解,而 MWC 又是基於 Lit 開發的,所以我們也需要了解 Lit。

這篇就讓我們來看看 Lit 吧。

 

參考

Lit 官網文檔

 

Lit 介紹

Lit 的前生是 Google 的 Polymer。它是一個幫助我們寫標準 Web Components 的庫。

它有 2 個特點:

  1. 標準 W3C Web Components
    Lit 開發出來的組件是 W3C 規範的 Web Components,不像 Angular、React、Vue 那些都是仿冒的。 
    符合標準的好處是可以 Plug and Play,組件本身不依賴框架技術。
    我用 Lit 寫出來的組件,可以拿到 Angular、React、Vue 任何項目裏跑。
  2. 提升開發體驗
    在不借助任何庫的情況下,手寫 W3C Web Components 開發體驗是很差的,代碼可讀性也差。
    Lit 主要就是爲了解決這些問題而誕生的。
    它藉助 DecoratorTemplate literals 特性,實現了聲明式定義組件和 MVVM。

 

Lit Getting Started

Lit 的目的是開發出 W3C Web Components,所以要掌握 Lit 就必須先掌握 W3C Web Components。

不熟悉的朋友們,請先看我以前寫的這篇 DOM – Web Components

安裝

Lit 是可以用在純 HTML、CSS、JS 上的,但是會降低開發體驗。所以我還是鼓勵大家用 TypeScript。

我這裏搭配 Vite 做演示,你想改用 Webpack 或 Rollup 也都可以。

yarn create vite

它默認會有一些 sample code,我們洗掉它,從一個乾淨的開始。

index.css

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

index.ts 清空

index.html

<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Vite + Lit + TS</title>
  <link rel="stylesheet" href="./src/index.css" />
  <script type="module" src="/src/index.ts"></script>
</head>

<body>
</body>

</html>

創建組件

Web Components 是由 HTML TemplatesShadow DOMCustom Elements 三種獨立的技術搭配而成的。

而 Lit 把它們組合在一起了。

hello-world.ts

import { LitElement, css, html } from 'lit';
import { customElement } from 'lit/decorators.js';

// 定義組件
// 取代了 window.customElements.define('hello-world', HelloWorldElement);
@customElement('hello-world')
export class HelloWorldElement extends LitElement {
  // 取代了 <template>
  render() {
    return html`<h1>Hello World</h1>`;
  }

  // 取代了 <style>
  static styles = css`
    h1 {
      color: red;
    }
  `;
}

// declare type for TypeScript
declare global {
  interface HTMLElementTagNameMap {
    'hello-world': HelloWorldElement;
  }
}

HTML 和 CSS 用了 Template literals 技術。它不像 Angular 搞 compiler 黑魔法,這個只是單純的 JS runtime render。

Tips: template literals 對 IDE 不友好,需要插件 lit-html 和 vscode-styled-components

使用組件

index.ts

import './hello-world';

setTimeout(() => {
  // 動態使用
  const helloWorld = document.createElement('hello-world');
  document.body.appendChild(helloWorld);
}, 3000);

記得要 import 先。

index.html

<body>
  <!-- 靜態使用 -->
  <hello-world></hello-world>
</body>

效果

 

Lit の Shadow DOM

所有原生 Shadow DOM 的特性,在 Lit 都可以用。

比如::host<slot>::slotted()::part()

除了 :host-context(),相關提問:Stack Overflow – :host-context not working as expected in Lit-Element web component

主要是因爲 Firefox 和 Safari 本來就不支持 :host-context 所以 Lit 乾脆就完全不支持了。可以使用 CSS Variables 作爲替代方案。

從這裏也能看出,Lit 實現 Web Components 的手法我們直覺認爲的不太一樣,它裏面動了一些手腳。

 

Lit の Custom Elements

所有原生 Custom Elements 的特性,在 Lit 都可以用。

比如 lifecycle:connectedCallback、disconnectedCallback、attributeChangedCallback、static get observedAttributes。

 

Lit の MVVM

到目前爲止,我們看到的 @customElement、extends LitElement、html``、css`` 只是 Lit 的小角色。

真正讓 Lit 發亮起來的是它的 MVVM,這也是 W3C Web Components 最缺失的功能。

MVVM 的中心思想

MVVM 的宗旨就是不要直接操作 DOM,不要調用 DOM API,凡事都通過 MVVM 庫去控制。

Lit 提供了很多種 binding、listening、query 的方式去取代 DOM 操作。

binding & listening

@customElement('hello-world')
export class HelloWorldElement extends LitElement {
  @state()
  private value = 'default value';

  private updateValue() {
    this.value = 'new value';
  }

  render() {
    return html`
      <h1>${this.value}</h1>
      <button @click="${this.updateValue}">update value</button>
    `;
  }

  static styles = css``;
}

效果

如果你熟悉 Angular,那應該對 Lit 的語法不會感到太陌生。它倆其實挺像的。畢竟都是 Google 出品,師出同門嘛。

我們逐個來看

有一個 value 屬性,@state() 表示這個屬性會被用於模板。這時 Lit 就知道每當這個屬性值發生變化,那就需要 re-render(它具體如何實現重渲染我不清楚,估計不會是大面積的替換,應該會做性能優化)

有一個 updateValue 方法,調用它就更新 value 屬性。

把 value 屬性插入模板。

@click 是一個特殊字符串,表示監聽 click 事件,接着把 updateValue 方法插入模板。

至此,Lit 就掌握足夠信息,可以做監聽和 update DOM 了。

Attribute & Property

@state 和 @property 的區別是,一個是 private 一個是 public。

@customElement('hello-world')
export class HelloWorldElement extends LitElement {
  @property({ type: Number, attribute: 'number-value' })
  numberValue!: number;

  @property({ type: Boolean, attribute: 'bool-value' })
  boolValue = false;

  render() {
    return html` <h1>${this.numberValue.toFixed(4)}</h1>
      <h1>${this.boolValue}</h1>`;
  }

  static styles = css``;
}

外部 HTML 控制

<hello-world number-value="50" bool-value></hello-world>

外部 JS 操控

document.querySelector('hello-world')!.numberValue = 50;
document.querySelector('hello-world')!.boolValue = false;

Dispatch Event

@customElement('hello-world')
export class HelloWorldElement extends LitElement {
  private handleClick() {
    this.dispatchEvent(new CustomEvent('clickhelloworld', { bubbles: true }));
  }

  render() {
    return html`<h1 @click="${this.handleClick}">Hello World</h1>`;
  }

  static styles = css``;
}

外部監聽

document.body.addEventListener('clickhelloworld', e => {
  console.log('clicked', e.target); // <hello-world>
});

注:組件 this.dispatchEvent 的這個 this 只的是 <hello-world> 這個 element。所以 event 不需要設置 composed

 

用 Lit 重寫 Counter Component

在 DOM – Web Components 文章的結尾,我寫了一個 Counter Component,我們現在用 Lit 重寫一遍。

最終效果是這樣

counter.ts

import { LitElement, css, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';

@customElement('counter-component')
export class HTMLCounterElement extends LitElement {
  @property({ type: Number })
  step = 1;

  @state()
  private number = 0;

  private minus() {
    this.number -= this.step;
  }

  private plus() {
    this.number += this.step;
  }

  render() {
    return html`
      <div class="counter">
        <button class="minus" @click="${this.minus}">-</button>
        <span class="number">${this.number}</span>
        <button class="plus" @click="${this.plus}">+</button>
      </div>
    `;
  }

  static styles = css`
    .counter {
      display: flex;
      gap: 16px;
    }
    .counter :is(.minus, .plus) {
      width: 64px;
      height: 64px;
    }
    .counter .number {
      width: 128px;
      height: 64px;
      border: 1px solid gray;
      font-size: 36px;
      display: grid;
      place-items: center;
    }
  `;
}

declare global {
  interface HTMLElementTagNameMap {
    'counter-component': HTMLCounterElement;
  }
}

index.html

<counter-component step="10"></counter-component>

 

Lit 的侷限

我們拿 material-web MWC 和 material-components-web MDC 做對比。

MDC 是傳統手法,先有 HTML、CSS,然後 JS 做 binding。

MWC 是 Web Components 手法,沒有 HTML、CSS,一切都是 JS 生成的。

結論就是 Lit 的渲染依賴於 JS。

如果我們用 Lit 開發企業網站,需要 SEO,那就需要搞服務端渲染 Server-side rendering。但這個目前還在 experimental 階段。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章