Lit cho nhà phát triển React

1. Giới thiệu

Lit là gì

Lit là một thư viện đơn giản để tạo các thành phần web nhẹ và nhanh, hoạt động trong mọi khung hoặc không có khung. Với Lit, bạn có thể tạo các thành phần, ứng dụng, hệ thống thiết kế, v.v. có thể chia sẻ.

Kiến thức bạn sẽ học được

Cách chuyển đổi một số khái niệm về React sang Lit, chẳng hạn như:

  • JSX và Tạo mẫu
  • Thành phần và Đồ hoá trang
  • Tiểu bang và Vòng đời
  • Móc
  • Thiếu nhi
  • Tham chiếu
  • Trạng thái dàn xếp

Sản phẩm bạn sẽ tạo ra

Vào cuối lớp học lập trình này, bạn sẽ có thể chuyển đổi các khái niệm thành phần React thành các thành phần tương tự Lit (Lit).

Bạn cần có

2. Ánh sáng so với phản ứng

Các khái niệm và khả năng cốt lõi của Lit giống với các khái niệm và khả năng của React theo nhiều cách, nhưng Lit cũng có một số điểm khác biệt chính và khác biệt:

Quán nhỏ

Lit rất nhỏ: nó có kích thước khoảng 5kb được rút gọn và được nén so với React + DOM's 40+ kb.

Biểu đồ thanh về kích thước gói được giảm kích thước và được nén tính bằng kb. Thanh Lit là 5kb và React + React DOM là 42,2kb

Nhanh chóng

Trong các điểm chuẩn công khai so sánh hệ thống tạo mẫu của Lit, lit-html, với VDOM của React, lit-html xuất hiện nhanh hơn từ 8 đến 10% so với React trong trường hợp xấu nhất và nhanh hơn 50% trong các trường hợp sử dụng phổ biến hơn.

LitElement (lớp cơ sở thành phần của Lit) tăng mức hao tổn tối thiểu đối với lit-html, nhưng tăng hiệu suất của React khoảng 16-30% khi so sánh các tính năng của thành phần như mức sử dụng bộ nhớ và tương tác và thời gian khởi động.

biểu đồ thanh được phân theo nhóm về hiệu suất so sánh giữa chế độ sáng và React (tính theo mili giây) (càng thấp thì càng tốt)

Không cần bản dựng

Với các tính năng mới của trình duyệt như mô-đun ES và giá trị cố định mẫu được gắn thẻ, Lit không yêu cầu biên dịch để chạy. Điều này có nghĩa là bạn có thể thiết lập môi trường nhà phát triển bằng thẻ tập lệnh + trình duyệt + máy chủ, đồng thời thiết lập và chạy.

Với các mô-đun ES và CDN hiện đại như Skypack hoặc UNPKG, thậm chí có thể bạn sẽ không cần tôim để bắt đầu!

Tuy nhiên, nếu muốn, bạn vẫn có thể tạo và tối ưu hoá mã Lit. Việc hợp nhất các mô-đun ES gốc gần đây của nhà phát triển rất hiệu quả cho Lit – Lit chỉ là JavaScript thông thường và không cần CLI hoặc xử lý bản dựng dành riêng cho khung.

Không phụ thuộc vào khung

Các thành phần của Lit được xây dựng từ một bộ tiêu chuẩn web có tên là Thành phần web. Điều này có nghĩa là việc tạo một thành phần trong Lit sẽ hoạt động trong các khung hiện tại và trong tương lai. Nếu hệ thống hỗ trợ các phần tử HTML, thì API này cũng hỗ trợ các Thành phần web.

Vấn đề duy nhất với khả năng tương tác của khung là khi khung có hỗ trợ hạn chế cho DOM. React là một trong những khung này, nhưng nó cho phép thoát thông qua Refs và Refs trong React không phải là một trải nghiệm tốt cho nhà phát triển.

Nhóm Lit đang thực hiện một dự án thử nghiệm có tên là @lit-labs/react. Dự án này sẽ tự động phân tích cú pháp các thành phần Lit của bạn và tạo một trình bao bọc React để bạn không phải sử dụng các ref.

Ngoài ra, Phần tử tuỳ chỉnh ở mọi nơi sẽ cho bạn biết khung và thư viện nào hoạt động tốt với các phần tử tuỳ chỉnh!

Hỗ trợ TypeScript hạng nhất

Mặc dù có thể viết tất cả mã Lit trong JavaScript, nhưng Lit được viết bằng TypeScript và nhóm Lit cũng khuyên các nhà phát triển nên sử dụng TypeScript!

Nhóm Lit đã làm việc với cộng đồng Lit để giúp duy trì các dự án đưa tính năng kiểm tra kiểu chữ và trí tuệ nhân tạo (Intellisense) của TypeScript vào mẫu Lit trong cả thời gian phát triển và xây dựng bằng lit-analyzerlit-plugin.

Ảnh chụp màn hình IDE cho thấy hoạt động kiểm tra kiểu không phù hợp để đặt boolean được nêu thành một số

Ảnh chụp màn hình một IDE cho thấy các đề xuất về kiến thức

Công cụ dành cho nhà phát triển được tích hợp vào trình duyệt

Thành phần văn bản chỉ là phần tử HTML trong DOM. Điều này có nghĩa là để kiểm tra các thành phần, bạn không cần phải cài đặt bất kỳ công cụ hoặc tập lệnh nào cho trình duyệt của mình.

Bạn chỉ cần mở các công cụ cho nhà phát triển, chọn một phần tử và khám phá các thuộc tính hoặc trạng thái của phần tử đó.

hình ảnh các công cụ của Chrome cho nhà phát triển cho thấy giá trị trả về $0 <mwc-textfield>, $0.value trả về hello world, $0.outlined trả về giá trị true và {$0} cho thấy việc mở rộng thuộc tính

Công nghệ này được xây dựng với tính năng kết xuất phía máy chủ (SSR)

Lit 2 đã được xây dựng với sự hỗ trợ của SSR. Tại thời điểm viết lớp học lập trình này, nhóm Lit vẫn chưa phát hành các công cụ SSR ở dạng ổn định, nhưng nhóm Lit đã và đang triển khai các thành phần kết xuất phía máy chủ trên các sản phẩm của Google và đã thử nghiệm SSR trong các ứng dụng React. Nhóm Lit hy vọng sẽ sớm phát hành các công cụ này ra bên ngoài trên GitHub.

Trong thời gian chờ đợi, bạn có thể theo dõi tiến độ của nhóm Lit tại đây.

Cần có sự đồng ý thấp

Việc sử dụng Lit không đòi hỏi phải có cam kết đáng kể để sử dụng! Bạn có thể tạo các thành phần trong Lit và thêm các thành phần đó vào dự án hiện tại. Nếu không thích thì bạn không phải chuyển đổi toàn bộ ứng dụng cùng một lúc vì các thành phần web hoạt động trong các khung khác!

Bạn đã xây dựng toàn bộ ứng dụng trong Lit và muốn chuyển sang một ứng dụng khác? Sau đó, bạn có thể đặt ứng dụng Lit hiện tại vào trong khung mới và di chuyển nội dung mình muốn sang các thành phần của khung mới.

Ngoài ra, nhiều khung hiện đại hỗ trợ đầu ra trong các thành phần web. Điều đó có nghĩa là các khung này thường có thể vừa với phần tử Lit.

3. Thiết lập và khám phá Sân chơi

Có hai cách để thực hiện lớp học lập trình này:

  • Bạn có thể hoàn toàn làm việc này trực tuyến, trong trình duyệt
  • (Nâng cao) Bạn có thể thực hiện trên máy cục bộ bằng VS Code

Truy cập mã

Xuyên suốt lớp học lập trình này, sẽ có các đường liên kết đến sân chơi Lit như sau:

Playground là một hộp cát mã chạy hoàn toàn trong trình duyệt. Công cụ này có thể biên dịch và chạy các tệp TypeScript và JavaScript, đồng thời cũng có thể tự động phân giải các tệp nhập vào các mô-đun nút. ví dụ:

// before
import './my-file.js';
import 'lit';

// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';

Bạn có thể làm toàn bộ hướng dẫn trong sân chơi Lit, sử dụng các điểm kiểm tra này làm điểm xuất phát. Nếu đang sử dụng VS Code, bạn có thể sử dụng các điểm kiểm tra này để tải mã khởi động xuống cho bất kỳ bước nào, cũng như sử dụng chúng để kiểm tra công việc của bạn.

Khám phá giao diện người dùng sân chơi được chiếu sáng

Thanh tab bộ chọn tệp được gắn nhãn Phần 1, Phần chỉnh sửa mã là Phần 2, bản xem trước đầu ra là Phần 3 và nút tải lại xem trước là Phần 4

Ảnh chụp màn hình giao diện người dùng Lit Playground nêu bật các phần mà bạn sẽ sử dụng trong lớp học lập trình này.

  1. Bộ chọn tệp. Ghi chú nút dấu cộng...
  2. Trình chỉnh sửa tệp.
  3. Xem trước mã.
  4. Nút tải lại.
  5. Nút Tải xuống.

Thiết lập mã VS (Nâng cao)

Dưới đây là những lợi ích khi sử dụng chế độ thiết lập Mã VS này:

  • Kiểm tra loại mẫu
  • Thông tin mẫu và tự động hoàn thành

Nếu bạn đã cài đặt TLD, VS Code (với trình bổ trợ lit-plugin) và biết cách sử dụng môi trường đó, bạn có thể chỉ cần tải xuống và khởi động các dự án này bằng cách thực hiện như sau:

  • Nhấn vào nút tải xuống
  • Giải nén nội dung của tệp tar vào một thư mục
  • (Nếu TS) thiết lập tsconfig nhanh để xuất ra các mô-đun es và es2015+
  • Cài đặt máy chủ nhà phát triển có thể phân giải các thông số mô-đun trần (nhóm Lit khuyên dùng @web/dev-server)
  • Chạy máy chủ nhà phát triển và mở trình duyệt của bạn (nếu đang sử dụng @web/dev-server, bạn có thể dùng npx web-dev-server --node-resolve --watch --open)
    • Nếu bạn đang dùng ví dụ package.json, hãy sử dụng npm run dev

4. JSX và Tạo mẫu

Trong phần này, bạn sẽ tìm hiểu kiến thức cơ bản về việc tạo mẫu trong Lit.

JSX và Mẫu lit

JSX là một phần mở rộng cú pháp cho JavaScript, cho phép người dùng React dễ dàng viết mẫu bằng mã JavaScript của họ. Mẫu văn bản có mục đích tương tự: thể hiện giao diện người dùng của một thành phần dưới dạng một hàm của trạng thái.

Cú pháp cơ bản

Trong React, bạn sẽ kết xuất JSX hello world như sau:

import 'react';
import ReactDOM from 'react-dom';

const name = 'Josh Perez';
const element = (
  <>
    <h1>Hello, {name}</h1>
    <div>How are you?</div>
  </>
);

ReactDOM.render(
  element,
  mountNode
);

Trong ví dụ trên, có hai phần tử và một "tên" đi kèm biến. In Lit, bạn sẽ làm như sau:

import {html, render} from 'lit';

const name = 'Josh Perez';
const element = html`
  <h1>Hello, ${name}</h1>
  <div>How are you?</div>`;

render(
  element,
  mountNode
);

Lưu ý rằng các mẫu Lit không cần Mảnh phản ứng để nhóm nhiều phần tử trong các mẫu của nó.

Trong Lit, các mẫu được bao bọc bằng một mẫu được gắn thẻ LITeral html, đây chính là nơi Lit nhận được tên của nó!

Giá trị mẫu

Mẫu Lit có thể chấp nhận các mẫu Lit khác, được gọi là TemplateResult. Ví dụ: gói name trong thẻ in nghiêng (<i>) và gói mã đó bằng một giá trị cố định của mẫu được gắn thẻ N.B. Nhớ sử dụng ký tự dấu phẩy ngược (`) chứ không phải ký tự dấu nháy đơn (').

import {html, render} from 'lit';

const name = html`<i>Josh Perez</i>`;
const element = html`
  <h1>Hello, ${name}</h1>
  <div>How are you?</div>`;

render(
  element,
  mountNode
);

Văn bản TemplateResult có thể chấp nhận các mảng, chuỗi, các TemplateResult khác cũng như lệnh.

Để thực hiện một bài tập, hãy thử chuyển đổi đoạn mã React sau đây sang Lit:

const itemsToBuy = [
  <li>Bananas</li>,
  <li>oranges</li>,
  <li>apples</li>,
  <li>grapes</li>
];
const element = (
  <>
    <h1>Things to buy:</h1>
    <ol>
      {itemsToBuy}
    </ol>
  </>);

ReactDOM.render(
  element,
  mountNode
);

Đáp số:

import {html, render} from 'lit';

const itemsToBuy = [
  html`<li>Bananas</li>`,
  html`<li>oranges</li>`,
  html`<li>apples</li>`,
  html`<li>grapes</li>`
];
const element = html`
  <h1>Things to buy:</h1>
  <ol>
    ${itemsToBuy}
  </ol>`;

render(
  element,
  mountNode
);

Đạo cụ chuyền bóng và bố trí dụng cụ

Một trong những khác biệt lớn nhất giữa cú pháp của JSX và Lit là cú pháp liên kết dữ liệu. Ví dụ: lấy đầu vào React này cùng với các liên kết:

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
  <input
      disabled={disabled}
      className={`static-class ${myClass}`}
      defaultValue={value}/>;

ReactDOM.render(
  element,
  mountNode
);

Trong ví dụ trên, dữ liệu đầu vào được xác định, thực hiện như sau:

  • Đặt thành một biến đã xác định bị vô hiệu hoá (false trong trường hợp này)
  • Đặt lớp thành static-class kèm theo một biến (trong trường hợp này là "static-class my-class")
  • Đặt giá trị mặc định

In Lit, bạn sẽ làm như sau:

import {html, render} from 'lit';

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
  <input
      ?disabled=${disabled}
      class="static-class ${myClass}"
      .value=${value}>`;

render(
  element,
  mountNode
);

Trong ví dụ về Lit, một liên kết boolean được thêm vào để bật/tắt thuộc tính disabled.

Tiếp theo, có một liên kết trực tiếp đến thuộc tính class thay vì className. Bạn có thể thêm nhiều liên kết vào thuộc tính class, trừ phi bạn đang dùng lệnh classMap. Đây là một trình trợ giúp khai báo để bật/tắt các lớp.

Cuối cùng, thuộc tính value được đặt cho dữ liệu đầu vào. Không giống như trong React, thao tác này sẽ không đặt phần tử đầu vào thành chỉ đọc vì nó tuân theo cách triển khai và hành vi gốc của đầu vào.

Cú pháp liên kết tuyên bố văn bản

html`<my-element ?attribute-name=${booleanVar}>`;
  • Tiền tố ? là cú pháp liên kết để bật/tắt một thuộc tính trên một phần tử
  • Tương đương với inputRef.toggleAttribute('attribute-name', booleanVar)
  • Hữu ích cho các phần tử sử dụng disableddisabled="false" vẫn được DOM đọc là true vì inputElement.hasAttribute('disabled') === true
html`<my-element .property-name=${anyVar}>`;
  • Tiền tố . là cú pháp liên kết để đặt thuộc tính của một phần tử
  • Tương đương với inputRef.propertyName = anyVar
  • Phù hợp để truyền dữ liệu phức tạp như đối tượng, mảng hoặc lớp
html`<my-element attribute-name=${stringVar}>`;
  • Liên kết với thuộc tính của phần tử
  • Tương đương với inputRef.setAttribute('attribute-name', stringVar)
  • Phù hợp với các giá trị cơ bản, bộ chọn quy tắc kiểu và querySelector

Trình xử lý truyền

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
  <input
      onClick={() => console.log('click')}
      onChange={e => console.log(e.target.value)} />;

ReactDOM.render(
  element,
  mountNode
);

Trong ví dụ trên, dữ liệu đầu vào được xác định, thực hiện như sau:

  • Ghi nhật ký từ "nhấp chuột" khi người dùng nhấp vào thông tin đầu vào
  • Ghi lại giá trị của dữ liệu đầu vào khi người dùng nhập một ký tự

In Lit, bạn sẽ làm như sau:

import {html, render} from 'lit';

const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
  <input
      @click=${() => console.log('click')}
      @input=${e => console.log(e.target.value)}>`;

render(
  element,
  mountNode
);

Trong ví dụ về Lit, có một trình nghe được thêm vào sự kiện click bằng @click.

Tiếp theo, thay vì sử dụng onChange, sẽ có một liên kết với sự kiện input gốc của <input> vì sự kiện change gốc chỉ kích hoạt trên blur (Phản hồi thông tin tóm tắt trên các sự kiện này).

Cú pháp trình xử lý sự kiện phát trực tiếp

html`<my-element @event-name=${() => {...}}></my-element>`;
  • Tiền tố @ là cú pháp liên kết cho trình nghe sự kiện
  • Tương đương với inputRef.addEventListener('event-name', ...)
  • Sử dụng tên sự kiện DOM gốc

5. Thành phần và Đồ hoá trang

Trong phần này, bạn sẽ tìm hiểu về các thành phần và hàm của lớp Lit. Trạng thái và Hook sẽ được trình bày chi tiết hơn ở các phần sau.

Thành phần và lớp LitElement

Thành phần Lit tương đương với một thành phần của lớp React là LitElement và khái niệm của Lit về "thuộc tính phản ứng" là sự kết hợp giữa đạo cụ và trạng thái của React. Ví dụ:

import React from 'react';
import ReactDOM from 'react-dom';

class Welcome extends React.Component {
  constructor(props) {
    super(props);
    this.state = {name: ''};
  }

  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

const element = <Welcome name="Elliott"/>
ReactDOM.render(
  element,
  mountNode
);

Trong ví dụ trên, có một thành phần React:

  • Hiển thị một name
  • Đặt giá trị mặc định của name thành chuỗi trống ("")
  • Chỉ định lại name cho "Elliott"

Đây là cách bạn sẽ thực hiện việc này trong LitElement

Trong TypeScript:

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

@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  @property({type: String})
  name = '';

  render() {
    return html`<h1>Hello, ${this.name}</h1>`
  }
}

Trong JavaScript:

import {LitElement, html} from 'lit';

class WelcomeBanner extends LitElement {
  static get properties() {
    return {
      name: {type: String}
    }
  }

  constructor() {
    super();
    this.name = '';
  }

  render() {
    return html`<h1>Hello, ${this.name}</h1>`
  }
}

customElements.define('welcome-banner', WelcomeBanner);

Và trong tệp HTML:

<!-- index.html -->
<head>
  <script type="module" src="./index.js"></script>
</head>
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>

Đánh giá về những gì đang xảy ra trong ví dụ ở trên:

@property({type: String})
name = '';
  • Xác định thuộc tính phản ứng công khai – một phần trong API công khai của thành phần
  • Hiển thị một thuộc tính (theo mặc định) cũng như một thuộc tính trên thành phần của bạn
  • Xác định cách chuyển đổi thuộc tính của thành phần (là các chuỗi) thành một giá trị
static get properties() {
  return {
    name: {type: String}
  }
}
  • Thư viện này có chức năng tương tự như trình trang trí TS @property nhưng chạy vốn trong JavaScript
render() {
  return html`<h1>Hello, ${this.name}</h1>`
}
  • Hàm này được gọi bất cứ khi nào thuộc tính phản ứng bị thay đổi
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • Thao tác này liên kết tên thẻ Phần tử HTML với định nghĩa lớp
  • Theo tiêu chuẩn Phần tử tuỳ chỉnh, tên thẻ phải chứa dấu gạch nối (-)
  • this trong LitElement là bản sao của phần tử tuỳ chỉnh (trong trường hợp này là <welcome-banner>)
customElements.define('welcome-banner', WelcomeBanner);
  • Đây là JavaScript tương đương với trình trang trí @customElement TS
<head>
  <script type="module" src="./index.js"></script>
</head>
  • Nhập định nghĩa phần tử tuỳ chỉnh
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>
  • Thêm phần tử tuỳ chỉnh vào trang
  • Đặt thuộc tính name thành 'Elliott'

Thành phần hàm

Lit không diễn giải 1:1 về thành phần hàm vì không sử dụng JSX hoặc bộ tiền xử lý. Tuy nhiên, việc soạn một hàm nhận thuộc tính và kết xuất DOM dựa trên các thuộc tính đó khá đơn giản. Ví dụ:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Elliott"/>
ReactDOM.render(
  element,
  mountNode
);

Lit, văn bản này sẽ là:

import {html, render} from 'lit';

function Welcome(props) {
  return html`<h1>Hello, ${props.name}</h1>`;
}

render(
  Welcome({name: 'Elliott'}),
  document.body.querySelector('#root')
);

6. Tiểu bang và Vòng đời

Trong phần này, bạn sẽ tìm hiểu về trạng thái và vòng đời của Lit.

Tiểu bang

Khái niệm "Thuộc tính phản ứng" của Lit là sự kết hợp giữa trạng thái và đạo cụ của React. Thuộc tính phản ứng khi bị thay đổi có thể kích hoạt vòng đời thành phần. Thuộc tính phản ứng có 2 biến thể:

Thuộc tính phản ứng công khai

// React
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props)
    this.state = {name: 'there'}
  }

  componentWillReceiveProps(nextProps) {
    if (this.props.name !== nextProps.name) {
      this.setState({name: nextProps.name})
    }
  }
}

// Lit (TS)
import {LitElement} from 'lit';
import {property} from 'lit/decorators.js';

class MyEl extends LitElement {
  @property() name = 'there';
}
  • Do @property xác định
  • Tương tự như các thành phần và trạng thái của React nhưng có thể thay đổi
  • API công khai do người sử dụng thành phần truy cập và thiết lập

Trạng thái phản ứng nội bộ

// React
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props)
    this.state = {name: 'there'}
  }
}

// Lit (TS)
import {LitElement} from 'lit';
import {state} from 'lit/decorators.js';

class MyEl extends LitElement {
  @state() name = 'there';
}
  • Do @state xác định
  • Tương tự như trạng thái của React nhưng có thể thay đổi
  • Trạng thái nội bộ riêng tư thường được truy cập từ bên trong thành phần hoặc các lớp con

Vòng đời

Vòng đời Lit khá giống với vòng đời của React, nhưng có một số điểm khác biệt đáng chú ý.

constructor

// React (js)
import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
    this._privateProp = 'private';
  }
}

// Lit (ts)
class MyEl extends LitElement {
  @property({type: Number}) counter = 0;
  private _privateProp = 'private';
}

// Lit (js)
class MyEl extends LitElement {
  static get properties() {
    return { counter: {type: Number} }
  }
  constructor() {
    this.counter = 0;
    this._privateProp = 'private';
  }
}
  • Lượng lit tương đương cũng là constructor
  • Không cần phải chuyển bất cứ nội dung nào đến lệnh gọi cấp cao
  • Người gọi (không hoàn toàn dành cho tất cả mọi người):
    • document.createElement
    • document.innerHTML
    • new ComponentClass()
    • Nếu trang có tên thẻ chưa được nâng cấp và định nghĩa được tải và đăng ký bằng @customElement hoặc customElements.define
  • Có chức năng tương tự constructor của React

render

// React
render() {
  return <div>Hello World</div>
}

// Lit
render() {
  return html`<div>Hello World</div>`;
}
  • Lượng lit tương đương cũng là render
  • Có thể trả về bất kỳ kết quả có thể kết xuất nào, ví dụ: TemplateResult hoặc string, v.v.
  • Tương tự như React, render() phải là một hàm thuần tuý
  • Sẽ hiển thị cho bất kỳ nút nào createRenderRoot() trả về (ShadowRoot theo mặc định)

componentDidMount

componentDidMount tương tự như tổ hợp cả phương thức gọi lại trong vòng đời firstUpdatedconnectedCallback của Lit.

firstUpdated

import Chart from 'chart.js';

// React
componentDidMount() {
  this._chart = new Chart(this.chartElRef.current, {...});
}

// Lit
firstUpdated() {
  this._chart = new Chart(this.chartEl, {...});
}
  • Được gọi lần đầu tiên mẫu của thành phần được kết xuất vào thư mục gốc của thành phần
  • Sẽ chỉ được gọi nếu phần tử được kết nối, ví dụ: không được gọi qua document.createElement('my-component') cho đến khi nút đó được nối vào cây DOM
  • Đây là vị trí tốt để thực hiện thiết lập thành phần yêu cầu DOM được kết xuất bởi thành phần
  • Không giống như các thay đổi componentDidMount của React đối với thuộc tính phản ứng trong firstUpdated sẽ dẫn đến kết xuất lại, mặc dù trình duyệt thường sẽ đưa các thay đổi vào cùng một khung. Nếu những thay đổi đó không yêu cầu quyền truy cập vào DOM của thư mục gốc, thì chúng thường nên được đưa vào willUpdate

connectedCallback

// React
componentDidMount() {
  this.window.addEventListener('resize', this.boundOnResize);
}

// Lit
connectedCallback() {
  super.connectedCallback();
  this.window.addEventListener('resize', this.boundOnResize);
}
  • Được gọi bất cứ khi nào phần tử tuỳ chỉnh được chèn vào cây DOM
  • Không giống như các thành phần React, khi các phần tử tuỳ chỉnh được tách khỏi DOM, chúng sẽ không bị huỷ bỏ và do đó có thể được "kết nối" nhiều lần
    • firstUpdated sẽ không được gọi lại
  • Hữu ích khi khởi động lại DOM hoặc đính kèm lại trình nghe sự kiện đã được dọn dẹp khi ngắt kết nối
  • Lưu ý: connectedCallback có thể được gọi trước firstUpdated. Vì vậy, trong lần gọi đầu tiên, DOM có thể không có sẵn

componentDidUpdate

// React
componentDidUpdate(prevProps) {
  if (this.props.title !== prevProps.title) {
    this._chart.setTitle(this.props.title);
  }
}

// Lit (ts)
updated(prevProps: PropertyValues<this>) {
  if (prevProps.has('title')) {
    this._chart.setTitle(this.title);
  }
}
  • Văn bản tương đương là updated (sử dụng thì quá khứ trong tiếng Anh của "update")
  • Không giống như React, updated cũng được gọi trong lần kết xuất ban đầu
  • Có chức năng tương tự componentDidUpdate của React

componentWillUnmount

// React
componentWillUnmount() {
  this.window.removeEventListener('resize', this.boundOnResize);
}

// Lit
disconnectedCallback() {
  super.disconnectedCallback();
  this.window.removeEventListener('resize', this.boundOnResize);
}
  • Lít tương đương với disconnectedCallback
  • Không giống như các thành phần React, khi các phần tử tuỳ chỉnh được tách khỏi DOM, thành phần này sẽ không bị huỷ bỏ
  • Không giống như componentWillUnmount, disconnectedCallback được gọi sau khi phần tử bị xoá khỏi cây
  • DOM bên trong thư mục gốc vẫn được gắn với cây con của thư mục gốc
  • Hữu ích trong việc dọn dẹp trình nghe sự kiện và các tệp tham chiếu bị rò rỉ để trình duyệt có thể thu thập thành phần vào thùng rác

Bài tập

import React from 'react';
import ReactDOM from 'react-dom';

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

Trong ví dụ trên, có một đồng hồ đơn giản thực hiện những việc sau:

  • Kết xuất này hiện thông báo "Hello World! Đó là" sau đó hiện thời gian
  • Mỗi giây, đồng hồ sẽ cập nhật
  • Khi được tháo, mã đánh dấu sẽ xoá khoảng thời gian gọi kim đánh dấu nhịp độ khung hình

Trước tiên, hãy bắt đầu với phần khai báo lớp thành phần:

// Lit (TS)
// some imports here are imported in advance
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
}

// Lit (JS)
// `html` is imported in advance
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
}

customElements.define('lit-clock', LitClock);

Tiếp theo, hãy khởi chạy date và khai báo thuộc tính này là thuộc tính phản ứng nội bộ với @state vì người dùng thành phần này sẽ không thiết lập date trực tiếp.

// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
  @state() // declares internal reactive prop
  private date = new Date(); // initialization
}

// Lit (JS)
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
  static get properties() {
    return {
      // declares internal reactive prop
      date: {state: true}
    }
  }

  constructor() {
    super();
    // initialization
    this.date = new Date();
  }
}

customElements.define('lit-clock', LitClock);

Tiếp theo, hãy hiển thị mẫu.

// Lit (JS & TS)
render() {
  return html`
    <div>
      <h1>Hello, World!</h1>
      <h2>It is ${this.date.toLocaleTimeString()}.</h2>
    </div>
  `;
}

Bây giờ, hãy triển khai phương thức đánh dấu nhịp độ khung hình.

tick() {
  this.date = new Date();
}

Tiếp theo là việc triển khai componentDidMount. Xin nhắc lại, Lit tương tự là sự kết hợp của firstUpdatedconnectedCallback. Trong trường hợp của thành phần này, việc gọi tick bằng setInterval không yêu cầu quyền truy cập vào DOM bên trong thư mục gốc. Ngoài ra, khoảng thời gian sẽ bị xoá khi phần tử bị xoá khỏi cây tài liệu. Vì vậy, nếu được đính kèm lại, khoảng thời gian sẽ cần phải bắt đầu lại. Do đó, connectedCallback là lựa chọn tốt hơn ở đây.

// Lit (TS)
@customElement('lit-clock')
class LitClock extends LitElement {
  @state()
  private date = new Date();
  // initialize timerId for TS
  private timerId = -1 as unknown as ReturnType<typeof setTimeout>;

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  ...
}

// Lit (JS)
constructor() {
  super();
  // initialization
  this.date = new Date();
  this.timerId = -1; // initialize timerId for JS
}

connectedCallback() {
  super.connectedCallback();
  this.timerId = setInterval(
    () => this.tick(),
    1000
  );
}

Cuối cùng, hãy dọn dẹp khoảng thời gian để khoảng thời gian này không thực thi dấu đánh dấu sau khi phần tử bị ngắt kết nối khỏi cây tài liệu.

// Lit (TS & JS)
disconnectedCallback() {
  super.disconnectedCallback();
  clearInterval(this.timerId);
}

Kết hợp tất cả lại với nhau, mã sẽ có dạng như sau:

// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';

@customElement('lit-clock')
class LitClock extends LitElement {
  @state()
  private date = new Date();
  private timerId = -1 as unknown as ReturnType<typeof setTimeout>;

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  tick() {
    this.date = new Date();
  }

  render() {
    return html`
      <div>
        <h1>Hello, World!</h1>
        <h2>It is ${this.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    clearInterval(this.timerId);
  }
}

// Lit (JS)
import {LitElement, html} from 'lit';

class LitClock extends LitElement {
  static get properties() {
    return {
      date: {state: true}
    }
  }

  constructor() {
    super();
    this.date = new Date();
  }

  connectedCallback() {
    super.connectedCallback();
    this.timerId = setInterval(
      () => this.tick(),
      1000
    );
  }

  tick() {
    this.date = new Date();
  }

  render() {
    return html`
      <div>
        <h1>Hello, World!</h1>
        <h2>It is ${this.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    clearInterval(this.timerId);
  }
}

customElements.define('lit-clock', LitClock);

7. Móc

Trong phần này, bạn sẽ tìm hiểu cách dịch các khái niệm của React Hook sang Lit.

Khái niệm về hook của React

Các hook phản ứng giúp các thành phần hàm "hook" trạng thái. Việc này mang lại một số lợi ích.

  • Chúng đơn giản hoá việc sử dụng lại logic có trạng thái
  • Giúp chia một thành phần thành các hàm nhỏ hơn

Ngoài ra, trọng tâm vào các thành phần dựa trên hàm giải quyết một số vấn đề nhất định với cú pháp dựa trên lớp của React, chẳng hạn như:

  • Phải vượt props từ constructor đến super
  • Khởi chạy thuộc tính không gọn gàng trong constructor
    • Đây là lý do mà nhóm React đưa ra vào thời điểm đó nhưng đã được giải quyết bằng ES2019
  • Các vấn đề do this không còn tham chiếu đến thành phần này nữa

Các khái niệm về phản ứng của hook trong Lit

Như đã đề cập trong phần Thành phần & Trong phần Props, Lit không đưa ra cách tạo phần tử tuỳ chỉnh từ một hàm, nhưng LitElement giải quyết được hầu hết các vấn đề chính liên quan đến các thành phần của lớp React. Ví dụ:

// React (at the time of making hooks)
import React from 'react';
import ReactDOM from 'react-dom';

class MyEl extends React.Component {
  constructor(props) {
    super(props); // Leaky implementation
    this.state = {count: 0};
    this._chart = null; // Deemed messy
  }

  render() {
    return (
      <>
        <div>Num times clicked {count}</div>
        <button onClick={this.clickCallback}>click me</button>
      </>
    );
  }

  clickCallback() {
    // Errors because `this` no longer refers to the component
    this.setState({count: this.count + 1});
  }
}

// Lit (ts)
class MyEl extends LitElement {
  @property({type: Number}) count = 0; // No need for constructor to set state
  private _chart = null; // Public class fields introduced to JS in 2019

  render() {
    return html`
        <div>Num times clicked ${count}</div>
        <button @click=${this.clickCallback}>click me</button>`;
  }

  private clickCallback() {
    // No error because `this` refers to component
    this.count++;
  }
}

Lit giải quyết các vấn đề này như thế nào?

  • constructor không nhận đối số
  • Tất cả các liên kết @event đều tự động liên kết với this
  • this trong hầu hết các trường hợp đề cập đến tham chiếu của phần tử tùy chỉnh
  • Giờ đây, bạn có thể tạo thực thể cho các thuộc tính của lớp dưới dạng thành phần của lớp. Thao tác này dọn dẹp các phương thức triển khai dựa trên hàm khởi tạo

Bộ điều khiển phản ứng

Các khái niệm chính đằng sau Hooks tồn tại trong Lit dưới dạng bộ điều khiển phản ứng. Các mẫu bộ điều khiển phản ứng cho phép chia sẻ logic trạng thái, chia các thành phần thành các bit nhỏ hơn, nhiều mô-đun hơn, cũng như nối vào vòng đời cập nhật của một phần tử.

Bộ điều khiển phản ứng là giao diện đối tượng có thể kết nối với vòng đời cập nhật của một máy chủ điều khiển như LitElement.

Vòng đời của ReactiveControllerreactiveControllerHost là:

interface ReactiveController {
  hostConnected(): void;
  hostUpdate(): void;
  hostUpdated(): void;
  hostDisconnected(): void;
}
interface ReactiveControllerHost {
  addController(controller: ReactiveController): void;
  removeController(controller: ReactiveController): void;
  requestUpdate(): void;
  readonly updateComplete: Promise<boolean>;
}

Bằng cách xây dựng một trình điều khiển phản ứng và đính kèm trình điều khiển đó vào một máy chủ bằng addController, vòng đời của trình điều khiển này sẽ được gọi cùng với vòng đời của máy chủ. Ví dụ: hãy xem lại ví dụ về đồng hồ trong phần Trạng thái & Mục Vòng đời:

import React from 'react';
import ReactDOM from 'react-dom';

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

Trong ví dụ trên, có một đồng hồ đơn giản thực hiện những việc sau:

  • Kết xuất này hiện thông báo "Hello World! Đó là" sau đó hiện thời gian
  • Mỗi giây, đồng hồ sẽ cập nhật
  • Khi được tháo, mã đánh dấu sẽ xoá khoảng thời gian gọi kim đánh dấu nhịp độ khung hình

Xây dựng giàn giáo thành phần

Trước tiên, hãy bắt đầu bằng việc khai báo lớp thành phần và thêm hàm render.

// Lit (TS) - index.ts
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';

@customElement('my-element')
class MyElement extends LitElement {
  render() {
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${'time to get Lit'}.</h2>
      </div>
    `;
  }
}

// Lit (JS) - index.js
import {LitElement, html} from 'lit';

class MyElement extends LitElement {
  render() {
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${'time to get Lit'}.</h2>
      </div>
    `;
  }
}

customElements.define('my-element', MyElement);

Xây dựng bộ điều khiển

Bây giờ, hãy chuyển sang clock.ts và tạo một lớp cho ClockController rồi thiết lập constructor:

// Lit (TS) - clock.ts
import {ReactiveController, ReactiveControllerHost} from 'lit';

export class ClockController implements ReactiveController {
  private readonly host: ReactiveControllerHost;

  constructor(host: ReactiveControllerHost) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
  }

  private tick() {
  }

  hostDisconnected() {
  }
}

// Lit (JS) - clock.js
export class ClockController {
  constructor(host) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
  }

  tick() {
  }

  hostDisconnected() {
  }
}

Bạn có thể xây dựng bộ điều khiển phản ứng theo bất kỳ cách nào miễn là có chung giao diện ReactiveController, nhưng việc sử dụng một lớp có constructor có thể lấy giao diện ReactiveControllerHost cũng như mọi thuộc tính cần thiết khác để khởi tạo bộ điều khiển sẽ là mẫu mà nhóm Lit thích sử dụng cho hầu hết các trường hợp cơ bản.

Bây giờ, bạn cần dịch các phương thức gọi lại trong vòng đời của React sang các phương thức gọi lại của bộ điều khiển. Nói ngắn gọn:

  • componentDidMount
    • Đến connectedCallback của LitElement
    • Đến hostConnected của bộ điều khiển
  • ComponentWillUnmount
    • Đến disconnectedCallback của LitElement
    • Đến hostDisconnected của bộ điều khiển

Để biết thêm thông tin về cách dịch vòng đời của React sang vòng đời Lit, hãy xem phần Trạng thái và Vòng đời.

Tiếp theo, hãy triển khai lệnh gọi lại hostConnected và các phương thức tick, đồng thời dọn dẹp khoảng thời gian trong hostDisconnected như đã thực hiện trong ví dụ ở phần Trạng thái & Vòng đời.

// Lit (TS) - clock.ts
export class ClockController implements ReactiveController {
  private readonly host: ReactiveControllerHost;
  private interval = 0 as unknown as ReturnType<typeof setTimeout>;
  date = new Date();

  constructor(host: ReactiveControllerHost) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
    this.interval = setInterval(() => this.tick(), 1000);
  }

  private tick() {
    this.date = new Date();
  }

  hostDisconnected() {
    clearInterval(this.interval);
  }
}

// Lit (JS) - clock.js
export class ClockController {
  interval = 0;
  host;
  date = new Date();

  constructor(host) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
    this.interval = setInterval(() => this.tick(), 1000);
  }

  tick() {
    this.date = new Date();
  }

  hostDisconnected() {
    clearInterval(this.interval);
  }
}

Sử dụng bộ điều khiển

Để sử dụng trình điều khiển đồng hồ, hãy nhập bộ điều khiển và cập nhật thành phần trong index.ts hoặc index.js.

// Lit (TS) - index.ts
import {LitElement, html, ReactiveController, ReactiveControllerHost} from 'lit';
import {customElement} from 'lit/decorators.js';
import {ClockController} from './clock.js';

@customElement('my-element')
class MyElement extends LitElement {
  private readonly clock = new ClockController(this); // Instantiate

  render() {
    // Use controller
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }
}

// Lit (JS) - index.js
import {LitElement, html} from 'lit';
import {ClockController} from './clock.js';

class MyElement extends LitElement {
  clock = new ClockController(this); // Instantiate

  render() {
    // Use controller
    return html`
      <div>
        <h1>Hello, world!</h1>
        <h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
      </div>
    `;
  }
}

customElements.define('my-element', MyElement);

Để sử dụng tay điều khiển, bạn cần tạo thực thể cho tay điều khiển bằng cách truyền một tham chiếu đến máy chủ lưu trữ tay điều khiển (là thành phần <my-element>), sau đó sử dụng nó trong phương thức render.

Kích hoạt kết xuất lại trong bộ điều khiển

Lưu ý rằng ứng dụng sẽ hiển thị thời gian, nhưng thời gian thì không được cập nhật. Lý do là đơn vị điều khiển đang đặt ngày mỗi giây, nhưng máy chủ không cập nhật. Nguyên nhân là do date đang thay đổi trên lớp ClockController và không còn thay đổi trên thành phần này nữa. Tức là sau khi đặt date trên tay điều khiển, máy chủ lưu trữ cần được yêu cầu chạy vòng đời cập nhật bằng host.requestUpdate().

// Lit (TS & JS) - clock.ts / clock.js
private tick() {
  this.date = new Date();
  this.host.requestUpdate();
}

Lúc này, đồng hồ chắc hẳn đang kêu tích tắc!

Để so sánh chi tiết hơn về các trường hợp sử dụng phổ biến với nội dung hấp dẫn, vui lòng xem phần Chủ đề nâng cao – Móc.

8. Thiếu nhi

Trong phần này, bạn sẽ tìm hiểu cách sử dụng khung giờ để quản lý phần tử con trong Lit.

Máy đánh bạc và Con

Các ô cho phép kết hợp bằng cách cho phép bạn lồng các thành phần.

Trong React, trẻ được kế thừa thông qua các đạo cụ. Ô mặc định là props.children và hàm render xác định vị trí của ô mặc định. Ví dụ:

const MyArticle = (props) => {
 return <article>{props.children}</article>;
};

Lưu ý props.children là Thành phần phản ứng chứ không phải là phần tử HTML.

Trong Lit, các thành phần con được bao gồm trong hàm kết xuất với các phần tử ô. Lưu ý rằng thành phần con không được kế thừa theo cách tương tự như React. Ở chế độ Lit, phần tử con là HTMLElements được đính kèm vào ô. Tệp đính kèm này được gọi là Phép chiếu (Projection).

@customElement("my-article")
export class MyArticle extends LitElement {
  render() {
    return html`
      <article>
        <slot></slot>
      </article>
   `;
  }
}

Nhiều khung giờ

Trong React, việc thêm nhiều ô về cơ bản cũng giống như việc kế thừa nhiều tài sản hơn.

const MyArticle = (props) => {
  return (
    <article>
      <header>
        {props.headerChildren}
      </header>
      <section>
        {props.sectionChildren}
      </section>
    </article>
  );
};

Tương tự như vậy, việc thêm nhiều phần tử <slot> sẽ tạo ra nhiều ô hơn trong Lit. Nhiều ô được xác định bằng thuộc tính name: <slot name="slot-name">. Việc này cho phép nhà xuất bản con khai báo khung giờ mà trẻ sẽ được chỉ định.

@customElement("my-article")
export class MyArticle extends LitElement {
  render() {
    return html`
      <article>
        <header>
          <slot name="headerChildren"></slot>
        </header>
        <section>
          <slot name="sectionChildren"></slot>
        </section>
      </article>
   `;
  }
}

Nội dung vị trí mặc định

Vùng quảng cáo sẽ hiển thị cây con khi không có nút nào được chiếu tới vùng đó. Khi các nút được chiếu lên một vị trí, vị trí đó sẽ không hiển thị cây con và thay vào đó sẽ hiển thị các nút được chiếu.

@customElement("my-element")
export class MyElement extends LitElement {
  render() {
    return html`
      <section>
        <div>
          <slot name="slotWithDefault">
            <p>
             This message will not be rendered when children are attached to this slot!
            <p>
          </slot>
        </div>
      </section>
   `;
  }
}

Chỉ định trẻ vào các khung giờ

Trong React, thành phần con được gán cho các ô thông qua các thuộc tính của một Thành phần. Trong ví dụ bên dưới, các phần tử React được truyền đến các thuộc tính headerChildrensectionChildren.

const MyNewsArticle = () => {
 return (
   <MyArticle
     headerChildren={<h3>Extry, Extry! Read all about it!</h3>}
     sectionChildren={<p>Children are props in React!</p>}
   />
 );
};

Trong Lit, phần tử con được chỉ định cho các ô bằng thuộc tính slot.

@customElement("my-news-article")
export class MyNewsArticle extends LitElement {
  render() {
    return html`
      <my-article>
        <h3 slot="headerChildren">
          Extry, Extry! Read all about it!
        </h3>
        <p slot="sectionChildren">
          Children are composed with slots in Lit!
        </p>
      </my-article>
   `;
  }
}

Nếu không có vùng mặc định (ví dụ: <slot>) và không có vùng nào có thuộc tính name (ví dụ: <slot name="foo">) khớp với thuộc tính slot của phần tử con của phần tử tuỳ chỉnh (ví dụ: <div slot="foo">), thì nút đó sẽ không được chiếu và không xuất hiện.

9. Tham chiếu

Đôi khi, nhà phát triển có thể cần phải truy cập vào API của một HTMLElement.

Trong phần này, bạn sẽ tìm hiểu cách lấy thông tin tham chiếu phần tử trong Lit.

Tham chiếu phản ứng

Thành phần React được chuyển đổi thành một loạt lệnh gọi hàm để tạo DOM ảo khi được gọi. DOM ảo này được diễn giải bằng ReactDOM và hiển thị HTMLElements.

Trong React, Tham chiếu là không gian trong bộ nhớ để chứa một HTMLElement được tạo.

const RefsExample = (props) => {
 const inputRef = React.useRef(null);
 const onButtonClick = React.useCallback(() => {
   inputRef.current?.focus();
 }, [inputRef]);

 return (
   <div>
     <input type={"text"} ref={inputRef} />
     <br />
     <button onClick={onButtonClick}>
       Click to focus on the input above!
     </button>
   </div>
 );
};

Trong ví dụ trên, thành phần React sẽ thực hiện những việc sau:

  • Kết xuất phần nhập văn bản trống và một nút có văn bản
  • Lấy tiêu điểm nhập khi nhấp vào nút này

Sau lần kết xuất ban đầu, React sẽ đặt inputRef.current thành HTMLInputElement được tạo thông qua thuộc tính ref.

Phát ngôn "Tài liệu tham khảo" với @query

Lit hoạt động gần trình duyệt và tạo ra một bản tóm tắt rất nhỏ so với các tính năng gốc của trình duyệt.

React tương đương với refs trong Lit là HTMLElement do trình trang trí @query@queryAll trả về.

@customElement("my-element")
export class MyElement extends LitElement {
  @query('input') // Define the query
  inputEl!: HTMLInputElement; // Declare the prop

  // Declare the click event listener
  onButtonClick() {
    // Use the query to focus
    this.inputEl.focus();
  }

  render() {
    return html`
      <input type="text">
      <br />
      <!-- Bind the click listener -->
      <button @click=${this.onButtonClick}>
        Click to focus on the input above!
      </button>
   `;
  }
}

Trong ví dụ trên, thành phần Lit thực hiện những việc sau:

  • Xác định một thuộc tính trên MyElement bằng cách sử dụng trình trang trí @query (tạo phương thức getter cho HTMLInputElement).
  • Khai báo và đính kèm một lệnh gọi lại sự kiện nhấp chuột có tên là onButtonClick.
  • Tập trung nội dung nhập vào khi nhấp vào nút

Trong JavaScript, trình trang trí @query@queryAll lần lượt thực hiện querySelectorquerySelectorAll. Đây là JavaScript tương đương với @query('input') inputEl!: HTMLInputElement;

get inputEl() {
  return this.renderRoot.querySelector('input');
}

Sau khi thành phần Lit xác nhận mẫu của phương thức render vào gốc của my-element, trình trang trí @query sẽ cho phép inputEl trả về phần tử input đầu tiên tìm thấy trong gốc kết xuất. Hàm này sẽ trả về null nếu @query không tìm thấy phần tử đã chỉ định.

Nếu có nhiều phần tử input trong gốc kết xuất, @queryAll sẽ trả về một danh sách các nút.

10. Trạng thái dàn xếp

Trong phần này, bạn sẽ tìm hiểu cách dàn xếp trạng thái giữa các thành phần trong Lit.

Các thành phần có thể tái sử dụng

Phản ứng bắt chước quy trình kết xuất chức năng với luồng dữ liệu từ trên xuống. Cha mẹ đưa ra trạng thái cho trẻ thông qua các đạo cụ. Trẻ giao tiếp với cha mẹ thông qua các lệnh gọi lại có trong đạo cụ.

const CounterButton = (props) => {
  const label = props.step < 0
    ? `- ${-1 * props.step}`
    : `+ ${props.step}`;


  return (
    <button
      onClick={() =>
        props.addToCounter(props.step)}>{label}</button>
  );
};

Trong ví dụ trên, thành phần React sẽ thực hiện những việc sau:

  • Tạo nhãn dựa trên giá trị props.step.
  • Hiển thị nút có nhãn +step hoặc -step
  • Cập nhật thành phần mẹ bằng cách gọi props.addToCounter với props.step làm đối số khi nhấp chuột

Mặc dù có thể truyền lệnh gọi lại trong Lit, nhưng các mẫu thông thường sẽ khác. Thành phần React trong ví dụ trên có thể được viết dưới dạng Thành phần Lit trong ví dụ bên dưới:

@customElement('counter-button')
export class CounterButton extends LitElement {
  @property({type: Number}) step: number = 0;

  onClick() {
    const event = new CustomEvent('update-counter', {
      bubbles: true,
      detail: {
        step: this.step,
      }
    });

    this.dispatchEvent(event);
  }

  render() {
    const label = this.step < 0
      ? `- ${-1 * this.step}`  // "- 1"
      : `+ ${this.step}`;      // "+ 1"

    return html`
      <button @click=${this.onClick}>${label}</button>
    `;
  }
}

Trong ví dụ trên, một Thành phần Lit sẽ hoạt động như sau:

  • Tạo thuộc tính phản ứng step
  • Điều phối một sự kiện tuỳ chỉnh có tên là update-counter mang giá trị step của phần tử khi nhấp vào

Các sự kiện trên trình duyệt xuất hiện từ phần tử con đến các phần tử mẹ. Sự kiện cho phép trẻ truyền phát các sự kiện tương tác và thay đổi trạng thái. Về cơ bản, phản ứng sẽ truyền trạng thái theo hướng ngược lại. Vì vậy, sẽ ít phổ biến khi thấy các Thành phần phản ứng được gửi đi và theo dõi các sự kiện theo cách tương tự như Thành phần Lit.

Thành phần có trạng thái

Trong React, bạn nên sử dụng hook để quản lý trạng thái. Bạn có thể tạo một Thành phần MyCounter bằng cách sử dụng lại Thành phần CounterButton. Hãy lưu ý cách truyền addToCounter đến cả hai thực thể của CounterButton.

const MyCounter = (props) => {
 const [counterSum, setCounterSum] = React.useState(0);
 const addToCounter = useCallback(
   (step) => {
     setCounterSum(counterSum + step);
   },
   [counterSum, setCounterSum]
 );

 return (
   <div>
     <h3>&Sigma;: {counterSum}</h3>
     <CounterButton
       step={-1}
       addToCounter={addToCounter} />
     <CounterButton
       step={1}
       addToCounter={addToCounter} />
   </div>
 );
};

Ví dụ trên thực hiện những việc sau:

  • Tạo trạng thái count.
  • Tạo lệnh gọi lại để thêm một số vào trạng thái count.
  • CounterButton sử dụng addToCounter để cập nhật count thêm step cho mỗi lượt nhấp.

Bạn có thể triển khai MyCounter tương tự trong Lit. Hãy lưu ý cách addToCounter không được truyền đến counter-button. Thay vào đó, lệnh gọi lại được liên kết dưới dạng trình nghe sự kiện với sự kiện @update-counter trên một phần tử mẹ.

@customElement("my-counter")
export class MyCounter extends LitElement {
  @property({type: Number}) count = 0;

  addToCounter(e: CustomEvent<{step: number}>) {
    // Get step from detail of event or via @query
    this.count += e.detail.step;
  }

  render() {
    return html`
      <div @update-counter="${this.addToCounter}">
        <h3>&Sigma; ${this.count}</h3>
        <counter-button step="-1"></counter-button>
        <counter-button step="1"></counter-button>
      </div>
    `;
  }
}

Ví dụ trên thực hiện những việc sau:

  • Tạo một thuộc tính phản ứng có tên là count. Thuộc tính này sẽ cập nhật thành phần khi giá trị thay đổi
  • Liên kết lệnh gọi lại addToCounter với trình nghe sự kiện @update-counter
  • Cập nhật count bằng cách thêm giá trị có trong detail.step của sự kiện update-counter
  • Đặt giá trị step của counter-button thông qua thuộc tính step

Cách thông thường hơn là sử dụng thuộc tính phản ứng trong Lit để thông báo về những thay đổi của cha mẹ sang con. Tương tự, bạn nên sử dụng hệ thống sự kiện của trình duyệt để tạo bong bóng thông tin chi tiết từ dưới lên.

Phương pháp này tuân theo các phương pháp hay nhất và tuân thủ mục tiêu của Lit là hỗ trợ các thành phần web trên nhiều nền tảng.

11. Định kiểu

Trong phần này, bạn sẽ tìm hiểu về cách tạo kiểu trong Lit.

Định kiểu

Tính năng Ánh sáng cung cấp nhiều cách tạo kiểu cho phần tử cũng như một giải pháp tích hợp sẵn.

Kiểu cùng dòng

Tính năng Lit hỗ trợ các kiểu cùng dòng cũng như liên kết với các kiểu đó.

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

@customElement('my-element')
class MyElement extends LitElement {
  render() {
    return html`
      <div>
        <h1 style="color:orange;">This text is orange</h1>
        <h1 style="color:rebeccapurple;">This text is rebeccapurple</h1>
      </div>
    `;
  }
}

Trong ví dụ trên, có 2 tiêu đề, mỗi tiêu đề có một kiểu cùng dòng.

Giờ thì hãy nhập và liên kết đường viền từ border-color.js với văn bản màu cam:

...
import borderColor from './border-color.js';

...

html`
  ...
  <h1 style="color:orange;${borderColor}">This text is orange</h1>
  ...`

Việc phải tính toán chuỗi kiểu mỗi lần có thể sẽ gây khó chịu một chút, vì vậy Lit đưa ra một lệnh để giúp bạn thực hiện việc này.

styleMap

Lệnh styleMap giúp bạn dễ dàng sử dụng JavaScript để đặt kiểu cùng dòng. Ví dụ:

import {LitElement, html, css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {styleMap} from 'lit/directives/style-map.js';

@customElement('my-element')
class MyElement extends LitElement {
  @property({type: String})
  color = '#000'

  render() {
    // Define the styleMap
    const headerStyle = styleMap({
      'border-color': this.color,
    });

    return html`
      <div>
        <h1
          style="border-style:solid;
          <!-- Use the styleMap -->
          border-width:2px;${headerStyle}">
          This div has a border color of ${this.color}
        </h1>
        <input
          type="color"
          @input=${e => (this.color = e.target.value)}
          value="#000">
      </div>
    `;
  }
}

Ví dụ trên thực hiện những việc sau:

  • Hiển thị h1 có đường viền và công cụ chọn màu
  • Thay đổi border-color thành giá trị từ công cụ chọn màu

Ngoài ra, còn có styleMap dùng để thiết lập kiểu của h1. styleMap tuân theo cú pháp tương tự như cú pháp liên kết thuộc tính style của React.

CSSResult

Bạn nên định kiểu cho các thành phần bằng cách sử dụng giá trị cố định trong mẫu được gắn thẻ css.

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

const ORANGE = css`orange`;

@customElement('my-element')
class MyElement extends LitElement {
  static styles = [
    css`
      #orange {
        color: ${ORANGE};
      }

      #purple {
        color: rebeccapurple;
      }
    `
  ];

  render() {
    return html`
      <div>
        <h1 id="orange">This text is orange</h1>
        <h1 id="purple">This text is rebeccapurple</h1>
      </div>
    `;
  }
}

Ví dụ trên thực hiện những việc sau:

  • Khai báo giá trị cố định của mẫu được gắn thẻ CSS kèm theo một liên kết
  • Đặt màu cho hai h1 bằng mã nhận dạng

Sau đây là các lợi ích khi sử dụng thẻ mẫu css:

  • Được phân tích cú pháp một lần cho mỗi lớp so với mỗi thực thể
  • Được triển khai chú trọng đến khả năng tái sử dụng của mô-đun
  • Có thể dễ dàng phân tách các kiểu thành các tệp riêng
  • Tương thích với polyfill Thuộc tính tuỳ chỉnh của CSS

Ngoài ra, hãy chú ý đến thẻ <style> trong index.html:

<!-- index.html -->
<style>
  h1 {
    color: red !important;
  }
</style>

Văn bản sẽ xác định phạm vi của các thành phần vào gốc của chúng. Tức là kiểu sẽ không bị rò rỉ cả ra lẫn vào bên trong. Để truyền kiểu từ các thành phần đến các thành phần, nhóm Lit khuyên bạn nên sử dụng Thuộc tính tuỳ chỉnh của CSS vì chúng có thể thâm nhập vào phạm vi của kiểu Lit.

Thẻ kiểu

Bạn cũng có thể chỉ cần đưa thẻ <style> vào cùng dòng trong mẫu của mình. Trình duyệt sẽ loại bỏ các thẻ kiểu trùng lặp này, nhưng bằng cách đặt các thẻ đó vào mẫu của bạn, các thẻ đó sẽ được phân tích cú pháp cho từng thực thể thành phần thay vì mỗi lớp, như trong trường hợp mẫu được gắn thẻ css. Ngoài ra, việc loại bỏ trùng lặp CSSResult trên trình duyệt nhanh hơn nhiều.

Việc sử dụng <link rel="stylesheet"> trong mẫu của bạn cũng là một khả năng cho các kiểu, nhưng điều này cũng không được khuyến khích vì nó có thể gây ra ánh sáng flash ban đầu của nội dung không được định kiểu (FOUC).

12. Chủ đề nâng cao (không bắt buộc)

JSX và Tạo mẫu

Ánh sáng & DOM ảo

Lit-html không bao gồm DOM ảo thông thường có sự khác biệt ở từng nút riêng lẻ. Thay vào đó, công cụ này sử dụng các tính năng hiệu suất hàm nội tại với thông số kỹ thuật giá trị cố định mẫu được gắn thẻ của ES2015. Hằng mẫu được gắn thẻ là các chuỗi cố định của mẫu có đính kèm các hàm thẻ.

Dưới đây là một ví dụ về giá trị cố định của mẫu:

const str = 'string';
console.log(`This is a template literal ${str}`);

Dưới đây là ví dụ về giá trị cố định của mẫu được gắn thẻ:

const tag = (strings, ...values) => ({strings, values});
const f = (x) => tag`hello ${x} how are you`;
console.log(f('world')); // {strings: ["hello ", " how are you"], values: ["world"]}
console.log(f('world').strings === f(1 + 2).strings); // true

Trong ví dụ trên, thẻ là hàm tag và hàm f trả về lệnh gọi một giá trị cố định mẫu được gắn thẻ.

Rất nhiều điều kỳ diệu về hiệu suất trong Lit bắt nguồn từ việc các mảng chuỗi được truyền vào hàm thẻ có cùng một con trỏ (như thể hiện trong console.log thứ hai). Trình duyệt không tạo lại một mảng strings mới trên mỗi lệnh gọi hàm thẻ vì trình duyệt này đang sử dụng cùng một giá trị cố định của mẫu (tức là ở cùng một vị trí trong AST). Vì vậy, các chức năng liên kết, phân tích cú pháp và lưu mẫu vào bộ nhớ đệm của Lit có thể tận dụng các tính năng này mà không gây ra nhiều mức hao tổn khác biệt trong thời gian chạy.

Hành vi tích hợp sẵn của trình duyệt của các giá trị cố định của mẫu được gắn thẻ mang lại cho Lit khá nhiều lợi thế về hiệu suất. Hầu hết các DOM ảo thông thường thực hiện phần lớn công việc của mình trong JavaScript. Tuy nhiên, giá trị cố định của mẫu được gắn thẻ thực hiện hầu hết những khác biệt trong C++ của trình duyệt.

Nếu bạn muốn bắt đầu sử dụng các giá trị cố định trong mẫu được gắn thẻ HTML bằng React hoặc Preact, thì nhóm Lit nên dùng thư viện htm.

Mặc dù, như trong trường hợp trang web của Lớp học lập trình của Google và một số trình soạn thảo mã trực tuyến, bạn sẽ nhận thấy rằng việc làm nổi bật cú pháp bằng giá trị của mẫu được gắn thẻ không phổ biến lắm. Một số IDE và trình chỉnh sửa văn bản hỗ trợ các định dạng này theo mặc định, chẳng hạn như Atom và trình đánh dấu khối mã của GitHub. Nhóm Lit cũng phối hợp rất chặt chẽ với cộng đồng để duy trì các dự án như lit-plugin – một trình bổ trợ VS Code giúp làm nổi bật cú pháp, kiểm tra loại và thông minh cho các dự án Lit của bạn.

Ánh sáng & JSX + DOM React

JSX không chạy trong trình duyệt mà thay vào đó sử dụng bộ tiền xử lý để chuyển đổi JSX thành các lệnh gọi hàm JavaScript (thường là qua Squarespace).

Ví dụ: iMessage sẽ biến đổi:

const element = <div className="title">Hello World!</div>;
ReactDOM.render(element, mountNode);

vào:

const element = React.createElement('div', {className: 'title'}, 'Hello World!');
ReactDOM.render(element, mountNode);

Sau đó, React DOM lấy đầu ra React và dịch sang DOM thực tế – thuộc tính, thuộc tính, trình nghe sự kiện và tất cả.

Lit-html sử dụng giá trị cố định của mẫu được gắn thẻ có thể chạy trong trình duyệt mà không cần dịch hoặc trình xử lý trước. Tức là để bắt đầu sử dụng Lit, bạn chỉ cần có tệp HTML, tập lệnh mô-đun ES và một máy chủ. Dưới đây là một tập lệnh hoàn toàn có thể chạy bằng trình duyệt:

<!DOCTYPE html>
<html>
  <head>
    <script type="module">
      import {html, render} from 'https://cdn.skypack.dev/lit';

      render(
        html`<div>Hello World!</div>`,
        document.querySelector('.root')
      )
    </script>
  </head>
  <body>
    <div class="root"></div>
  </body>
</html>

Ngoài ra, vì hệ thống tạo mẫu của Lit, lit-html, không sử dụng DOM ảo thông thường mà sử dụng trực tiếp DOM API, kích thước của Lit 2 dưới 5kb được giảm thiểu và được nén so với React (2,8kb) + 40kb rút gọn và giải nén của React-dom.

Sự kiện

React sử dụng một hệ thống sự kiện tổng hợp. Điều này có nghĩa là React-dom phải xác định mọi sự kiện sẽ được dùng trên mọi thành phần và cung cấp một trình nghe sự kiện camelCase tương đương cho từng loại nút. Do đó, JSX không có phương thức để xác định trình nghe sự kiện cho sự kiện tuỳ chỉnh và các nhà phát triển phải sử dụng ref, sau đó bắt buộc phải áp dụng một trình nghe. Điều này tạo ra trải nghiệm tương đương cho nhà phát triển khi tích hợp các thư viện không lưu ý đến React, dẫn đến việc phải viết một trình bao bọc dành riêng cho React.

Lit-html truy cập trực tiếp vào DOM và sử dụng các sự kiện gốc, vì vậy, bạn có thể thêm trình nghe sự kiện dễ dàng như @event-name=${eventNameListener}. Điều này có nghĩa là quá trình phân tích cú pháp trong thời gian chạy sẽ diễn ra ít hơn để thêm trình nghe sự kiện cũng như kích hoạt sự kiện.

Thành phần và Đồ hoá trang

Thành phần phản ứng & phần tử tuỳ chỉnh

Trong trường hợp này, LitElement sử dụng các phần tử tuỳ chỉnh để đóng gói các thành phần. Các phần tử tuỳ chỉnh gây ra một số điểm đánh đổi giữa các thành phần của React khi nói đến việc sắp xếp thành phần (trạng thái và vòng đời được thảo luận thêm trong phần Trạng thái và vòng đời).

Phần tử tuỳ chỉnh có một số ưu điểm như một hệ thống thành phần:

  • Dành cho trình duyệt và không yêu cầu bất kỳ công cụ nào
  • Phù hợp với mọi API trình duyệt từ innerHTMLdocument.createElement đến querySelector
  • Thường có thể được dùng trên các khung
  • Có thể đăng ký từng phần với customElements.define và tính năng "hydrate" Mô hình đối tượng tài liệu (DOM)

Một số nhược điểm của Phần tử tuỳ chỉnh so với các thành phần React:

  • Bạn không thể tạo phần tử tuỳ chỉnh mà không xác định lớp (do đó sẽ không có các thành phần chức năng giống như JSX)
  • Phải chứa thẻ đóng
    • Lưu ý: mặc dù các nhà cung cấp trình duyệt tiện lợi cho nhà phát triển có xu hướng hối tiếc về thông số thẻ tự đóng, đó là lý do tại sao thông số kỹ thuật mới hơn có xu hướng không bao gồm thẻ tự đóng
  • Đưa một nút bổ sung vào cây DOM, nút này có thể gây ra sự cố về bố cục
  • Phải được đăng ký qua JavaScript

Lit kết hợp các thành phần tuỳ chỉnh thay vì một hệ thống thành phần riêng vì các thành phần tuỳ chỉnh được tích hợp vào trình duyệt. Nhóm Lit tin rằng lợi ích trên nhiều khung lớn hơn lợi ích mà lớp trừu tượng thành phần mang lại. Trên thực tế, những nỗ lực của nhóm Lit đã khắc phục được những vấn đề chính khi đăng ký JavaScript. Ngoài ra, một số công ty như GitHub tận dụng tính năng đăng ký tải từng phần tử tuỳ chỉnh để từng bước cải thiện các trang bằng sự tinh tế không bắt buộc.

Chuyển dữ liệu sang các phần tử tuỳ chỉnh

Một quan niệm sai lầm phổ biến về phần tử tuỳ chỉnh là bạn chỉ có thể truyền dữ liệu dưới dạng chuỗi. Quan niệm sai lầm này có thể xuất phát từ thực tế là các thuộc tính phần tử chỉ có thể được viết dưới dạng chuỗi. Mặc dù đúng là Lit sẽ truyền thuộc tính chuỗi cho các loại đã xác định, nhưng các phần tử tuỳ chỉnh cũng có thể chấp nhận dữ liệu phức tạp làm thuộc tính.

Ví dụ: theo định nghĩa LitElement sau đây:

// data-test.ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('data-test')
class DataTest extends LitElement {
  @property({type: Number})
  num = 0;

  @property({attribute: false})
  data = {a: 0, b: null, c: [html`<div>hello</div>`, html`<div>world</div>`]}

  render() {
    return html`
      <div>num + 1 = ${this.num + 1}</div>
      <div>data.a = ${this.data.a}</div>
      <div>data.b = ${this.data.b}</div>
      <div>data.c = ${this.data.c}</div>`;
  }
}

Thuộc tính phản ứng gốc num được xác định. Thuộc tính này sẽ chuyển đổi giá trị chuỗi của một thuộc tính thành number, sau đó cấu trúc dữ liệu phức tạp được đưa vào cùng với attribute:false để tắt hoạt động xử lý thuộc tính của Lit.

Sau đây là cách chuyển dữ liệu vào phần tử tuỳ chỉnh này:

<head>
  <script type="module">
    import './data-test.js'; // loads element definition
    import {html} from './data-test.js';

    const el = document.querySelector('data-test');
    el.data = {
      a: 5,
      b: null,
      c: [html`<div>foo</div>`,html`<div>bar</div>`]
    };
  </script>
</head>
<body>
  <data-test num="5"></data-test>
</body>

Tiểu bang và Vòng đời

Các phương thức gọi lại khác trong vòng đời phản ứng

static getDerivedStateFromProps

Không có quy tắc tương đương nào trong Lit vì đạo cụ và trạng thái đều giống nhau về thuộc tính lớp

shouldComponentUpdate

  • Lít tương đương là shouldUpdate
  • Được gọi trong lần kết xuất đầu tiên, không giống như React
  • Có chức năng tương tự shouldComponentUpdate của React

getSnapshotBeforeUpdate

Theo Lit, getSnapshotBeforeUpdate tương tự như cả updatewillUpdate

willUpdate

  • Đã gọi trước update
  • Không giống như getSnapshotBeforeUpdate, willUpdate được gọi trước render
  • Các thay đổi đối với thuộc tính phản ứng trong willUpdate không kích hoạt lại chu kỳ cập nhật
  • Vị trí phù hợp để tính toán các giá trị thuộc tính phụ thuộc vào các thuộc tính khác và được sử dụng trong phần còn lại của quá trình cập nhật
  • Phương thức này được gọi trên máy chủ trong SSR, vì vậy bạn không nên truy cập vào DOM ở đây

update

  • Được gọi sau willUpdate
  • Không giống như getSnapshotBeforeUpdate, update được gọi trước render
  • Các thay đổi đối với thuộc tính phản ứng trong update sẽ không kích hoạt lại chu kỳ cập nhật nếu bạn thay đổi trước khi gọi super.update
  • Vị trí phù hợp để thu thập thông tin từ DOM xung quanh thành phần trước khi kết xuất kết xuất được cam kết cho DOM
  • Phương thức này không được gọi trên máy chủ trong SSR

Các phương thức gọi lại khác trong vòng đời Lit

Có một số phương thức gọi lại trong vòng đời không được đề cập trong phần trước vì không có phương thức tương tự nào trong React. Các yếu tố này là:

attributeChangedCallback

Phương thức này được gọi khi một trong các observedAttributes của phần tử thay đổi. Cả observedAttributesattributeChangedCallback đều nằm trong thông số kỹ thuật của các thành phần tuỳ chỉnh và được Lit triển khai nâng cao để cung cấp API thuộc tính cho các thành phần Lit.

adoptedCallback

Được gọi khi thành phần được di chuyển sang tài liệu mới, ví dụ: từ documentFragment của HTMLTemplateElement đến document chính. Lệnh gọi lại này cũng nằm trong thông số kỹ thuật của phần tử tuỳ chỉnh và chỉ nên dùng cho các trường hợp sử dụng nâng cao khi thành phần thay đổi tài liệu.

Các phương thức và thuộc tính khác trong vòng đời

Các phương thức và thuộc tính này là các thành phần của lớp mà bạn có thể gọi, ghi đè hoặc chờ để giúp thao tác với quy trình vòng đời.

updateComplete

Đây là Promise phân giải khi phần tử cập nhật xong vì vòng đời cập nhật và kết xuất không đồng bộ. Ví dụ:

async nextButtonClicked() {
  this.step++;
  // Wait for the next "step" state to render
  await this.updateComplete;
  this.dispatchEvent(new Event('step-rendered'));
}

getUpdateComplete

Đây là một phương thức sẽ được ghi đè để tuỳ chỉnh khi updateComplete phân giải. Điều này thường xảy ra khi một thành phần đang kết xuất một thành phần con và chu kỳ kết xuất của các thành phần đó phải đồng bộ hoá. ví dụ:

class MyElement extends LitElement {
  ...
  async getUpdateComplete() {
    await super.getUpdateComplete();
    await this.myChild.updateComplete;
  }
}

performUpdate

Phương thức này gọi phương thức gọi lại trong vòng đời cập nhật. Việc cập nhật này thường không cần thiết, ngoại trừ một số ít trường hợp khi quá trình cập nhật phải được thực hiện đồng bộ hoặc để lên lịch tuỳ chỉnh.

hasUpdated

Thuộc tính này là true nếu thành phần đã cập nhật ít nhất một lần.

isConnected

Là một phần của thông số kỹ thuật của phần tử tuỳ chỉnh, thuộc tính này sẽ là true nếu phần tử đang được đính kèm vào cây tài liệu chính.

Trực quan hoá vòng đời cập nhật Lit

Có 3 phần trong vòng đời cập nhật:

  • Trước khi cập nhật
  • Cập nhật
  • Sau khi cập nhật

Cập nhật trước

Đồ thị không chu trình có hướng của các nút có tên lệnh gọi lại. hàm khởi tạo để requestUpdate. @property vào Trình đặt thuộc tính. thuộc tínhChangesCallback thành Trình đặt thuộc tính. Phương thức đặt thuộc tính thành hasChanged đã thay đổi thành requestUpdate. requestUpdate chỉ ra biểu đồ vòng đời cập nhật tiếp theo.

Sau ngày requestUpdate, một bản cập nhật theo lịch sẽ chờ xử lý.

Cập nhật

Đồ thị không chu trình có hướng của các nút có tên lệnh gọi lại. Mũi tên từ hình ảnh trước về các điểm trong vòng đời trước khi cập nhật để thực hiệnCập nhật. biểu diễn cập nhật thành shouldUpdate. shouldUpdate trỏ đến cả hai &quot;hoàn tất cập nhật nếu sai&quot; và &quot;cập nhật&quot; cả hai giá trị willUpdate. sẽ cập nhật để cập nhật. cập nhật cho cả lần hiển thị cũng như biểu đồ vòng đời sau khi cập nhật tiếp theo. cũng trỏ đến biểu đồ vòng đời sau khi cập nhật.

Sau khi cập nhật

Đồ thị không chu trình có hướng của các nút có tên lệnh gọi lại. Mũi tên từ hình ảnh trước đó cho biết các điểm trong vòng đời của bản cập nhật đến firstUpdated. đầu tiên đã được cập nhật thành đã cập nhật. đã cập nhật thành updateComplete.

Móc

Lý do nên dùng nội dung hấp dẫn

Hook được đưa vào React cho các trường hợp sử dụng thành phần hàm đơn giản cần có trạng thái. Trong nhiều trường hợp đơn giản, các thành phần hàm có hook có xu hướng đơn giản và dễ đọc hơn nhiều so với các thành phần tương ứng trong lớp thành phần. Tuy nhiên, khi giới thiệu bản cập nhật trạng thái không đồng bộ cũng như truyền dữ liệu giữa các hook hoặc hiệu ứng, mẫu hook có xu hướng chưa đủ và giải pháp dựa trên lớp như bộ điều khiển phản ứng có xu hướng sẽ nổi bật.

Hook và yêu cầu API tay điều khiển

Thông thường, bạn có thể viết một hook yêu cầu dữ liệu từ một API. Ví dụ: lấy thành phần hàm React này thực hiện như sau:

  • index.tsx
    • Hiển thị văn bản
    • Hiển thị phản hồi của useAPI
      • Mã nhận dạng người dùng + Tên người dùng
      • Thông báo Lỗi
        • 404 khi tiếp cận người dùng 11 (theo thiết kế)
        • Lỗi huỷ bỏ nếu tìm nạp API bị huỷ
      • Đang tải thông báo
    • Hiển thị nút hành động
      • Người dùng tiếp theo: tìm nạp API cho người dùng tiếp theo
      • Huỷ: thao tác này sẽ huỷ việc tìm nạp API và hiển thị lỗi
  • useApi.tsx
    • Xác định một hook tuỳ chỉnh useApi
    • Sẽ không đồng bộ tìm nạp đối tượng người dùng từ một API
    • Phát ra:
      • Tên người dùng
      • Liệu hoạt động tìm nạp có đang tải hay không
      • Mọi thông báo lỗi
      • Lệnh gọi lại để huỷ tìm nạp
    • Huỷ tìm nạp đang diễn ra nếu bị tháo khỏi

Sau đây là cách triển khai Lit + Bộ điều khiển phản ứng.

Những điểm cần nhớ:

  • Bộ điều khiển phản ứng gần giống với hook tuỳ chỉnh
  • Truyền dữ liệu không kết xuất được giữa các lệnh gọi lại và hiệu ứng
    • React sử dụng useRef để truyền dữ liệu giữa useEffectuseCallback
    • Văn bản nhỏ sử dụng một thuộc tính lớp riêng tư
    • Phản ứng về cơ bản là bắt chước hành vi của một thuộc tính lớp riêng tư

Ngoài ra, nếu bạn thực sự thích cú pháp thành phần hàm React có các hook nhưng sử dụng cùng một môi trường không có bản dựng của Lit, nhóm Lit khuyên bạn nên sử dụng thư viện Haunted.

Thiếu nhi

Vị trí mặc định

Khi bạn không cung cấp thuộc tính slot cho các phần tử HTML, chúng sẽ được gán vào một vùng mặc định chưa đặt tên. Trong ví dụ bên dưới, MyApp sẽ đặt một đoạn vào một ô được đặt tên. Theo mặc định, đoạn văn bản còn lại sẽ là đoạn văn bản chưa đặt tên".

@customElement("my-element")
export class MyElement extends LitElement {
  render() {
    return html`
      <section>
        <div>
          <slot></slot>
        </div>
        <div>
          <slot name="custom-slot"></slot>
        </div>
      </section>
   `;
  }
}

@customElement("my-app")
export class MyApp extends LitElement {
  render() {
    return html`
      <my-element>
        <p slot="custom-slot">
          This paragraph will be placed in the custom-slot!
        </p>
        <p>
          This paragraph will be placed in the unnamed default slot!
        </p>
      </my-element>
   `;
  }
}

Cập nhật vị trí

Khi cấu trúc của các thành phần con vị trí thay đổi, sự kiện slotchange sẽ được kích hoạt. Thành phần Lit có thể liên kết một trình nghe sự kiện với sự kiện slotchange. Trong ví dụ bên dưới, vị trí đầu tiên tìm thấy trong shadowRoot sẽ có assignedNodes tương ứng được ghi vào bảng điều khiển trên slotchange.

@customElement("my-element")
export class MyElement extends LitElement {
  onSlotChange(e: Event) {
    const slot = this.shadowRoot.querySelector('slot');
    console.log(slot.assignedNodes({flatten: true}));
  }

  render() {
    return html`
      <section>
        <div>
          <slot @slotchange="{this.onSlotChange}"></slot>
        </div>
      </section>
   `;
  }
}

Tham chiếu

Tạo tệp đối chiếu

Cả Lit và React đều hiển thị tham chiếu đến một HTMLElement sau khi các hàm render của chúng được gọi. Tuy nhiên, bạn nên xem lại cách React và Lit sáng tạo DOM, sau đó được trả về thông qua trình trang trí Lit @query hoặc tham chiếu React.

React là một quy trình chức năng giúp tạo các Thành phần React chứ không phải HTMLElements. Vì một Tham chiếu được khai báo trước khi kết xuất HTMLElement, nên một không gian trong bộ nhớ sẽ được phân bổ. Đây là lý do tại sao bạn thấy null là giá trị ban đầu của Ref, vì phần tử DOM thực tế chưa được tạo (hoặc hiển thị), tức là useRef(null).

Sau khi ReactDOM chuyển đổi Thành phần React thành HTMLElement, hệ thống sẽ tìm một thuộc tính có tên là ref trong ReactComponent. Nếu có, ReactDOM sẽ đặt tham chiếu của HTMLElement thành ref.current.

LitElement sử dụng hàm thẻ mẫu html từ lit-html để soạn Phần tử mẫu nâng cao. LitElement gắn nhãn nội dung của mẫu vào DOM tối của phần tử tuỳ chỉnh sau khi hiển thị. DOM bóng là một cây DOM có phạm vi được bao bọc bởi một gốc bóng đổ. Sau đó, trình trang trí @query sẽ tạo một phương thức getter cho thuộc tính mà về cơ bản, thực hiện this.shadowRoot.querySelector trên thư mục gốc có phạm vi.

Truy vấn nhiều phần tử

Trong ví dụ dưới đây, trình trang trí @queryAll sẽ trả về hai đoạn văn bản ở gốc bóng đổ dưới dạng một NodeList.

@customElement("my-element")
export class MyElement extends LitElement {
  @queryAll('p')
  paragraphs!: NodeList;

  render() {
    return html`
      <p>Hello, world!</p>
      <p>How are you?</p>
   `;
  }
}

Về cơ bản, @queryAll tạo một phương thức getter cho paragraphs để trả về kết quả this.shadowRoot.querySelectorAll(). Trong JavaScript, một phương thức getter có thể được khai báo để thực hiện cùng một mục đích:

get paragraphs() {
  return this.renderRoot.querySelectorAll('p');
}

Các phần tử thay đổi truy vấn

Trình trang trí @queryAsync phù hợp hơn để xử lý một nút có thể thay đổi dựa trên trạng thái của một thuộc tính phần tử khác.

Trong ví dụ bên dưới, @queryAsync sẽ tìm phần tử đoạn đầu tiên. Tuy nhiên, phần tử đoạn sẽ chỉ hiển thị khi renderParagraph tạo ngẫu nhiên một số lẻ. Lệnh @queryAsync sẽ trả về một lời hứa sẽ phân giải khi có đoạn đầu tiên.

@customElement("my-dissappearing-paragraph")
export class MyDisapppearingParagraph extends LitElement {
  @queryAsync('p')
  paragraph!: Promise<HTMLElement>;

  renderParagraph() {
    const randomNumber = Math.floor(Math.random() * 10)
    if (randomNumber % 2 === 0) {
      return "";
    }

    return html`<p>This checkbox is checked!`
  }

  render() {
    return html`
      ${this.renderParagraph()}
   `;
  }
}

Trạng thái dàn xếp

Trong React, theo quy ước là sử dụng lệnh gọi lại vì trạng thái được trung gian bởi chính React. Phản ứng tốt nhất là không dựa vào trạng thái do các phần tử cung cấp. DOM chỉ đơn giản là một hiệu ứng của quá trình kết xuất.

Trạng thái bên ngoài

Bạn có thể sử dụng Redux, AdMob hoặc bất kỳ thư viện quản lý trạng thái nào khác cùng với Lit.

Các thành phần văn bản được tạo trong phạm vi trình duyệt. Vì vậy, bất kỳ thư viện nào cũng tồn tại trong phạm vi trình duyệt đều có sẵn cho Lit. Nhiều thư viện thú vị đã được xây dựng để sử dụng các hệ thống quản lý trạng thái hiện có trong Lit.

Sau đây là loạt video của Vaadin giải thích cách tận dụng Redux trong thành phần Lit.

Hãy xem lit-mobx của Adobe để xem một trang web có quy mô lớn có thể tận dụng AdMob trong Lit như thế nào.

Ngoài ra, hãy tham khảo Apollo Elements để biết cách các nhà phát triển đưa GraphQL vào các thành phần web của họ.

Lit hoạt động với các tính năng gốc của trình duyệt và có thể sử dụng hầu hết các giải pháp quản lý trạng thái trong phạm vi trình duyệt trong thành phần Lit.

Định kiểu

DOM bóng

Để đóng gói các kiểu và DOM trong một Phần tử tuỳ chỉnh một cách tự nhiên, Lit sử dụng DOM tối. Rễ bóng tạo ra một cây bóng đổ tách biệt với cây tài liệu chính. Tức là hầu hết các kiểu đều thuộc phạm vi của tài liệu này. Một số kiểu nhất định bị rò rỉ như màu sắc và các kiểu khác liên quan đến phông chữ.

Shadow DOM cũng giới thiệu các khái niệm và bộ chọn mới cho thông số CSS:

:host, :host(:hover), :host([hover]) {
  /* Styles the element in which the shadow root is attached to */
}

slot[name="title"]::slotted(*), slot::slotted(:hover), slot::slotted([hover]) {
  /*
   * Styles the elements projected into a slot element. NOTE: the spec only allows
   * styling the direcly slotted elements. Children of those elements are not stylable.
   */
}

Kiểu chia sẻ

Lit giúp bạn dễ dàng chia sẻ kiểu giữa các thành phần dưới dạng CSSTemplateResults thông qua thẻ mẫu css. Ví dụ:

// typography.ts
export const body1 = css`
  .body1 {
    ...
  }
`;

// my-el.ts
import {body1} from './typography.ts';

@customElement('my-el')
class MyEl Extends {
  static get styles = [
    body1,
    css`/* local styles come after so they will override bod1 */`
  ]

  render() {
    return html`<div class="body1">...</div>`
  }
}

Giao diện

Gốc đổ bóng cho thấy một chút thách thức đối với việc thiết kế giao diện thông thường, thường là phương pháp gắn thẻ kiểu từ trên xuống. Cách thông thường để xử lý giao diện bằng các Thành phần web sử dụng Shadow DOM là hiển thị một API kiểu thông qua Thuộc tính tuỳ chỉnh CSS. Ví dụ: đây là mẫu mà Material Design sử dụng:

.mdc-textfield-outline {
  border-color: var(--mdc-theme-primary, /* default value */ #...);
}
.mdc-textfield--input {
  caret-color: var(--mdc-theme-primary, #...);
}

Sau đó, người dùng thay đổi giao diện của trang web bằng cách áp dụng các giá trị thuộc tính tuỳ chỉnh:

html {
  --mdc-theme-primary: #F00;
}
html[dark] {
  --mdc-theme-primary: #F88;
}

Nếu bạn bắt buộc phải sắp xếp giao diện từ trên xuống và không thể hiển thị kiểu, thì bạn luôn có thể tắt DOM bóng bằng cách ghi đè createRenderRoot để trả về this. Sau đó, bạn sẽ hiển thị các thành phần của mình vào chính phần tử tuỳ chỉnh thay vì vào gốc bóng được đính kèm với phần tử tuỳ chỉnh. Với thao tác này, bạn sẽ mất: đóng gói kiểu, đóng gói DOM và vị trí.

Sản xuất

IE 11

Nếu bạn cần hỗ trợ các trình duyệt cũ hơn như IE 11, bạn sẽ phải tải một số polyfill có kích thước khoảng 33kb khác. Bạn có thể tìm thêm thông tin tại đây.

Gói có điều kiện

Nhóm Lit đề xuất phân phối hai gói khác nhau, một gói cho IE 11 và một gói cho các trình duyệt hiện đại. Việc này mang lại một số lợi ích như sau:

  • Phục vụ ES 6 nhanh hơn và sẽ phục vụ hầu hết khách hàng của bạn
  • ES 5 được dịch mã làm tăng đáng kể kích thước gói
  • Gói có điều kiện mang đến cho bạn những lợi ích tốt nhất ở cả hai mặt
    • Hỗ trợ IE 11
    • Không làm chậm các trình duyệt hiện đại

Bạn có thể xem thêm thông tin về cách tạo gói được phân phát có điều kiện trên trang web tài liệu của chúng tôi tại đây.