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ẹ, 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à nhiều nội dung khác 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à đạo cụ
  • Trạng thái và vòng đời
  • Móc
  • Thiếu nhi
  • Tài liệu tham khảo
  • Trạng thái dàn xếp

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

Khi kết thúc lớp học lập trình này, bạn sẽ có thể chuyển đổi các khái niệm về thành phần React sang các thành phần tương tự trong Lit.

Bạn cần có

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

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

Quá nhỏ

Lit rất nhỏ: chỉ khoảng 5 kb được rút gọn và nén bằng gzip so với hơn 40 kb của React + ReactDOM.

Biểu đồ thanh về kích thước gói được rút gọn và 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 nhanh hơn 8-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) thêm một mức hao tổn tối thiểu vào lit-html, nhưng vượt qua hiệu suất của React từ 16 đến 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ớ, mức tương tác và thời gian khởi động.

biểu đồ thanh được nhóm theo hiệu suất so sánh lit với React tính bằng mili giây (thấp hơn là tốt hơn)

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

Với các tính năng trình duyệt mới 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 phát triển bằng một thẻ tập lệnh + một trình duyệt + một máy chủ và bắt đầu sử dụng.

Với các mô-đun ES và CDN hiện đại như Skypack hoặc UNPKG, bạn thậm chí có thể không cần NPM để 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 gần đây của nhà phát triển xung quanh các mô-đun ES gốc đã mang lại lợi ích cho Lit – Lit chỉ là JavaScript thông thường và không cần CLI dành riêng cho khung hoặc xử lý bản dựng.

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

Các thành phần của Lit được xây dựng dựa trên 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 trình duyệt hỗ trợ các phần tử HTML, thì trình duyệt đó cũng hỗ trợ Thành phần web.

Vấn đề duy nhất với khả năng tương tác khung là khi các khung có hỗ trợ hạn chế cho DOM. React là một trong những khung này, nhưng cho phép các lối thoát thông qua Refs và Refs trong React không mang lại 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, Custom Elements Everywhere (Phần tử tuỳ chỉnh ở mọi nơi) sẽ cho bạn biết những 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 nghị 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 mang đến tính năng kiểm tra kiểu và intellisense của TypeScript cho các mẫu Lit ở cả thời gian phát triển và thời gian tạo bản dựng bằng lit-analyzerlit-plugin.

Ảnh chụp màn hình IDE cho thấy hoạt động kiểm tra loại 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á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 Lit chỉ là các 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} hiển thị kết quả 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 để hỗ trợ 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 dự kiến sẽ sớm phát hành các công cụ này 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.

Chi phí đầu tư 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 có. Nếu không thích, bạn không cần 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 khung mới và di chuyển mọi thứ bạn 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ợ dữ liệu đầ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 thành phần Lit.

3. Thiết lập và khám phá Playground

Có hai cách để tham gia 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ủa bạn. 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ó thể tự động phân giải các lệnh 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ể thực hiện toàn bộ hướng dẫn trong Lit Playground, 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 thẻ bộ chọn tệp được gắn nhãn Mục 1, mục chỉnh sửa mã là Mục 2, bản xem trước đầu ra là Mục 3 và nút tải lại bản xem trước là Mục 4

Ảnh chụp màn hình giao diện người dùng Lit playground làm nổi 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. Bản 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)

Sau đây là những lợi ích của việc sử dụng chế độ thiết lập VS Code này:

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

Nếu bạn đã cài đặt Gradle, 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 có TS) hãy thiết lập tsconfig nhanh để xuất các mô-đun es và es2015 trở lên
  • Cài đặt một máy chủ phát triển có thể phân giải chỉ định mô-đun trần (nhóm Lit đề xuất @web/dev-server)
  • Chạy máy chủ phát triển và mở trình duyệt (nếu đang sử dụng @web/dev-server, bạn có thể sử dụng npx web-dev-server --node-resolve --watch --open)
    • Nếu bạn đang sử 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ề cách tạo mẫu trong Lit.

Mẫu JSX và 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 các mẫu trong mã JavaScript. 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ẽ hiển thị một lời chào thế giới JSX 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ó 2 phần tử và một biến "name" được bao gồm. Trong 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 gói bằng html mẫu được gắn thẻ LITeral. Đây cũng chính là nguồn gốc của tên Lit!

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 bằng một giá trị cố định mẫu được gắn thẻ Lưu ý Hãy nhớ sử dụng ký tự dấu phẩy ngược (`) chứ không phải ký tự dấu ngoặc kép đơ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
);

TemplateResult được thắp sáng có thể chấp nhận các mảng, chuỗi, TemplateResult khác cũng như các 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 điểm khác biệt lớn nhất giữa cú pháp JSX và Lit là cú pháp liên kết dữ liệu. Ví dụ: hãy lấy dữ liệu đầu vào React này 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, một dữ liệu đầu vào được xác định để thực hiện những việc 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 cộng với một biến (trong trường hợp này là "static-class my-class")
  • Đặt giá trị mặc định

Trong 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 sử 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 trên 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ử nhập vào chế độ chỉ có thể đọc vì phần tử này tuân theo cách triển khai và hành vi gốc của dữ liệu đầu vào.

Cú pháp liên kết thuộc tính Lit

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
  • Thích 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)
  • Thích hợp với các giá trị cơ bản, bộ chọn quy tắc kiểu và querySelector

Truyền trình xử lý

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, một dữ liệu đầu vào được xác định để thực hiện những việc sau:

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

Trong 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, có một liên kết đến sự kiện input gốc của <input>sự kiện change gốc chỉ kích hoạt trên blur (React tóm tắt 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à đạo cụ

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à móc sẽ được trình bày chi tiết hơn trong các phần sau.

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

Thành phần Lit tương đương với một thành phần của lớp React là LitElement, còn khái niệm của Lit về "thuộc tính phản ứng" là sự kết hợp giữa các đạ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ị 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 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>

Xem xét những gì đang xảy ra trong ví dụ trên:

@property({type: String})
name = '';
  • Xác định một thuộc tính phản ứng công khai – một phần của 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
  • Do tiêu chuẩn Phần tử tuỳ chỉnh, tên thẻ phải có 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 có cách diễn giải 1:1 về thành phần hàm vì không sử dụng JSX hoặc trình xử lý trước. 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
);

Trong Lit, mã 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. Trạng thái 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. Khi thay đổi, các thuộc tính phản ứng có thể kích hoạt vòng đời của thành phần. Có hai biến thể thuộc tính phản ứng:

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 dùng thành phần truy cập và đặt

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';
  }
}
  • Giá trị tương đương của Lit cũng là constructor
  • Bạn không cần truyền bất kỳ giá trị nào đến lệnh gọi gốc
  • Được gọi bởi (không hoàn toàn bao gồm):
    • 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ự như constructor của React

render

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

// Lit
render() {
  return html`<div>Hello World</div>`;
}
  • Giá trị tương đương của Lit cũng là render
  • Có thể trả về bất kỳ kết quả nào có thể kết xuất, 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 mà createRenderRoot() trả về (ShadowRoot theo mặc định)

componentDidMount

componentDidMount tương tự như sự kết hợp của cả hai lệnh 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
  • 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 thêm vào cây DOM
  • Đây là vị trí thích hợp để thực hiện việc thiết lập thành phần yêu cầu DOM do thành phần hiển thị
  • Không giống như componentDidMount của React, các thay đổi đối với thuộc tính phản ứng trong firstUpdated sẽ khiến quá trình kết xuất lại diễn ra, mặc dù trình duyệt thường sẽ phân lô 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 gốc, thì chúng thường sẽ nằm trong 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, các phần tử này không bị huỷ 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ệnh gọi đầu tiên, DOM có thể không hoạt động

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);
  }
}
  • Tương đương với Lit là updated (sử dụng thì quá khứ của từ "cập nhật" trong tiếng Anh)
  • 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 sẽ không bị huỷ
  • 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 đính kèm vào 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à tham chiếu bị rò rỉ để trình duyệt có thể thu thập rác thành phần

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à" rồi hiển thị 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 analog là sự kết hợp giữ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 để không thực thi dấu kiểm 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);
}

Khi 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 chuyển đổi các khái niệm về React Hook sang Lit.

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

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

  • Các lớp này giúp đơ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, việc tập trung 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

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 và đạo cụ, Lit không cung cấp cách tạo phần tử tuỳ chỉnh qua một hàm, nhưng LitElement giải quyết được hầu hết các vấn đề chính với 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 tệp tham chiếu của phần tử tuỳ chỉnh
  • Giờ đây, bạn có thể tạo bản sao các thuộc tính lớp dưới dạng thành viên 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 trình điều khiển phản ứng. Các mẫu trình đ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, 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 và 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:

  • Tệp này hiển thị "Hello World! Hiện tại là", sau đó hiển thị thời gian
  • Mỗi giây, đồng hồ sẽ cập nhật
  • Khi tháo rời, tính năng này sẽ xoá khoảng thời gian gọi kim đánh dấu

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

Trước tiên, hãy bắt đầu với phần 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);

Tạo trình đ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 lệnh gọi lại trong vòng đời của React thành lệnh gọi lại của bộ điều khiển. Tóm lại:

  • 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ề việc chuyển đổi vòng đời 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ụ ở mục Trạng thái và 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 trình đ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 quá trình kết xuất lại trong trình điều khiển

Lưu ý rằng đồng hồ sẽ hiển thị thời gian, nhưng thời gian không cập nhật. Điều này là do trình điều khiển đang đặt ngày mỗi giây, nhưng máy chủ lưu trữ 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();
}

Giờ thì đồng hồ đã bắt đầu chạy!

Để so sánh chi tiết hơn về các trường hợp sử dụng phổ biến với các móc, vui lòng xem phần Các 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 các khe để quản lý phần tử con trong Lit.

Máy đánh bạc và trẻ em

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

Trong React, các thành phần con được kế thừa thông qua các thuộc tính. Khe mặc định là props.children và hàm render xác định vị trí của khe mặc định. Ví dụ:

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

Xin lưu ý rằng props.children là thành phần React chứ không phải 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 các thành phần con không được kế thừa theo cách tương tự như React. Trong Lit, phần tử con là HTMLElement được đính kèm vào các khe. 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 khe

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 khe đượ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 mặc định của khung

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 phần tử con cho các vị trí

Trong React, các phần tử con được chỉ định cho các khe 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 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 truy cập vào API của 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.

Tài liệu tham khảo về React

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

Trong React, Refs 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 một vùng 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 bài hát "Tham chiếu" 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 mỏng so với các tính năng gốc của trình duyệt.

Phản ứng 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 lệnh gọi lại sự kiện nhấp 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 cam kết mẫu của phương thức render vào thư mục gốc của my-element, trình trang trí @query hiện sẽ cho phép inputEl trả về phần tử input đầu tiên tìm thấy trong thư mục gốc hiển thị. 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

React mô phỏng quy trình kết xuất chức năng bằng luồng dữ liệu từ trên xuống. Thành phần mẹ cung cấp trạng thái cho thành phần con thông qua các thuộc tính. 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, một thành phần React thực hiện những việc sau:

  • Tạo nhãn dựa trên giá trị props.step.
  • Kết xuất một 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 lại 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ụ dưới đây:

@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, Thành phần Lit sẽ thực hiện những việc sau:

  • Tạo thuộc tính phản ứng step
  • Gử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

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 các thành phần con phát đi các sự kiện tương tác và thay đổi trạng thái. Về cơ bản, React truyền trạng thái theo hướng ngược lại, vì vậy, hiếm khi thấy Thành phần React gửi và nghe 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 addToCounter được truyền đế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 một 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 theo step trên 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ị tìm thấy 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

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 Sáng tạo mang đến 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 nội tuyến.

Bây giờ, 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ể hơi phiền phức, vì vậy, Lit cung cấp một lệnh để giúp giải quyết vấn đề này.

styleMap

Lệnh styleMap giúp bạn dễ dàng sử dụng JavaScript để đặt kiểu nội tuyến. 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:

  • Cho thấy một 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 để đặt 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 sẽ 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 của hai h1 có 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
  • Dễ dàng tách các kiểu thành 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 lưu ý thẻ <style> trong index.html:

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

Tính năng Lit sẽ giới hạn các kiểu của thành phần ở cấp độ gốc. Điều này có nghĩa là các kiểu sẽ không bị rò rỉ vào và ra. Để truyền kiểu cho 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 CSS vì các thuộc tính này có thể thâm nhập vào phạm vi kiểu Lit.

Thẻ kiểu

Bạn cũng có thể chỉ cần nội tuyến các thẻ <style> trong mẫu. Trình duyệt sẽ loại bỏ trùng lặp các thẻ kiểu này, nhưng bằng cách đặt các thẻ này vào mẫu, các thẻ này sẽ được phân tích cú pháp theo mỗi thực thể thành phần thay vì theo 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

Lit và DOM ảo

Lit-html không bao gồm một DOM ảo thông thường để so sánh từng nút riêng lẻ. Thay vào đó, tính năng 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. Giá trị cố định của mẫu được gắn thẻ là các chuỗi cố định mẫu có các hàm thẻ đi kèm với chúng.

Dưới đây là ví dụ về một 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ừ thực tế là các mảng chuỗi được truyền vào hàm thẻ có cùng một con trỏ (như 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 mẫu (tức là ở cùng một vị trí trong AST). Vì vậy, tính 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 hao tổn về thời gian chạy.

Hành vi tích hợp sẵn của trình duyệt đối với các giá trị cố định mẫu được gắn thẻ mang lại cho Lit một 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, các giá trị cố định mẫu được gắn thẻ thực hiện hầu hết các hoạt động so sánh 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, chẳng hạ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.

Lit & JSX + React DOM

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

Ví dụ: Babel sẽ chuyển đổi mã sau:

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

thành:

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 trong 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 Virtual DOM thông thường mà sử dụng trực tiếp DOM API, kích thước của Lit 2 nhỏ hơn 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, đồng thời 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 một sự kiện tuỳ chỉnh và nhà phát triển phải sử dụng ref, sau đó áp dụng trình nghe một cách bắt buộc. Đ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, việc thêm trình nghe sự kiện cũng dễ dàng như @event-name=${eventNameListener}. Điều này có nghĩa là ít phải phân tích cú pháp thời gian chạy 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à đạo cụ

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

Trong phần nội dung, LitElement sử dụng các phần tử tuỳ chỉnh để đóng gói các thành phần của nó. Các phần tử tuỳ chỉnh đưa ra một số sự đánh đổi giữa các thành phần React khi nói đến việc phân 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).

Một số lợi thế của Phần tử tuỳ chỉnh khi đóng vai trò là 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ể được đăng ký từng phần bằng customElements.define và "hydrat" 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:

  • Không thể tạo phần tử tuỳ chỉnh mà không xác định lớp (do đó sẽ không có thành phần chức năng giống 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 đã sử dụng các thành phần tuỳ chỉnh thay vì hệ thống thành phần tuỳ chỉnh vì các thành phần tuỳ chỉnh được tích hợp sẵn vào trình duyệt. Đồng thời, nhóm Lit tin rằng các lợi ích trên nhiều khung vượt trội so với lợi ích do 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.

Truyền dữ liệu đến các phần tử tuỳ chỉnh

Một quan niệm sai lầm thường gặp về phần tử tuỳ chỉnh là dữ liệu chỉ có thể được truyền dưới dạng chuỗi. Quan niệm sai lầm này có thể xuất phát từ việc 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 các thuộc tính chuỗi đến 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 dưới dạng thuộc tính.

Ví dụ: với định nghĩa LitElement sau:

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

Trạng thái 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

  • Giá trị tương đương được thắp sá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ự như shouldComponentUpdate của React

getSnapshotBeforeUpdate

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

willUpdate

  • Được 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
  • Đây là nơi thích 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 quy 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 tại đâ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ố lệnh gọi lại vòng đời không được đề cập trong phần trước vì không có lệnh gọi lại tương tự trong React. Các loại chiến dịch phụ đó 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 này được di chuyển sang tài liệu mới, ví dụ: từ documentFragment của HTMLTemplateElement sang document chính. Lệnh gọi lại này cũng là một phần của thông số kỹ thuật phần tử tuỳ chỉnh và chỉ nên được 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 viên 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 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

Mộ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. hàm khởi tạo để requestUpdate. @property thành Property Setter. attributeChangedCallback thành Property Setter. Phương thức setter của thuộc tính thành hasChanged. hasChanged thành requestUpdate. requestUpdate trỏ đến biểu đồ vòng đời cập nhật tiếp theo.

Sau requestUpdate, hệ thống sẽ chờ bản cập nhật theo lịch.

Cập nhật

Biểu đồ không tuần hoàn 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 của các điểm vòng đời trước khi cập nhật đến performUpdate. performUpdate đến shouldUpdate. shouldUpdate trỏ đến cả &quot;hoàn tất cập nhật nếu sai&quot; cũng như willUpdate. willUpdate để cập nhật. cập nhật cả hiển thị cũng như biểu đồ vòng đời tiếp theo, sau khi cập nhật. hiển thị cũng trỏ đến biểu đồ vòng đời tiếp theo, 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 gọi lại. Mũi tên từ hình ảnh trước của vòng đời cập nhật trỏ đến firstUpdated. firstUpdated thành updated. updated 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 trạng thái. Trong nhiều trường hợp đơn giản, các thành phần hàm có hook thường đơn giản và dễ đọc hơn nhiều so với các thành phần lớp tương ứng. Tuy nhiên, khi giới thiệu các 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 không đủ và giải pháp dựa trên lớp như trình điều khiển phản ứng có xu hướng tỏa sáng.

hook và bộ điều khiển yêu cầu API

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
    • Kết xuất 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 trình bổ trợ 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 thể kết xuất 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
    • Lit sử dụng 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 với các hook nhưng vẫn muốn sử dụng môi trường không có bản dựng của Lit, thì 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. Đoạn văn còn lại sẽ được đặt theo mặc định vào vị trí không 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 phần tử con của khe thay đổi, sự kiện slotchange sẽ được kích hoạt. Thành phần Lit có thể liên kết trình nghe sự kiện với sự kiện slotchange. Trong ví dụ bên dưới, khe đầu tiên tìm thấy trong shadowRoot sẽ có assignedNodes đượ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 xét cách React và Lit kết hợp DOM mà sau đó được trả về thông qua trình trang trí @query Lit 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ì Ref được khai báo trước khi HTMLElement hiển thị, nên một không gian trong bộ nhớ sẽ được phân bổ. Đây là lý do bạn thấy null là giá trị ban đầu của một 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 một Thành phần React thành HTMLElement, ReactDOM 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 đến ref.current.

LitElement sử dụng hàm thẻ mẫu html từ lit-html để tạo một Thành phần mẫu. LitElement đóng dấu nội dung của mẫu vào shadow DOM của phần tử tuỳ chỉnh sau khi kết xuất. Shadow DOM là một cây DOM có giới hạn được đóng gói bằng 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, về cơ bản thực hiện this.shadowRoot.querySelector trên gốc có giới hạn.

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 trong gốc bóng dưới dạng 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, bạn có thể khai báo phương thức getter để thực hiện cùng một mục đích:

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

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

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 văn đầu tiên. Tuy nhiên, phần tử đoạn văn sẽ chỉ được hiển thị khi renderParagraph tạo ngẫu nhiên một số lẻ. Chỉ thị @queryAsync sẽ trả về một lời hứa sẽ giải quyết khi có đoạn vă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, quy ước là sử dụng lệnh gọi lại vì trạng thái do chính React điều phối. 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 phim 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 cách một trang web có quy mô lớn có thể tận dụng AdMob trong Lit.

Ngoài ra, hãy xem 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. Shadow Roots tạo một cây bóng riêng 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 sẽ bị rò rỉ, chẳng hạn 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.
   */
}

Chia sẻ kiểu

Lit giúp bạn dễ dàng chia sẻ kiểu giữa các thành phần ở 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ắt buộc phải tạo giao diện từ trên xuống và bạn không thể hiển thị các kiểu, thì bạn luôn có thể tắt Shadow DOM bằng cách ghi đè createRenderRoot để trả về this. Sau đó, this sẽ hiển thị mẫu của các thành phần cho chính phần tử tuỳ chỉnh thay vì cho gốc bóng được đính kèm vào 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ó dung lượng 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 có một số lợi ích như sau:

  • Việc phân phát ES 6 nhanh hơn và sẽ phân phát cho hầu hết ứng dụng của bạn
  • ES 5 được chuyển đổi 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ủa cả hai phương thức
    • Hỗ trợ IE 11
    • Không làm chậm trên 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.