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ó
- 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à Công cụ cho nhà phát triển Chrome.
- Có kiến thức về React
- (Nâng cao) Nếu bạn 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 sẽ cần lit-plugin cho VS Code và kháng nghị API.
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.
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.
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-analyzer
và lit-plugin
.
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ử đó.
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
Ả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.
- Bộ chọn tệp. Ghi chú nút dấu cộng...
- Trình chỉnh sửa tệp.
- Bản xem trước mã.
- Nút tải lại.
- 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)
- Sau đây là ví dụ về
package.json
- Sau đây là ví dụ về
- 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ụngnpm run dev
- Nếu bạn đang sử dụng ví dụ
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
disabled
vì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 để đặ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>
vì 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ặccustomElements.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ặcstring
, 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 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 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 trongfirstUpdated
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 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 đượ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ướcfirstUpdated
, 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 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 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
đếnsuper
- 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ớithis
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 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 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
- Đến
ComponentWillUnmount
- Đến
disconnectedCallback
của LitElement - Đến
hostDisconnected
của bộ điều khiển
- Đế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 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, 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
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 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 choHTMLInputElement
). - 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
và @queryAll
lần lượt thực hiện querySelector
và querySelectorAll
. Đâ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ớiprops.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>Σ: {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ụngaddToCounter
để cập nhậtcount
theostep
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>Σ ${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 trongdetail.step
của sự kiệnupdate-counter
- Đặt giá trị
step
củacounter-button
thông qua thuộc tínhstep
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.
Thẻ liên kết
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ừ
innerHTML
vàdocument.createElement
đếnquerySelector
- 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ả update
và willUpdate
willUpdate
- Được 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
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ướcrender
- 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ọisuper.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ả observedAttributes
và attributeChangedCallback
đề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
Sau requestUpdate
, hệ thống sẽ chờ bản cập nhật theo lịch.
Cập nhật
Sau khi cập nhật
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
- Xác định một trình bổ trợ tuỳ chỉnh
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ữauseEffect
vàuseCallback
- 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ư
- 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 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.