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 nhanh, gọn nhẹ, hoạt động trong mọi khung hoặc không có khung nào. Với Lit, bạn có thể tạo các thành phần, ứng dụng, hệ thống thiết kế có thể chia sẻ và nhiều nội dung khác.
Kiến thức bạn sẽ học được
Cách chuyển đổi một số khái niệm của React sang Lit, chẳng hạn như:
- JSX và mẫu
- Thành phần và đạo cụ
- Trạng thái và vòng đời
- Khoảnh khắc níu chân
- Thiếu nhi
- Refs
- Trạng thái trung gian
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 khái niệm tương tự trong Lit.
Bạn cần có
- Phiên bản mới nhất của Chrome, Safari, Firefox hoặc Edge.
- Có kiến thức về HTML, CSS, JavaScript và Chrome DevTools.
- Kiến thức về React
- (Nâng cao) Nếu muốn có trải nghiệm phát triển tốt nhất, hãy tải VS Code xuống. Bạn cũng cần có lit-plugin cho VS Code và NPM.
2. Lit so với React
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à yếu tố phân biệt chính:
Nó nhỏ
Lit có kích thước rất nhỏ: chỉ khoảng 5 KB khi được rút gọn và nén gzip so với 40 KB của React + ReactDOM.

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 React 8-10% 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) chỉ thêm một lượng nhỏ tài nguyên vào lit-html, nhưng vượt trội hơn React từ 16 đến 30%về hiệu suất 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.

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à chuỗi ký tự 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 thẻ tập lệnh + trình duyệt + máy chủ và bắt đầu hoạt độ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!
Mặc dù vậy, 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 rất phù hợp với Lit – Lit chỉ là JavaScript thông thường và không cần đến các 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 hỗ trợ các phần tử HTML, thì trình duyệt đó sẽ hỗ trợ Thành phần web.
Vấn đề duy nhất với khả năng tương tác giữa các khung là khi các khung có chế độ hỗ trợ hạn chế cho DOM. React là một trong những khung này, nhưng nó cho phép các lối thoát thông qua Refs và Refs trong React không phải là 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 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 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!
Khả năng hỗ trợ TypeScript hàng đầu
Mặc dù bạn có thể viết tất cả mã Lit bằng JavaScript, nhưng Lit được viết bằng TypeScript và nhóm Lit khuyến nghị nhà phát triển cũng nên sử dụng TypeScript!
Nhóm Lit đã hợp tác với cộng đồng Lit để duy trì các dự án mang tính năng kiểm tra kiểu TypeScript và tính năng hỗ trợ thông minh cho các mẫu Lit tại cả thời gian phát triển và thời gian xây dựng bằng lit-analyzer và lit-plugin.


Các công cụ cho nhà phát triển được tích hợp vào trình duyệt
Các 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 cài đặt bất kỳ công cụ hoặc tiện ích nào cho trình duyệt của mình.
Bạn chỉ cần mở công cụ dành 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ử đó.

Công cụ này được xây dựng dựa trên tính năng kết xuất phía máy chủ (SSR)
Lit 2 được xây dựng với mục tiêu 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 đã triển khai các thành phần được 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 ra bên ngoài trên GitHub.
Trong thời gian chờ đợi, bạn có thể theo dõi tiến trình của nhóm Lit tại đây.
Chi phí tham gia thấp
Bạn không cần phải cam kết sử dụng Lit trong thời gian dài! Bạn có thể tạo các thành phần trong Lit và thêm chúng vào dự án hiện có. Nếu không thích, bạn không cần 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 đã tạo một ứng dụng hoàn chỉnh bằng 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 bên trong khung mới và di chuyển bất cứ thứ gì 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ợ đầu ra trong các thành phần web, nghĩa là các khung này thường có thể nằm trong chính một phần tử Lit.
3. Thiết lập và khám phá Sân chơi
Có 2 cách để thực hiện lớp học lập trình này:
- Bạn có thể thực hiện hoàn toàn trực tuyến, trong trình duyệt
- (Nâng cao) Bạn có thể thực hiện việc này trên máy cục bộ bằng VS Code
Truy cập vào mã
Trong suốt lớp học lập trình, sẽ có các đường liên kết đến sân chơi Lit như sau:
Sân chơi là một hộp cát mã chạy hoàn toàn trong trình duyệt của bạn. Nó 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 lần 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 bắt đầu. Nếu đang dùng VS Code, bạn có thể sử dụng các điểm kiểm tra này để tải mã bắt đầu cho bất kỳ bước nào, cũng như sử dụng các điểm kiểm tra này để kiểm tra bài làm của mình.
Khám phá giao diện người dùng của sân chơi được chiếu sáng

Ảnh chụp màn hình giao diện người dùng của Lit playground làm nổi bật những phần mà bạn sẽ sử dụng trong lớp học lập trình này.
- Bộ chọn tệp. Lưu ý nút dấu cộng...
- Trình chỉnh sửa tệp.
- Xem trước mã.
- Nút Tải lại.
- Nút tải xuống.
Thiết lập VS Code (Nâng cao)
Sau đây là những lợi ích khi sử dụng chế độ thiết lập VS Code này:
- Kiểm tra loại mẫu
- Tính năng IntelliSense và tự động hoàn thành mẫu
Nếu đã cài đặt NPM, VS Code (có trình bổ trợ lit-plugin) và biết cách sử dụng môi trường đó, bạn chỉ cần tải xuống và bắt đầu các dự án này bằng cách làm như sau:
- Nhấn 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 một quick tsconfig xuất ra các mô-đun es và es2015+
- Cài đặt một máy chủ phát triển có thể phân giải các chỉ định mô-đun trần (nhóm Lit đề xuất @web/dev-server)
- Sau đây là một ví dụ
package.json
- Sau đây là một ví dụ
- Chạy máy chủ phát triển và mở trình duyệt (nếu đang dùng @web/dev-server, bạn có thể 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ụngnpm run dev
- Nếu bạn đang sử dụng ví dụ
4. JSX và 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.
Mẫu JSX và Lit
JSX là một tiện ích 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 của họ. Mẫu Lit 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 mộ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ó 2 phần tử và một biến "name" được đưa vào. 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
);
Xin lưu ý rằng các mẫu Lit không cần một React Fragment để 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 html mẫu được gắn thẻ LITeral. Đây cũng chính là nguồn gốc tên của Lit!
Giá trị mẫu
Các mẫu Lit có thể chấp nhận các mẫu Lit khác, còn được gọi là TemplateResult. Ví dụ: gói name trong thẻ in nghiêng (<i>) và gói thẻ này bằng một chuỗi ký tự mẫu được gắn thẻ Lưu ý Đảm bảo 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
);
Lit TemplateResult có thể chấp nhận các mảng, chuỗi, TemplateResult khác cũng như các chỉ thị.
Để thực hành, hãy thử chuyển đổi 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
);
Truyền và thiết lập các prop
Một trong những điểm 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ụ: hãy xem xét đầu vào React này có 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 đầu vào được xác định để thực hiện những việc sau:
- Đặt trạng thái vô hiệu hoá thành một biến đã xác định (trong trường hợp này là false)
- Đặt lớp thành
static-classcộ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 dùng chỉ thị classMap. Đây là một trợ giúp khai báo để chuyển đổi 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ử đầu vào ở chế độ 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 Lit prop
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
disabledvìdisabled="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 để thiết lập một thuộc tính của 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 một 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, một đầu vào được xác định để thực hiện những việc sau:
- Ghi lại từ "click" khi người dùng nhấp vào đầ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ự
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, sẽ có một liên kết đến sự kiện input gốc của <input> vì sự kiện change gốc chỉ kích hoạt trên blur (React trừu tượng hoá các sự kiện này).
Cú pháp trình xử lý sự kiện Lit
html`<my-element @event-name=${() => {...}}></my-element>`;
- Tiền tố
@là cú pháp liên kết cho một 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 hàm và thành phần lớp Lit. Trạng thái và Hook 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
LitElement là thành phần tương đương của Lit với thành phần lớp React, còn khái niệm "thuộc tính phản ứng" của Lit là sự kết hợp giữa các thuộc tính 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
namethành chuỗi trống ("") - Giao lại
namecho"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 diễn 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 dịch 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}
}
}
- Thao tác này có chức năng tương tự như trình trang trí
@propertyTS nhưng chạy nguyên bản trong JavaScript
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
- Phương thức này được gọi bất cứ khi nào có thuộc tính phản ứng 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 một định nghĩa lớp
- Do tiêu chuẩn Custom Elements, tên thẻ phải có dấu gạch ngang (-)
thistrong LitElement đề cập đến phiên bản 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à phiên bản JavaScript tương đương của phương thức trang trí
@customElementTS
<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
namethà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 một trình tiền xử lý. Mặc dù vậy, bạn có thể dễ dàng tạo một hàm nhận các thuộc tính và hiển thị DOM dựa trên các thuộc tính đó. 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, Reactive Properties (Thuộc tính phản ứng) có thể kích hoạt vòng đời của thành phần. Các thuộc tính phản ứng có hai 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
@propertyxác định - Tương tự như các thuộc tính và trạng thái của React nhưng có thể thay đổi
- API công khai mà người dùng của 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
@statexá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 của 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';
}
}
- Đơn vị tương đương là
constructor - Bạn không cần truyền bất kỳ thông tin nào vào lệnh gọi super
- Được gọi bởi (không hoàn toàn bao gồm):
document.createElementdocument.innerHTMLnew ComponentClass()- Nếu tên thẻ chưa được nâng cấp nằm trên trang và định nghĩa được tải và đăng ký bằng
@customElementhoặccustomElements.define
- Có chức năng tương tự như
constructorcủa React
render
// React
render() {
return <div>Hello World</div>
}
// Lit
render() {
return html`<div>Hello World</div>`;
}
- Đơn vị tương đương là
render - Có thể trả về mọi kết quả có thể hiển thị, ví dụ:
TemplateResulthoặcstring, v.v. - Tương tự như React,
render()phải là một hàm thuần tuý - Sẽ kết xuất vào bất kỳ nút nào mà
createRenderRoot()trả về (theo mặc định làShadowRoot)
componentDidMount
componentDidMount tương tự như sự kết hợp của cả phương thức gọi lại trong vòng đời firstUpdated và connectedCallback 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 vào lần đầu tiên mẫu của thành phần được kết xuất vào 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 thiết lập thành phần yêu cầu DOM do thành phần kết xuất
- Không giống như các thay đổi
componentDidMountcủa React đối với các thuộc tính phản ứng trongfirstUpdatedsẽ khiến quá trình kết xuất lại diễn ra, mặc dù trình duyệt thường sẽ gộp các thay đổi vào cùng một khung hình. 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 trongwillUpdate
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 tách khỏi DOM, chúng sẽ không bị huỷ và do đó có thể được "kết nối" nhiều lần
firstUpdatedsẽ không được gọi lại
- Hữu ích khi khởi động lại DOM hoặc đính kèm lại các trình nghe sự kiện đã được dọn dẹp khi ngắt kết nối
- Lưu ý:
connectedCallbackcó thể được gọi trướcfirstUpdated, vì vậy, trong lần 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
updated(sử dụng thì quá khứ đơn của động từ "update" trong tiếng Anh) - Không giống như React,
updatedcũng được gọi trong quá trình kết xuất ban đầu - Có chức năng tương tự như
componentDidUpdatecủa React
componentWillUnmount
// React
componentWillUnmount() {
this.window.removeEventListener('resize', this.boundOnResize);
}
// Lit
disconnectedCallback() {
super.disconnectedCallback();
this.window.removeEventListener('resize', this.boundOnResize);
}
- Lit 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 bị 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 gốc vẫn được đính kèm vào cây con của gốc
- Hữu ích cho việc dọn dẹp trình nghe sự kiện và các tham chiếu 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:
- Nó hiển thị "Hello World! It is" (Đó là) rồi hiển thị thời gian
- Đồng hồ sẽ cập nhật từng giây
- Khi được tháo, thành phần này sẽ xoá khoảng thời gian gọi đánh dấu
Trước tiên, hãy bắt đầu bằng 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 đây là một thuộc tính phản ứng nội bộ bằng @state vì người dùng của thành phần sẽ không đặt trực tiếp date.
// 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.
tick() {
this.date = new Date();
}
Tiếp theo là việc triển khai componentDidMount. Một lần nữa, Lit tương tự như sự kết hợp giữa firstUpdated và connectedCallback. 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 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. Do đó, nếu được đính kèm lại, khoảng thời gian sẽ cần bắt đầu lại. Do đó, connectedCallback là lựa chọn phù hợp hơn trong trường hợp 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 đó không thực thi dấu tích 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ã này 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. Khoảnh khắc níu chân
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.
Các khái niệm về React Hook
Các hook của React cung cấp một cách để các thành phần hàm "kết nối" vào trạng thái. Việc này mang lại một số lợi ích.
- Các thành phần này đơ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 đề với cú pháp dựa trên lớp của React, chẳng hạn như:
- Phải vượt qua
propstừconstructorđếnsuper - Việc khởi tạo thuộc tính không gọn gàng trong
constructor- Đây là lý do mà nhóm React đã nêu tại thời điểm đó nhưng đã được ES2019 giải quyết
- Các vấn đề do
thiskhông còn đề cập đến thành phần này nữa
Các khái niệm về hook của React trong Lit
Như đã đề cập trong phần Components & Props (Thành phần và thuộc tính), Lit không cung cấp cách tạo các phần tử tuỳ chỉnh từ một hàm, nhưng LitElement giải quyết hầu hết các vấn đề chính với các thành phần 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 những vấn đề này như thế nào?
constructorkhông nhận đối số- Tất cả các liên kết
@eventđều tự động liên kết vớithis thistrong phần lớn các trường hợp đề cập đến thông tin tham chiếu của phần tử tuỳ 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 viên của lớp. Thao tác này sẽ 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 hồi
Các khái niệm chính đằng sau Hook 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 có trạng thái, chia các thành phần thành các phần nhỏ hơn, mô-đun hơn, cũng như kết 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à một 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ủ lưu trữ bộ điều khiển như LitElement.
Vòng đời của ReactiveController và reactiveControllerHost 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 tạo một bộ điều khiển phản ứng và đính kèm bộ điều khiển đó vào một máy chủ lưu trữ bằng addController, vòng đời của bộ điều khiển sẽ được gọi cùng với vòng đời của máy chủ lưu trữ. Ví dụ: hãy nhớ 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:
- Nó hiển thị "Hello World! It is" (Đó là) rồi hiển thị thời gian
- Đồng hồ sẽ cập nhật từng giây
- Khi được tháo, thành phần này sẽ xoá khoảng thời gian gọi đánh dấu
Xây dựng cấu trúc thành phần
Trước tiên, hãy bắt đầu bằng 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ể tạo bộ điều khiển phản ứng theo bất kỳ cách nào miễn là bộ điều khiển đó dùng chung giao diện ReactiveController, nhưng việc sử dụng một lớp có constructor có thể nhận giao diện ReactiveControllerHost cũng như mọi thuộc tính khác cần thiết để khởi động bộ điều khiển là một mẫu mà nhóm Lit muốn sử dụng cho hầu hết các trường hợp cơ bản.
Giờ đây, bạn cần dịch các lệnh gọi lại trong vòng đời của React thành các lệnh gọi lại của bộ điều khiển. Tóm lại:
componentDidMount- Đến
connectedCallbackcủa LitElement - Đến
hostConnectedcủa bộ điều khiển
- Đến
ComponentWillUnmount- Đến
disconnectedCallbackcủa LitElement - Đến
hostDisconnectedcủa bộ điều khiển
- Đến
Để biết thêm thông tin về cách chuyển đổi vòng đời của React sang vòng đời của 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ư trong ví dụ ở phần 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 bộ đ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 bộ điều khiển, bạn cần tạo thực thể bộ điều khiển bằng cách truyền một tham chiếu đến máy chủ lưu trữ bộ điều khiển (là thành phần <my-element>), rồi sử dụng bộ điều khiển trong phương thức render.
Kích hoạt quá trình kết xuất lại trong bộ điều khiển
Lưu ý rằng thời gian sẽ xuất hiện nhưng không được cập nhật. Điều này là do bộ điều khiển đang đặt ngày mỗi giây, nhưng máy chủ không cập nhật. Lý do là date đang thay đổi trên lớp ClockController chứ không phải thành phần nữa. Điều này có nghĩa là sau khi date được đặt trên bộ điều khiển, bạn cần yêu cầu máy chủ 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ồ sẽ bắt đầu đếm ngược!
Để so sánh kỹ lưỡng hơn về các trường hợp sử dụng phổ biến với hook, vui lòng xem phần Chủ đề nâng cao – Hook.
8. Thiếu nhi
Trong phần này, bạn sẽ tìm hiểu cách sử dụng các vị trí để quản lý phần tử con trong Lit.
Máy đánh bạc và trẻ em
Các vị trí cho phép tạo thành phần 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 prop. Vị trí mặc định là props.children và hàm render xác định vị trí của vị trí mặc định. Ví dụ:
const MyArticle = (props) => {
return <article>{props.children}</article>;
};
Xin lưu ý rằng props.children là các Thành phần React chứ không phải các phần tử HTML.
Trong Lit, các thành phần con được tạo trong hàm kết xuất bằng các phần tử slot. 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, các thành phần con là HTMLElements được đính kèm vào các vị trí. Tệp đính kèm này có tên là Projection (Dự đoán).
@customElement("my-article")
export class MyArticle extends LitElement {
render() {
return html`
<article>
<slot></slot>
</article>
`;
}
}
Nhiều vị trí
Trong React, việc thêm nhiều vị trí về cơ bản cũng giống như việc kế thừa nhiều thuộc tính.
const MyArticle = (props) => {
return (
<article>
<header>
{props.headerChildren}
</header>
<section>
{props.sectionChildren}
</section>
</article>
);
};
Tương tự, việc thêm nhiều phần tử <slot> sẽ tạo ra nhiều vị trí hơn trong Lit. Nhiều vị trí được xác định bằng thuộc tính name: <slot name="slot-name">. Điều này cho phép các thành phần con khai báo vị trí mà chúng 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 ô
Các vị trí sẽ hiển thị cây con của chúng khi không có nút nào được chiếu vào vị trí đó. Khi các nút được chiếu vào một vị trí, vị trí đó sẽ không hiển thị cây con mà 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ẻ em vào các vị trí
Trong React, các thành phần con được chỉ định cho các vị trí thông qua 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 prop headerChildren và sectionChildren.
const MyNewsArticle = () => {
return (
<MyArticle
headerChildren={<h3>Extry, Extry! Read all about it!</h3>}
sectionChildren={<p>Children are props in React!</p>}
/>
);
};
Trong Lit, các phần tử con được chỉ định cho các vị trí 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ị trí mặc định (ví dụ: <slot>) và không có vị trí nào có thuộc tính name (ví dụ: <slot name="foo">) khớp với thuộc tính slot của các thành phần con trong thành phần tuỳ chỉnh (ví dụ: <div slot="foo">), thì nút đó sẽ không được chiếu và sẽ không hiển thị.
9. Refs
Đôi khi, nhà phát triển có thể cần 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 các 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 thành 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à kết xuất HTMLElements.
Trong React, Ref 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ẽ làm như sau:
- Kết xuất một trường nhập văn bản trống và một nút có văn bản
- Tập trung vào dữ liệu đầu vào khi nhấp vào nút
Sau lần hiển thị ban đầu, React sẽ đặt inputRef.current thành HTMLInputElement được tạo thông qua thuộc tính ref.
Thích "Tài liệu tham khảo" bằng @query
Lit nằm gần trình duyệt và tạo ra một lớp trừu tượng rất mỏng so với các tính năng gốc của trình duyệt.
Tương đương với refs trong Lit là HTMLElement do các trình trang trí @query và @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 sẽ làm những việc sau:
- Xác định một thuộc tính trên
MyElementbằng cách sử dụng trình trang trí@query(tạo một phương thức getter choHTMLInputElement). - 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 vào nội dung đầu vào khi nhấp vào nút
Trong JavaScript, các đối tượng trang trí @query và @queryAll lần lượt thực hiện querySelector và querySelectorAll. Đây là phiên bản JavaScript tương đương của @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 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 được tìm thấy trong gốc kết xuất. Phương thức này sẽ trả về null nếu @query không tìm thấy phần tử được 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 trung gian
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.
Thành phần có thể sử dụng lại
React mô phỏng cá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ẹ cung cấp trạng thái cho các thành phần con thông qua các thuộc tính. Các thành phần con giao tiếp với thành phần mẹ thông qua các lệnh gọi lại có trong props.
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 sẽ 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ó +step hoặc -step làm nhãn
- Cập nhật thành phần mẹ bằng cách gọi
props.addToCountervớiprops.steplàm đối số khi nhấp vào
Mặc dù bạn 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. Bạn có thể viết Thành phần React trong ví dụ trên 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, một 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-countermang giá trịstepcủa phần tử khi nhấp vào
Các sự kiện trên trình duyệt sẽ truyền từ các phần tử con lên phần tử mẹ. Các sự kiện cho phép trẻ phát sóng 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, bạn hiếm khi thấy các Thành phần React gử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 thường dùng các 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 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>Σ: {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 một 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. CounterButtonsử dụngaddToCounterđể cập nhậtcounttheosteptrên mỗi lượt nhấp.
Bạn có thể triển khai MyCounter tương tự trong Lit. Lưu ý rằng 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 một 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>Σ ${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
addToCountervới trình nghe sự kiện@update-counter - Cập nhật
countbằng cách thêm giá trị có trongdetail.stepcủa sự kiệnupdate-counter - Đặt giá trị
stepcủacounter-buttonthông qua thuộc tínhstep
Việc sử dụng các thuộc tính phản ứng trong Lit để truyền tin về các thay đổi từ phần tử mẹ đến phần tử con là cách thông thường hơn. Tương tự, bạn nên sử dụng hệ thống sự kiện của trình duyệt để truyền 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à cung cấp khả năng hỗ trợ nhiều nền tảng cho các thành phần web.
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
Lit cung cấp nhiều cách để tạo kiểu cho các phần tử cũng như một giải pháp tích hợp.
Kiểu cùng dòng
Lit hỗ trợ các kiểu nội tuyến 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 mộ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 toái, vì vậy Lit cung cấp một chỉ thị để giúp bạn giải quyết vấn đề này.
styleMap
Chỉ thị styleMap giúp bạn dễ dàng sử dụng JavaScript để đặt kiểu nội 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ị một
h1có đường viền và công cụ chọn màu - Thay đổi
border-colorthành giá trị trong công cụ chọn màu
Ngoài ra, còn có styleMap dùng để đặt kiểu cho 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 sử dụng chuỗi ký tự mẫu được gắn thẻ css để tạo kiểu cho các thành phần.
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 một chuỗi ký tự mẫu được gắn thẻ CSS bằng một liên kết
- Đặt màu cho 2
h1có mã nhận dạng
Lợi ích của việc 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 phiên bản
- Được triển khai với mục tiêu tái sử dụng mô-đun
- Có thể dễ dàng 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 CSS
Ngoài ra, hãy lưu ý thẻ <style> trong index.html:
<!-- index.html -->
<style>
h1 {
color: red !important;
}
</style>
Lit sẽ đặt phạm vi cho kiểu của các thành phần theo gốc của chúng. Điều này có nghĩa là các kiểu sẽ không bị rò rỉ vào và ra. Để truyền các kiểu sang thành phần, nhóm Lit đề xuất sử dụng CSS Custom Properties (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 chúng trong các mẫu, chúng sẽ được phân tích cú pháp cho mỗi phiên bản thành phần thay vì cho mỗi lớp như trường hợp với mẫu được gắn thẻ css. Ngoài ra, quá trình loại bỏ các CSSResult trùng lặp của trình duyệt diễn ra nhanh hơn nhiều.
Thẻ liên kết
Bạn cũng có thể sử dụng <link rel="stylesheet"> trong mẫu cho các kiểu, nhưng bạn cũng không nên làm như vậy vì điều này có thể gây ra hiện tượng nhấp nháy ban đầu của nội dung chưa được tạo kiểu (FOUC).
12. Chủ đề nâng cao (không bắt buộc)
JSX và mẫu
Lit và DOM ảo
Lit-html không có DOM ảo thông thường để so sánh từng nút riêng lẻ. Thay vào đó, nó sử dụng các tính năng hiệu suất vốn có trong thông số tagged template literal của ES2015. Tagged template literal là các chuỗi template literal có các hàm thẻ được đính kèm.
Sau đây là một ví dụ về chuỗi ký tự mẫu:
const str = 'string';
console.log(`This is a template literal ${str}`);
Dưới đây là ví dụ về một chuỗi ký tự 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ề một lệnh gọi của một chuỗi ký tự mẫu được gắn thẻ.
Rất nhiều điều kỳ diệu về hiệu suất trong Lit đến từ việc các mảng chuỗi được truyền vào hàm thẻ có cùng con trỏ (như minh hoạ trong console.log thứ hai). Trình duyệt không tạo lại mảng strings mới trên mỗi lệnh gọi hàm thẻ, vì trình duyệt đang sử dụng cùng một chuỗi mẫu (tức là ở cùng một vị trí trong AST). Vì vậy, hoạt động liên kết, phân tích cú pháp và lưu vào bộ nhớ đệm mẫu của Lit có thể tận dụng những tính năng này mà không tốn nhiều chi phí khác biệt về thời gian chạy.
Hành vi trình duyệt tích hợp sẵn này của các chuỗi ký tự mẫu được gắn thẻ mang lại cho Lit một lợi thế đáng kể về hiệu suất. Hầu hết các DOM ảo thông thường đều thực hiện phần lớn công việc của mình bằng JavaScript. Tuy nhiên, các chuỗi ký tự mẫu được gắn thẻ thực hiện hầu hết các thao tác 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 chuỗi theo nghĩa đen của mẫu được gắn thẻ HTML với React hoặc Preact, thì nhóm Lit đề xuất thư viện htm.
Mặc dù vậy, như trường hợp với trang web Google Codelabs và một số trình chỉnh sửa mã trực tuyến, bạn sẽ nhận thấy rằng tính năng làm nổi bật cú pháp chuỗi ký tự 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 tính nă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 hợp tác chặt chẽ với cộng đồng để duy trì các dự án như lit-plugin. Đây là một trình bổ trợ VS Code sẽ thêm tính năng làm nổi bật cú pháp, kiểm tra kiểu và tính năng tự động hoàn thành cho các dự án Lit của bạn.
Lit và JSX + React DOM
JSX không chạy trong trình duyệt mà sử dụng một trình tiền xử lý để chuyển đổi JSX thành các lệnh gọi hàm JavaScript (thường là thông qua Babel).
Ví dụ: Babel sẽ chuyển đổi mã này:
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 sẽ lấy đầu ra của React và chuyển đổi đầu ra đó thành DOM thực tế – các thuộc tính, thuộc tính, trình nghe sự kiện và mọi thứ.
Lit-html sử dụng các chuỗi ký tự mẫu được gắn thẻ có thể chạy trong trình duyệt mà không cần chuyển đổi hoặc bộ tiền xử lý. Điều này có nghĩa là để bắt đầu với Lit, bạn chỉ cần một tệp HTML, một tập lệnh mô-đun ES và một máy chủ. Sau đây là một tập lệnh hoàn toàn có thể chạy trên 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, nên kích thước của Lit 2 dưới 5 KB khi được rút gọn và nén gzip so với 40 KB khi được rút gọn và nén gzip của React (2, 8 KB) + react-dom (39, 4 KB).
Sự kiện
React sử dụng một hệ thống sự kiện giả tạo. Đ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 nào để 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 rồi á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 nhà phát triển kém khi tích hợp các thư viện không có React, do đó, bạn 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 quá trình phân tích cú pháp thời gian chạy hơn được thực hiệ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ụ
Các thành phần React và 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ủa nó. Các phần tử tuỳ chỉnh có một số điểm đánh đổi giữa các thành phần React khi nói đến việc tạo thành phần (trạng thái và vòng đời sẽ được thảo luận thêm trong phần Trạng thái và vòng đời).
Một số ưu điểm của Custom Elements khi là một hệ thống thành phần:
- Là tính năng gốc của 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ừ
innerHTMLvàdocument.createElementđếnquerySelector - Thường có thể dùng trên nhiều khung
- Có thể được đăng ký một cách lười biếng bằng
customElements.definevà "hydrate" DOM
Một số nhược điểm của Custom Elements so với các thành phần React:
- Không thể tạo một phần tử tuỳ chỉnh mà không xác định một lớp (do đó, không có thành phần chức năng tương tự 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ề quy cách thẻ tự đóng, đó là lý do tại sao các quy cách mới hơn có xu hướng không bao gồm thẻ tự đóng
- Giới thiệu một nút bổ sung vào cây DOM có thể gây ra các vấn đề về bố cục
- Phải được đăng ký thông qua JavaScript
Lit đã sử dụng các phần tử tuỳ chỉnh thay vì hệ thống phần tử riêng biệt vì các phần tử tuỳ chỉnh được tích hợp vào trình duyệt và nhóm Lit tin rằng lợi ích của việc sử dụng nhiều khung hình lớn hơn 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 trong không gian lit-ssr đã khắc phục được các vấn đề chính với việc đăng ký JavaScript. Ngoài ra, một số công ty như GitHub tận dụng tính năng đăng ký trì hoãn phần tử tuỳ chỉnh để tăng cường dần các trang bằng những điểm nhấn 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ề các 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 của phần tử chỉ có thể được ghi dưới dạng chuỗi. Mặc dù Lit sẽ truyền các thuộc tính chuỗi đến các loại được xác định, nhưng các thành phần 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ụ: giả sử bạn có đị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>`;
}
}
Một thuộc tính phản ứng nguyên thuỷ num được xác định 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 giới thiệu bằng attribute:false sẽ huỷ kích hoạt tính năng xử lý thuộc tính của Lit.
Đây là cách truyền dữ liệu đến 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 của React
static getDerivedStateFromProps
Không có thành phần tương đương trong Lit vì cả props và state đều là các thuộc tính lớp giống nhau
shouldComponentUpdate
- Đơn vị 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ự như
shouldComponentUpdatecủa React
getSnapshotBeforeUpdate
Trong Lit, getSnapshotBeforeUpdate tương tự như cả update và willUpdate
willUpdate
- Đã gọi trước
update - Không giống như
getSnapshotBeforeUpdate,willUpdateđược gọi trướcrender - Các thay đổi đối với thuộc tính phản ứng trong
willUpdatekhông kích hoạt lại chu kỳ cập nhật - Nơi 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 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ướcrender - Các thay đổi đối với các thuộc tính phản ứng trong
updatesẽ không kích hoạt lại chu kỳ cập nhật nếu trước khi gọisuper.update - Đây là vị trí phù hợp để thu thập thông tin từ DOM xung quanh thành phần trước khi đầu ra được kết xuất được chuyển đến DOM
- Phương thức này không được gọi trên máy chủ trong SSR
Các lệnh gọi lại khác trong vòng đời của 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ả observedAttributes và attributeChangedCallback đều là một phần của quy cách thành phần tuỳ chỉnh và được Lit triển khai dưới phần 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 đến một 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 là một phần của thông số kỹ thuật về các thành phần 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
Đây là những phương thức và thuộc tính là thành viên lớp mà bạn có thể gọi, ghi đè hoặc chờ để giúp thao tác quy trình vòng đời.
updateComplete
Đây là một Promise sẽ phân giải khi phần tử đã cập nhật xong vì các vòng đời cập nhật và kết xuất là 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à phương thức cần được ghi đè để tuỳ chỉnh thời điểm 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à các chu kỳ kết xuất của chúng 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 là phương thức gọi các lệnh gọi lại trong vòng đời cập nhật. Thông thường, bạn không cần thực hiện việc này, trừ những trường hợp hiếm gặp mà bạn phải cập nhật đồ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 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 về các phần tử tuỳ chỉnh, thuộc tính này sẽ là true nếu phần tử hiện được đính kèm vào cây tài liệu chính.
Hình ảnh trực quan về vòng đời cập nhật Lit
Vòng đời cập nhật có 3 phần:
- Trước khi cập nhật
- Cập nhật
- Sau khi cập nhật
Trước khi cập nhật

Sau requestUpdate, một bản cập nhật theo lịch sẽ được triển khai.
Cập nhật

Sau khi cập nhật

Khoảnh khắc níu chân
Lý do nên dùng đoạn lôi cuốn
Các 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 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. Mặc dù vậy, 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à một giải pháp dựa trên lớp như bộ điều khiển phản ứng có xu hướng nổi bật.
Các lệnh gọi và bộ điều khiển yêu cầu API
Bạn thường viết một hook yêu cầu dữ liệu từ một API. Ví dụ: hãy xem thành phần hàm React này. Thành phần này thực hiện những việc sau:
index.tsx- Kết xuất văn bản
- Kết xuất phản hồi
- của
- Mã nhận dạng người dùng + Tên người dùng
- Thông báo lỗi
- Lỗi 404 khi đạt đến người dùng thứ 11 (theo thiết kế)
- Lỗi huỷ nếu quá trình tìm nạp API bị huỷ
- Đang tải thông báo
useAPI - Kết xuất một 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ỷ tìm nạp API và hiển thị lỗi
useApi.tsx- Xác định một hook tuỳ chỉnh
useApi - Sẽ tìm nạp không đồng bộ một đối tượng người dùng từ một API
- Phát ra:
- Tên người dùng
- Liệu quá trình tìm nạp có đang tải hay không
- Mọi thông báo lỗi
- Một lệnh gọi lại để huỷ tìm nạp
- Huỷ các lệnh tìm nạp đang diễn ra nếu bị huỷ gắn kết
- Xác định một hook tuỳ chỉnh
Dưới đây là quy trình triển khai Lit + Reactive Controller.
Những điểm cần nhớ:
- Reactive Controller giống với các hook tuỳ chỉnh nhất
- 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ữauseEffectvàuseCallback - Lit sử dụng một thuộc tính lớp riêng tư
- Về cơ bản, React đang mô phỏng hành vi của một thuộc tính lớp riêng tư
- React sử dụng
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 lại muốn có cùng môi trường không cần bản dựng của Lit, thì nhóm Lit rất khuyến khích bạn dùng thư viện Haunted.
Thiếu nhi
Vị trí mặc định
Khi các phần tử HTML không được chỉ định thuộc tính slot, chúng sẽ được chỉ định cho khe cắm mặc định không tên. Trong ví dụ bên dưới, MyApp sẽ chèn một đoạn văn vào một vị trí có tên. Đoạn văn còn lại sẽ được đặt mặc định là "vị trí 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>
`;
}
}
Thông tin cập nhật về vị trí
Khi cấu trúc của các thành phần con của khe thay đổi, sự kiện slotchange sẽ được kích hoạt. Một thành phần Lit có thể liên kết một trình nghe sự kiện với một sự kiện slotchange. Trong ví dụ bên dưới, khe cắm đầu tiên được 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>
`;
}
}
Refs
Tạo tệp đối chiếu
Cả Lit và React đều hiển thị một tham chiếu đến 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 tạo thành DOM, sau đó được trả về thông qua một trình trang trí @query Lit hoặc một Tham chiếu React.
React là một quy trình chức năng tạo ra các Thành phần React chứ không phải HTMLElements. Vì Ref được khai báo trước khi HTMLElement được kết xuất, nên một khoảng trống trong bộ nhớ sẽ được phân bổ. Đó là lý do 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 kết xuất), tức là useRef(null).
Sau khi ReactDOM chuyển đổi một Thành phần React thành HTMLElement, nó sẽ tìm một thuộc tính có tên là ref trong ReactComponent. Nếu có, ReactDOM sẽ đặt giá trị tham chiếu của HTMLElement vào ref.current.
LitElement sử dụng hàm thẻ mẫu html từ lit-html để tạo một Template Element (Phần tử mẫu) ở chế độ nền. LitElement đóng dấu nội dung của mẫu vào DOM tối của một phần tử tuỳ chỉnh sau khi kết xuất. Shadow DOM là một cây DOM có phạm vi đượ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 sẽ thực hiện một this.shadowRoot.querySelector trên gốc có phạm vi.
Truy vấn nhiều phần tử
Trong ví dụ bên dưới, trình trang trí @queryAll sẽ trả về hai đoạ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 sẽ tạo một phương thức getter cho paragraphs, phương thức này sẽ trả về kết quả của this.shadowRoot.querySelectorAll(). Trong JavaScript, bạn có thể khai báo một phương thức getter để thực hiện cùng mục đích:
get paragraphs() {
return this.renderRoot.querySelectorAll('p');
}
Các phần tử thay đổi truy vấn
Đối tượng 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 thấy 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 promise sẽ phân giải khi đoạn văn bản đầu tiên có sẵ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 trung gian
Trong React, quy ước là sử dụng lệnh gọi lại vì trạng thái được điều chỉnh bởi chính React. React cố gắng hết sức để không dựa vào trạng thái do các phần tử cung cấp. DOM chỉ là một hiệu ứng của quy trình kết xuất.
Trạng thái bên ngoài
Bạn có thể sử dụng Redux, MobX 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 Lit được tạo trong phạm vi trình duyệt. Vì vậy, mọi thư viện cũng có trong phạm vi trình duyệt đều có sẵn cho Lit. Nhiều thư viện tuyệt vời đã được xây dựng để tận dụng các hệ thống quản lý trạng thái hiện có trong Lit.
Đây là một loạt bài của Vaadin giải thích cách tận dụng Redux trong một thành phần Lit.
Hãy xem lit-mobx của Adobe để biết cách một trang web quy mô lớn có thể tận dụng MobX 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 trình duyệt gốc và 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 đều có thể được dùng trong thành phần Lit.
Định kiểu
DOM ảo
Để đóng gói các kiểu và DOM một cách tự nhiên trong một Phần tử tuỳ chỉnh, Lit sử dụng Shadow DOM. Shadow Root tạo một cây bóng tách biệt với cây tài liệu chính. Điều này có nghĩa là hầu hết các kiểu đều được giới hạn trong tài liệu này. Một số kiểu 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ữ.
DOM bóng cũng giới thiệu các khái niệm và bộ chọn mới cho quy cách 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ẻ các 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
Các gốc bóng đổ gây ra một chút khó khăn cho việc tạo giao diện thông thường, thường là các phương pháp thẻ kiểu từ trên xuống. Cách thông thường để giải quyết vấn đề tạo giao diện bằng Thành phần web sử dụng Shadow DOM là hiển thị một API kiểu thông qua CSS Custom Properties. Ví dụ: đây là một 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 sẽ 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ử dụng tính năng tạo giao diện từ trên xuống và không thể hiển thị các kiểu, thì bạn luôn có thể tắt DOM bóng bằng cách ghi đè createRenderRoot để trả về this. Thao tác này sẽ hiển thị mẫu của các thành phần vào chính phần tử tuỳ chỉnh thay vì vào một gốc bóng được đính kèm vào phần tử tuỳ chỉnh. Với cách này, bạn sẽ mất: tính năng đóng gói kiểu, tính năng đóng gói DOM và các khe cắm.
Sản xuất
IE 11
Nếu cần hỗ trợ các trình duyệt cũ như IE 11, bạn sẽ phải tải một số polyfill có dung lượng khoảng 33 KB. 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át hai gói riêng biệt, 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:
- Việc phân phát ES 6 sẽ nhanh hơn và phục vụ hầu hết khách hàng của bạn
- ES 5 được chuyển đổi mã nguồn 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 của cả hai
- Hỗ trợ IE 11
- Không bị 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 một gói được phân phối có điều kiện trên trang tài liệu của chúng tôi tại đây.