Phần tử tuỳ chỉnh là một lớp được đăng ký với tên thẻ thông qua customElements.define(). Việc đăng ký và vòng đời cơ bản đã được hiểu rõ; các quy tắc dưới đây nhắm đến những phần thường được triển khai không chính xác.
Vòng đời: các phần được triển khai không chính xác
Bản thân các lệnh gọi lại được ghi lại đầy đủ; sau đây là những quan niệm sai lầm gây ra lỗi:
connectedCallbackcó thể chạy nhiều lần. Hàm này kích hoạt mỗi khi chèn, vì vậy, việc di chuyển phần tử sẽ chạy lại hàm này. Thiết lập idempotent — không giả định một lần chạy đầu tiên.connectedMoveCallbackbỏ qua cặp ngắt kết nối/kết nối lại khi di chuyển. Khi một phần tử được di dời bằngNode.moveBefore(), lệnh gọi lại này sẽ kích hoạt thay vìdisconnectedCallback→connectedCallback, vì vậy, trạng thái, tiêu điểm và chế độ phát iframe/nội dung nghe nhìn sẽ vẫn tồn tại sau khi di chuyển. Xác định chú thích này khi việc thiết lập hoặc chia nhỏ gây tốn kém tài nguyên hệ thống hoặc chạy lại một cách rõ ràng.- Tear down in
disconnectedCallback. Xoá trình nghe, bộ hẹn giờ và trình quan sát chung, nếu không bạn sẽ bị rò rỉ; nhưng phần tử có thể được chèn lại, vì vậy đừng coi việc chia nhỏ là vĩnh viễn. - Gọi
super()trước trong hàm khởi tạo và không phụ thuộc vào việc có toàn bộ DOM. Nếu gọisuper()sau khi chạm vàothis, thì bạn sẽ nhận đượcReferenceError. Các thuộc tính thường (nhưng không phải lúc nào cũng) xuất hiện; danh sách dài các phần tử con có thể chưa phân tích cú pháp xong. Sử dụngconnectedCallbackđể khởi chạy một lần,slotchangeđể theo dõi các phần tử con trực tiếp vàattributeChangedCallbackđể theo dõi các thuộc tính.connectedCallbackvẫn có thể kích hoạt trước khi tất cả phần tử con được phân tích cú pháp, vì vậy, đừng bỏ qua các lệnh gọislotchange/attributeChangedCallbacktiếp theo. attributeChangedCallbackchỉ kích hoạt choobservedAttributes, và nên duy trì mức giá rẻ cho các thuộc tính không ảnh hưởng đến kết quả.- Các hook vòng đời và
observedAttributesđược chụp nhanh khi đăng ký.customElements.define()đọc cả hai một lần; việc thêm một hook hoặc mở rộngobservedAttributestrên lớp sau lệnh gọi sẽ bị bỏ qua mà không có thông báo. Cụ thể,attributeChangedCallbackkhông bao giờ kích hoạt trừ phiobservedAttributesđã xuất hiện tại thời điểmdefine().
Thời gian nâng cấp và :defined
Một phần tử có thể tồn tại trong DOM trước khi lớp của phần tử đó được đăng ký (trong HTML do máy chủ kết xuất hoặc khi tập lệnh tải muộn). Cho đến khi được xác định, đây là một HTMLElement không hoạt động và không có hành vi nào của bạn; khi customElements.define() chạy, trình duyệt sẽ nâng cấp mọi phần tử trùng khớp đã có trong tài liệu, chạy hàm khởi tạo và connectedCallback sau đó.
Hai hậu quả mà các mô hình thường bỏ qua:
- Thuộc tính và phần tử con có thể đã xuất hiện khi hàm khởi tạo/
connectedCallbackcủa bạn chạy trong quá trình nâng cấp. Đọc các thuộc tính hiện có trongconnectedCallbackđể đồng bộ hoá trạng thái ban đầu và không bao giờ giả định rằng phần tử bắt đầu trống. - Phần tử con vẫn có thể bị thiếu ngay cả trong
connectedCallback— trình phân tích cú pháp HTML có thể tạo ra danh sách phần tử con dài ở giữa quá trình phân tích cú pháp, vì vậy, lệnh gọi lại đôi khi chạy trước khi mọi phần tử con được đính kèm. Xem dấu đầu dòng hàm khởi tạo trong phần Vòng đời ở trên để biết mẫu (slotchange+attributeChangedCallback).
Ẩn các phần tử không xác định để tránh nội dung không được định kiểu nhấp nháy — nhưng không mở, vì vậy nội dung vẫn có thể truy cập được nếu tập lệnh không bao giờ tải. Tạo ảnh động cho opacity có độ trễ thay vì ẩn hoàn toàn bằng visibility: hidden, để phần tử tự hiển thị sau khi hết thời gian chờ ngay cả khi không bao giờ được nâng cấp:
my-element:not(:defined) {
opacity: 0;
/* Reveal anyway after 2s, so a failed or slow script never hides content for good. */
animation: defined-fallback 0.2s 2s forwards;
}
@keyframes defined-fallback { to { opacity: 1; } }
Hãy đặt phạm vi cho các rào chắn này cho những phần tử cần đến chúng, hoặc cho một cây con cụ thể thông qua :has(:not(:defined)), thay vì toàn bộ body. Một tiện ích chậm, không quan trọng (ví dụ: nút chia sẻ) không được làm chậm quá trình hiển thị cho toàn bộ trang.
Phản ánh thuộc tính ⇄ thuộc tính
Giữ cho thuộc tính HTML và thuộc tính JS đồng bộ để phần tử có thể sử dụng được từ cả mã đánh dấu và tập lệnh. Giá trị thuộc tính luôn là chuỗi; hãy ép kiểu cho các giá trị này.
class MyToggle extends HTMLElement {
static observedAttributes = ['label', 'count', 'disabled'];
// String attribute reflected via getter/setter.
get label() { return this.getAttribute('label') ?? ''; }
set label(v) {
v ? this.setAttribute('label', v) : this.removeAttribute('label');
}
// Numeric attribute: coerce from string on read, serialize on write.
get count() { return Number(this.getAttribute('count')) || 0; }
set count(v) { this.setAttribute('count', String(v)); }
// Boolean attribute: present = true, absent = false. Never set it to "false".
get disabled() { return this.hasAttribute('disabled'); }
set disabled(on) { this.toggleAttribute('disabled', !!on); }
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal !== newVal) this.render();
}
}
Nơi lưu trữ giá trị
Ví dụ trên giữ DOM làm nguồn đáng tin cậy – mỗi phương thức getter đều đọc getAttribute. Đó là giá trị mặc định phù hợp cho một số thuộc tính đơn giản, giống như chuỗi: chỉ có một nơi mà trạng thái có thể tồn tại, vì vậy, trạng thái không thể không đồng bộ.
Đối với các thành phần phong phú hơn, hãy giữ nguồn đáng tin cậy trong trường JS và chỉ phản ánh ra bên ngoài một thuộc tính khi có thứ khác phải nhìn thấy thuộc tính đó — nhắm mục tiêu CSS hoặc người dùng đọc thuộc tính đó. Việc đọc một trường sẽ tiết kiệm hơn nhiều so với việc phân tích cú pháp và ép buộc một thuộc tính trong mỗi lần truy cập, đồng thời các giá trị không phải chuỗi sẽ không bao giờ được chuyển đổi qua lại thông qua quá trình chuyển đổi tuần tự. Đây là lý do các thư viện như Lit giữ trạng thái trong JS và phản ánh có chọn lọc thay vì coi các thuộc tính là bộ nhớ: chỉ phản ánh những gì thế giới bên ngoài phải quan sát, chứ không phải mọi thuộc tính.
Lỗi:
observedAttributeskỷ luật: chỉ liệt kê các thuộc tính mà thay đổi của chúng phải hiển thị lại hoặc kích hoạt tác dụng phụ. Việc quan sát mọi thứ sẽ thêm lệnh gọi lại vào mọi đột biến mà không mang lại lợi ích gì.- Vòng lặp phản chiếu: phương thức setter ghi một thuộc tính sẽ nhập lại
attributeChangedCallback. Bảo vệ bằng quy trình kiểm traoldVal !== newVal, như trên. - Trạng thái chỉ có kiểu: đối với trạng thái nội bộ chỉ tồn tại để tạo kiểu (đang tải, đang hoạt động, đã mở rộng), hãy ưu tiên trạng thái tuỳ chỉnh hơn là phản ánh thuộc tính tổng hợp – xem phần Trạng thái tuỳ chỉnh bên dưới. Chỉ phản ánh một thuộc tính khi người dùng phải có thể đặt trạng thái từ mã đánh dấu.
Trạng thái tuỳ chỉnh
Trạng thái tuỳ chỉnh là cờ boolean mà một phần tử hiển thị về chính nó để CSS nhắm đến, được đặt thông qua ElementInternals và khớp với lớp giả :state(). Sử dụng các trạng thái này cho trạng thái giao diện người dùng nội bộ, trong thời gian chạy (busy, expanded, invalid) thay vì phản ánh một thuộc tính tổng hợp: các trạng thái này không thể xung đột với thuộc tính của người dùng, không bao giờ xuất hiện trong DOM và chỉ có thể đọc từ bên ngoài (người dùng có thể tạo kiểu cho :state() nhưng không thể giả mạo trạng thái này).
class AsyncButton extends HTMLElement {
#internals;
#action = async () => {};
constructor() {
super();
this.#internals = this.attachInternals();
}
set action(fn) { this.#action = fn; } // host supplies the work to run
async run() {
if (this.#internals.states.has('busy')) return; // guard re-entry
this.#internals.states.add('busy');
this.#internals.states.delete('success');
try {
await this.#action();
this.#internals.states.add('success');
} catch {
this.#internals.states.add('error');
} finally {
this.#internals.states.delete('busy');
}
}
}
CustomStateSet là một tập hợp, vì vậy, các trạng thái độc lập sẽ kết hợp (busy cộng với error) mà không cần tạo enum data-status kết hợp.
Trạng thái tuỳ chỉnh chỉ có trong thời gian chạy. Trạng thái này không được chuyển đổi tuần tự sang DOM, vì vậy, trạng thái này sẽ không tồn tại khi tải lại, khi phần tử được tạo lại hoặc khi hiển thị máy chủ. Nếu trạng thái phải được đặt theo cách khai báo trong mã đánh dấu hoặc duy trì trong các lần tải, thì đó là công việc của một thuộc tính (xem phần Nơi lưu trữ giá trị) – khôi phục trạng thái đó trong connectedCallback từ nguồn đáng tin cậy của bạn. Nguyên tắc chung: trạng thái tuỳ chỉnh cho những gì thành phần lấy được và sở hữu trong thời gian chạy và chỉ cần tạo kiểu; một thuộc tính cho những gì phải được đặt từ mã đánh dấu hoặc được chuyển đổi tuần tự.
Để tạo kiểu cho các trạng thái này (cả bên trong thành phần và từ biểu định kiểu của người dùng), hãy xem bài viết Tạo kiểu cho thành phần web.
Quy ước đặt tên
- Trạng thái riêng tư thực sự: các trường
#gốc (#count) để đóng gói cứng; tiền tố_chỉ khi các lớp con vẫn phải truy cập được. Lưu ý rằng các trường#không được kế thừa — một lớp con không thể thấy các trường riêng tư của lớp cơ sở. - Thuộc tính có khoá ký hiệu là một giải pháp trung gian: ẩn khỏi kiểu liệt kê thông thường, nhưng có thể truy cập được bằng bất kỳ mã nào chứa ký hiệu (xuất ký hiệu hoặc hiển thị ký hiệu dưới dạng trường
statictrên lớp). Vì mỗi ký hiệu là duy nhất, nên hai ký hiệu có cùng nội dung mô tả sẽ không bao giờ xung đột. Điều này rất hữu ích khi một lớp con hoặc lớp đồng cấp cần quyền truy cập có kiểm soát mà các trường#không thể cung cấp. - Không che khuất các thuộc tính chung (
style,class,id,slot,part,title,lang,dir) bằng các thuộc tính của riêng bạn; làm như vậy sẽ phá vỡ hành vi tích hợp của các thuộc tính đó. Lưu ý rằngdisabledchỉ áp dụng trên toàn cục cho các thành phần điều khiển biểu mẫu; trên các phần tử khác, bạn phải tự triển khai hiệu ứng của nó.
Khi đặt tên cho một nền tảng API mới, hãy tuân theo quy ước của nền tảng: sử dụng kiểu viết kebab-case chữ thường có ít nhất một dấu gạch nối cho tên thẻ phần tử tuỳ chỉnh (ví dụ: my-toggle), lowerCamelCase cho các thuộc tính và phương thức, đồng thời đồng bộ hoá tên thuộc tính chữ thường với các thuộc tính tương ứng.