Từ thành phần web đến phần tử lit

1. Giới thiệu

Lần cập nhật gần đây nhất: ngày 10 tháng 08 năm 2021

Thành phần web

Thành phần web là một bộ API nền tảng web cho phép bạn tạo thẻ HTML đóng gói, có thể sử dụng lại và tuỳ chỉnh mới để sử dụng trong trang web và ứng dụng web. Các thành phần và tiện ích tuỳ chỉnh được xây dựng theo các tiêu chuẩn của Thành phần web sẽ hoạt động trên các trình duyệt hiện đại và có thể dùng với bất kỳ thư viện hoặc khung JavaScript nào hoạt động với HTML.

Lit là gì

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

Lit cung cấp các API để đơn giản hoá các tác vụ phổ biến của Thành phần web như quản lý thuộc tính, thuộc tính và hiển thị.

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

  • Thành phần web là gì
  • Khái niệm về Thành phần web
  • Cách tạo một Thành phần web
  • lit-html và LitElement là gì
  • Tác dụng của Lit trên một thành phần web

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

  • Thành phần web thích / không thích vanilla
  • Thành phần web dựa trên Lit thích / không thích

Bạn cần có

  • Mọi trình duyệt hiện đại được cập nhật (Chrome, Safari, Firefox, Chromium Edge). Thành phần web hoạt động trong mọi trình duyệt hiện đại và bạn có thể sử dụng tính năng polyfill cho Microsoft Internet Explorer 11 và Microsoft Edge không phải Chromium.
  • Có kiến thức về HTML, CSS, JavaScript và Công cụ của Chrome cho nhà phát triển.

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

Truy cập mã

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

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

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

// after
import './my-file.js';
import 'https://unpkg.com/lit?module';

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

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

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

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

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

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

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

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

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

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

3. Xác định phần tử tuỳ chỉnh

Phần tử tùy chỉnh

Thành phần web là một tập hợp 4 API web gốc. Các yếu tố này là:

  • Mô-đun ES
  • Phần tử tùy chỉnh
  • DOM bóng
  • Mẫu HTML

Bạn đã sử dụng quy cách của mô-đun ES, cho phép bạn tạo các mô-đun javascript với tính năng nhập và xuất được tải vào trang bằng <script type="module">.

Xác định Phần tử tùy chỉnh

Thông số kỹ thuật của Phần tử tùy chỉnh cho phép người dùng xác định các phần tử HTML của riêng họ bằng JavaScript. Tên phải chứa dấu gạch nối (-) để phân biệt với các thành phần gốc của trình duyệt. Xoá tệp index.js và xác định một lớp phần tử tuỳ chỉnh:

index.js

class RatingElement extends HTMLElement {}

customElements.define('rating-element', RatingElement);

Một phần tử tuỳ chỉnh được xác định bằng cách liên kết một lớp mở rộng HTMLElement với tên thẻ có dấu gạch nối. Lệnh gọi đến customElements.define yêu cầu trình duyệt liên kết lớp RatingElement với tagName ‘rating-element'. Điều này có nghĩa là mọi phần tử trong tài liệu của bạn có tên <rating-element> sẽ được liên kết với lớp này.

Đặt <rating-element> vào nội dung tài liệu và xem nội dung nào kết xuất.

index.html

<body>
 <rating-element></rating-element>
</body>

Bây giờ, khi xem kết quả đầu ra, bạn sẽ thấy rằng không có nội dung nào được kết xuất. Đây là kết quả bình thường vì bạn chưa cho trình duyệt biết cách kết xuất <rating-element>. Bạn có thể xác nhận rằng định nghĩa Phần tử tùy chỉnh đã thành công bằng cách chọn <rating-element> trong Công cụ dành cho nhà phát triển Chrome bộ chọn phần tử và trong bảng điều khiển, gọi:

$0.constructor

Kết quả nào sẽ xuất ra:

class RatingElement extends HTMLElement {}

Vòng đời của phần tử tuỳ chỉnh

Phần tử tuỳ chỉnh đi kèm với một tập hợp các hook trong vòng đời. Các yếu tố này là:

  • constructor
  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

constructor được gọi khi phần tử được tạo lần đầu tiên, chẳng hạn như bằng cách gọi document.createElement(‘rating-element') hoặc new RatingElement(). Hàm dựng là nơi phù hợp để thiết lập phần tử của bạn, nhưng thường được coi là phương pháp không hợp lệ khi thực hiện các thao tác DOM trong hàm khởi tạo cho phần tử "khởi động" hiệu suất.

connectedCallback được gọi khi phần tử tuỳ chỉnh được đính kèm vào DOM. Đây thường là nơi các thao tác DOM ban đầu xảy ra.

disconnectedCallback được gọi sau khi phần tử tuỳ chỉnh bị xoá khỏi DOM.

attributeChangedCallback(attrName, oldValue, newValue) được gọi khi bất kỳ thuộc tính nào do người dùng chỉ định thay đổi.

adoptedCallback được gọi khi phần tử tuỳ chỉnh được áp dụng từ documentFragment khác vào tài liệu chính qua adoptNode, chẳng hạn như trong HTMLTemplateElement.

Hiển thị DOM

Bây giờ, hãy quay lại phần tử tuỳ chỉnh và liên kết một số DOM với phần tử đó. Đặt nội dung của phần tử khi phần tử được đính kèm vào DOM:

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   this.innerHTML = `
     <style>
       rating-element {
         display: inline-flex;
         align-items: center;
       }
       rating-element button {
         background: transparent;
         border: none;
         cursor: pointer;
       }
     </style>
     <button class="thumb_down" >
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
     </button>
     <span class="rating">${this.rating}</span>
     <button class="thumb_up">
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
     </button>
   `;
 }
}

customElements.define('rating-element', RatingElement);

Trong constructor, bạn lưu trữ một thuộc tính thực thể có tên là rating trên phần tử. Trong connectedCallback, bạn thêm thành phần con DOM vào <rating-element> để cho thấy điểm xếp hạng hiện tại, cùng với các nút thích và không thích.

4. DOM bóng

Tại sao nên sử dụng DOM tối?

Ở bước trước, bạn sẽ nhận thấy rằng bộ chọn trong thẻ kiểu mà bạn đã chèn sẽ chọn bất kỳ yếu tố xếp hạng nào trên trang cũng như bất kỳ nút nào. Điều này có thể làm cho kiểu bị rò rỉ ra khỏi phần tử và chọn các nút khác mà bạn có thể không có ý định tạo kiểu. Ngoài ra, các kiểu khác bên ngoài phần tử tuỳ chỉnh này có thể vô tình tạo kiểu cho các nút bên trong phần tử tuỳ chỉnh của bạn. Ví dụ: thử đặt một thẻ kiểu vào phần đầu của tài liệu chính:

index.html

<!DOCTYPE html>
<html>
 <head>
   <script src="./index.js" type="module"></script>
   <style>
     span {
       border: 1px solid red;
     }
   </style>
 </head>
 <body>
   <rating-element></rating-element>
 </body>
</html>

Dữ liệu đầu ra của bạn phải có một hộp đường viền màu đỏ bao quanh khoảng thời gian dành cho điểm xếp hạng. Đây là một trường hợp không đáng kể, nhưng việc thiếu đóng gói DOM có thể dẫn đến các vấn đề lớn hơn cho các ứng dụng phức tạp hơn. Đây là lúc Shadow DOM xuất hiện.

Đính kèm gốc bóng

Đính kèm một Shadow Root vào phần tử và kết xuất DOM bên trong gốc đó:

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   const shadowRoot = this.attachShadow({mode: 'open'});

   shadowRoot.innerHTML = `
     <style>
       :host {
         display: inline-flex;
         align-items: center;
       }
       button {
         background: transparent;
         border: none;
         cursor: pointer;
       }
     </style>
     <button class="thumb_down" >
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
     </button>
     <span class="rating">${this.rating}</span>
     <button class="thumb_up">
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
     </button>
   `;
 }
}

customElements.define('rating-element', RatingElement);

Khi làm mới trang, bạn sẽ thấy rằng kiểu trong tài liệu chính không thể chọn các nút bên trong Shadow Root nữa.

Bạn đã thực hiện việc này như thế nào? Trong connectedCallback, bạn đã gọi this.attachShadow để đính kèm một gốc bóng (shadow) vào một phần tử. Chế độ open có nghĩa là nội dung bóng có thể kiểm tra được, đồng thời cho phép truy cập vào gốc bóng đổ qua this.shadowRoot. Ngoài ra, hãy xem Thành phần web trong trình kiểm tra Chrome:

Cây dom trong trình kiểm tra chrome. Có một <rating-element> với a#shadow-root (mở) làm thành phần con và DOM từ trước bên trong shadowroot đó.

Bây giờ, bạn sẽ thấy một gốc bóng có thể mở rộng chứa nội dung. Mọi thứ bên trong gốc bóng đổ đó được gọi là DOM bóng. Nếu chọn phần tử xếp hạng trong Công cụ cho nhà phát triển của Chrome và gọi $0.children, thì bạn sẽ thấy phần tử này không trả về phần tử con nào. Lý do là DOM bóng không được xem là một phần của cùng một cây DOM với phần tử con trực tiếp, mà được xem là Cây tối.

DOM sáng

Thử nghiệm: thêm một nút làm phần tử con trực tiếp của <rating-element>:

index.html

<rating-element>
 <div>
   This is the light DOM!
 </div>
</rating-element>

Làm mới trang và bạn sẽ thấy rằng nút DOM mới này trong Light DOM của Phần tử tùy chỉnh này không hiển thị trên trang. Lý do là DOM bóng có các tính năng kiểm soát cách chiếu các nút DOM sáng vào vùng bóng đổ thông qua các phần tử <slot>.

5. Mẫu HTML

Lý do nên dùng Mẫu

Việc sử dụng innerHTML và các chuỗi giá trị cố định của mẫu không được dọn dẹp có thể gây ra các vấn đề bảo mật với tính năng chèn tập lệnh. Trước đây, các phương thức bao gồm việc sử dụng DocumentFragment, nhưng chúng cũng đi kèm với các vấn đề khác như tải hình ảnh và tập lệnh chạy khi mẫu được xác định, cũng như gây ra các trở ngại cho việc tái sử dụng. Đây là nơi phần tử <template> xuất hiện; mẫu cung cấp DOM trơ, một phương thức có hiệu suất cao để sao chép các nút và tạo mẫu có thể tái sử dụng.

Sử dụng Mẫu

Tiếp theo, hãy chuyển đổi thành phần này để sử dụng Mẫu HTML:

index.html

<body>
 <template id="rating-element-template">
   <style>
     :host {
       display: inline-flex;
       align-items: center;
     }
     button {
       background: transparent;
       border: none;
       cursor: pointer;
     }
   </style>
   <button class="thumb_down" >
     <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
   </button>
   <span class="rating"></span>
   <button class="thumb_up">
     <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
   </button>
 </template>

 <rating-element>
   <div>
     This is the light DOM!
   </div>
 </rating-element>
</body>

Tại đây, bạn đã chuyển nội dung DOM vào một thẻ mẫu trong DOM của tài liệu chính. Bây giờ, hãy tái cấu trúc định nghĩa phần tử tuỳ chỉnh:

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   const shadowRoot = this.attachShadow({mode: 'open'});
   const templateContent = document.getElementById('rating-element-template').content;
   const clonedContent = templateContent.cloneNode(true);
   shadowRoot.appendChild(clonedContent);

   this.shadowRoot.querySelector('.rating').innerText = this.rating;
 }
}

customElements.define('rating-element', RatingElement);

Để sử dụng phần tử mẫu này, bạn cần truy vấn mẫu, lấy nội dung của mẫu đó rồi sao chép các nút đó bằng templateContent.cloneNode, trong đó đối số true thực hiện việc sao chép sâu. Sau đó, bạn sẽ khởi tạo dom bằng dữ liệu.

Xin chúc mừng, giờ bạn đã có Thành phần web! Rất tiếc, ứng dụng này chưa có tính năng gì, vì vậy, tiếp theo, hãy thêm một số chức năng.

6. Thêm chức năng

Liên kết thuộc tính

Hiện tại, cách duy nhất để đặt điểm xếp hạng cho phần tử điểm xếp hạng là tạo phần tử, đặt thuộc tính rating trên đối tượng, sau đó đưa thuộc tính đó lên trang. Thật không may, đây không phải là cách các phần tử HTML gốc có xu hướng hoạt động. Các phần tử HTML gốc có xu hướng cập nhật khi cả thuộc tính và thuộc tính thay đổi.

Để phần tử tuỳ chỉnh cập nhật chế độ xem khi thuộc tính rating thay đổi bằng cách thêm các dòng sau:

index.js

constructor() {
  super();
  this._rating = 0;
}

set rating(value) {
  this._rating = value;
  if (!this.shadowRoot) {
    return;
  }

  const ratingEl = this.shadowRoot.querySelector('.rating');
  if (ratingEl) {
    ratingEl.innerText = this._rating;
  }
}

get rating() {
  return this._rating;
}

Bạn thêm một phương thức setter và getter cho thuộc tính điểm xếp hạng, sau đó cập nhật văn bản của phần tử điểm xếp hạng nếu có sẵn. Điều này có nghĩa là nếu bạn đặt thuộc tính điểm xếp hạng trên phần tử này, thì khung hiển thị sẽ cập nhật; hãy kiểm tra nhanh trong bảng điều khiển Công cụ dành cho nhà phát triển của bạn!

Liên kết thuộc tính

Bây giờ, hãy cập nhật khung hiển thị khi thuộc tính thay đổi; điều này tương tự như dữ liệu đầu vào cập nhật khung hiển thị khi bạn đặt <input value="newValue">. May mắn thay, vòng đời của Thành phần web bao gồm attributeChangedCallback. Cập nhật điểm xếp hạng bằng cách thêm các dòng sau:

index.js

static get observedAttributes() {
 return ['rating'];
}

attributeChangedCallback(attributeName, oldValue, newValue) {
 if (attributeName === 'rating') {
   const newRating = Number(newValue);
   this.rating = newRating;
 }
}

Để attributeChangedCallback kích hoạt, bạn phải đặt một phương thức getter tĩnh cho RatingElement.observedAttributes which defines the attributes to be observed for changes. Sau đó, bạn đặt mức phân loại theo công bố trong DOM. Hãy thử:

index.html

<rating-element rating="5"></rating-element>

Điểm xếp hạng hiện sẽ được cập nhật theo cách rõ ràng!

Chức năng của nút

Giờ đây, tất cả những gì còn thiếu là chức năng của nút. Hành vi của thành phần này phải cho phép người dùng đưa ra một lượt bình chọn lên hoặc xuống và đưa ra phản hồi bằng hình ảnh cho người dùng. Bạn có thể triển khai việc này với một số trình nghe sự kiện và thuộc tính phản ánh, nhưng trước tiên, hãy cập nhật các kiểu để đưa ra phản hồi bằng hình ảnh bằng cách thêm các dòng sau:

index.html

<style>
...

 :host([vote=up]) .thumb_up {
   fill: green;
 }
  :host([vote=down]) .thumb_down {
   fill: red;
 }
</style>

Trong DOM bóng, bộ chọn :host đề cập đến nút hoặc phần tử tuỳ chỉnh mà Shadow Root được đính kèm. Trong trường hợp này, nếu thuộc tính vote"up" thì nút thích sẽ chuyển sang màu xanh lục, nhưng nếu vote"down", then it will turn the thumb-down button red. Bây giờ, hãy triển khai logic cho việc này bằng cách tạo thuộc tính / thuộc tính phản ánh cho vote tương tự như cách bạn triển khai rating. Bắt đầu bằng phương thức setter thuộc tính và getter:

index.js

constructor() {
  super();
  this._rating = 0;
  this._vote = null;
}

set vote(newValue) {
  const oldValue = this._vote;
  if (newValue === oldValue) {
    return;
  }
  if (newValue === 'up') {
    if (oldValue === 'down') {
      this.rating += 2;
    } else {
      this.rating += 1;
    }
  } else if (newValue === 'down') {
    if (oldValue === 'up') {
      this.rating -= 2;
    } else {
      this.rating -= 1;
    }
  }
  this._vote = newValue;
  this.setAttribute('vote', newValue);
}

get vote() {
  return this._vote;
}

Bạn khởi tạo thuộc tính thực thể _vote bằng null trong constructor và trong phương thức setter, bạn kiểm tra xem giá trị mới có khác không. Nếu có, bạn sẽ điều chỉnh điểm xếp hạng cho phù hợp và quan trọng là phản ánh thuộc tính vote trở lại máy chủ lưu trữ bằng this.setAttribute.

Tiếp theo, hãy thiết lập liên kết thuộc tính:

index.js

static get observedAttributes() {
  return ['rating', 'vote'];
}

attributeChangedCallback(attributeName, oldValue, newValue) {
  if (attributeName === 'rating') {
    const newRating = Number(newValue);

    this.rating = newRating;
  } else if (attributeName === 'vote') {
    this.vote = newValue;
  }
}

Xin nhắc lại, đây là quy trình tương tự mà bạn đã thực hiện với liên kết thuộc tính rating; bạn thêm vote vào observedAttributes rồi đặt thuộc tính vote trong attributeChangedCallback. Cuối cùng, hãy thêm một số trình nghe sự kiện nhấp chuột để cung cấp chức năng của các nút!

index.js

constructor() {
 super();
 this._rating = 0;
 this._vote = null;
 this._boundOnUpClick = this._onUpClick.bind(this);
 this._boundOnDownClick = this._onDownClick.bind(this);
}

connectedCallback() {
  ...
  this.shadowRoot.querySelector('.thumb_up')
    .addEventListener('click', this._boundOnUpClick);
  this.shadowRoot.querySelector('.thumb_down')
    .addEventListener('click', this._boundOnDownClick);
}

disconnectedCallback() {
  this.shadowRoot.querySelector('.thumb_up')
    .removeEventListener('click', this._boundOnUpClick);
  this.shadowRoot.querySelector('.thumb_down')
    .removeEventListener('click', this._boundOnDownClick);
}

_onUpClick() {
  this.vote = 'up';
}

_onDownClick() {
  this.vote = 'down';
}

Trong constructor, bạn liên kết một số trình nghe lượt nhấp với phần tử này và giữ lại các tham chiếu xung quanh. Trong connectedCallback, bạn nghe các sự kiện nhấp chuột trên các nút. Trong disconnectedCallback, bạn dọn dẹp những trình nghe này và trên chính trình nghe lượt nhấp, bạn đã thiết lập vote một cách thích hợp.

Xin chúc mừng, giờ đây bạn đã có một Thành phần web với đầy đủ tính năng; hãy thử nhấp vào một vài nút! Vấn đề bây giờ là tệp JS của tôi hiện đã đạt 96 dòng, tệp HTML 43 dòng và mã khá chi tiết và bắt buộc đối với một thành phần đơn giản như vậy. Đây chính là lúc dự án Lit của Google ra đời!

7. Văn bản html

Điểm kiểm tra mã

Tại sao lại là lit-html

Trước hết, thẻ <template> hữu ích và có hiệu suất cao, nhưng không được đóng gói với logic của thành phần, do đó gây khó khăn cho việc phân phối mẫu với phần còn lại của logic. Ngoài ra, cách sử dụng các phần tử mẫu vốn đã cho vay bắt buộc đối với mã bắt buộc, trong nhiều trường hợp, dẫn đến mã khó đọc hơn so với các mẫu mã khai báo.

Đây là lúc lit-html cần đến! Lit html là hệ thống hiển thị của Lit cho phép bạn viết các mẫu HTML trong JavaScript, sau đó kết xuất và kết xuất lại các mẫu đó cùng với dữ liệu một cách hiệu quả để tạo và cập nhật DOM. Nó tương tự như các thư viện JSX và VDOM phổ biến nhưng chạy tự nhiên trong trình duyệt và hiệu quả hơn nhiều trong nhiều trường hợp.

Sử dụng Lit HTML

Tiếp theo, hãy di chuyển Thành phần web gốc rating-element để sử dụng mẫu Lit. Mẫu này sử dụng Các giá trị cố định mẫu được gắn thẻ. Đây là các hàm lấy chuỗi mẫu làm đối số có cú pháp đặc biệt. Sau đó, Lit sẽ sử dụng các thành phần mẫu để nâng cao khả năng kết xuất nhanh cũng như cung cấp một số tính năng dọn dẹp nhằm tăng cường bảo mật. Hãy bắt đầu bằng cách di chuyển <template> trong index.html sang mẫu Lit bằng cách thêm phương thức render() vào thành phần web:

chỉ mục.js

// Dont forget to import from Lit!
import {render, html} from 'lit';

class RatingElement extends HTMLElement {
  ...
  render() {
    if (!this.shadowRoot) {
      return;
    }

    const template = html`
      <style>
        :host {
          display: inline-flex;
          align-items: center;
        }
        button {
          background: transparent;
          border: none;
          cursor: pointer;
        }

       :host([vote=up]) .thumb_up {
         fill: green;
       }

       :host([vote=down]) .thumb_down {
         fill: red;
       }
      </style>
      <button class="thumb_down">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
      </button>
      <span class="rating">${this.rating}</span>
      <button class="thumb_up">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
      </button>`;

    render(template, this.shadowRoot);
  }
}

Bạn cũng có thể xoá mẫu của mình khỏi index.html. Trong phương thức kết xuất này, bạn xác định một biến có tên là template và gọi hàm giá trị cố định của mẫu được gắn thẻ html. Bạn cũng sẽ nhận thấy rằng mình đã thực hiện một liên kết dữ liệu đơn giản bên trong phần tử span.rating bằng cách sử dụng cú pháp nội suy giá trị cố định của mẫu ${...}. Điều này có nghĩa là sau cùng, bạn sẽ không bắt buộc phải cập nhật nút đó nữa. Ngoài ra, bạn sẽ gọi phương thức render đã chiếu sáng để kết xuất đồng bộ mẫu vào gốc bóng.

Di chuyển sang cú pháp khai báo

Giờ đây, khi bạn đã loại bỏ phần tử <template>, hãy tái cấu trúc mã để gọi phương thức render mới được xác định. Bạn có thể bắt đầu bằng cách tận dụng liên kết trình nghe sự kiện của lit để xoá mã trình nghe:

index.js

<button
    class="thumb_down"
    @click=${() => {this.vote = 'down'}}>
...
<button
    class="thumb_up"
    @click=${() => {this.vote = 'up'}}>

Các mẫu văn bản có thể thêm trình nghe sự kiện vào một nút có cú pháp liên kết @EVENT_NAME. Trong trường hợp này, bạn sẽ cập nhật thuộc tính vote mỗi khi người dùng nhấp vào các nút này.

Tiếp theo, hãy dọn dẹp mã khởi chạy trình nghe sự kiện trong constructorconnectedCallbackdisconnectedCallback:

chỉ mục.js

constructor() {
  super();
  this._rating = 0;
  this._vote = null;
}

connectedCallback() {
  this.attachShadow({mode: 'open'});
  this.render();
}

// remove disonnectedCallback and _onUpClick and _onDownClick

Bạn có thể xoá logic trình nghe lượt nhấp khỏi cả 3 lệnh gọi lại và thậm chí xoá hoàn toàn disconnectedCallback! Bạn cũng có thể xoá tất cả mã khởi tạo DOM khỏi connectedCallback, làm cho mã này trông thanh lịch hơn nhiều. Điều này cũng có nghĩa là bạn có thể loại bỏ phương thức trình nghe _onUpClick_onDownClick!

Cuối cùng, hãy cập nhật phương thức setter thuộc tính để sử dụng phương thức render sao cho dom có thể cập nhật khi các thuộc tính hoặc thuộc tính thay đổi:

index.js

set rating(value) {
  this._rating = value;
  this.render();
}

...

set vote(newValue) {
  const oldValue = this._vote;
  if (newValue === oldValue) {
    return;
  }

  if (newValue === 'up') {
    if (oldValue === 'down') {
      this.rating += 2;
    } else {
      this.rating += 1;
    }
  } else if (newValue === 'down') {
    if (oldValue === 'up') {
      this.rating -= 2;
    } else {
      this.rating -= 1;
    }
  }

  this._vote = newValue;
  this.setAttribute('vote', newValue);
  // add render method
  this.render();
}

Tại đây, bạn có thể xoá logic cập nhật dom khỏi phương thức setter rating và đã thêm một lệnh gọi đến render qua phương thức setter vote. Giờ đây, mẫu sẽ dễ đọc hơn nhiều vì bạn có thể thấy vị trí áp dụng các liên kết và trình nghe sự kiện.

Hãy làm mới trang. Nút này sẽ hoạt động được như khi người dùng nhấn vào nút tán thành!

Thanh trượt xếp hạng thích và xuống có giá trị là 6 và ngón cái có màu xanh lục

8. LitElement

Tại sao nên sử dụng LitElement

Mã này vẫn còn một số vấn đề. Trước tiên, nếu bạn thay đổi thuộc tính hoặc thuộc tính vote thì việc này có thể thay đổi thuộc tính rating, dẫn đến việc gọi render hai lần. Mặc dù các lệnh gọi kết xuất lặp lại về cơ bản là không hoạt động và hiệu quả, nhưng máy ảo JavaScript vẫn đang dành thời gian để gọi hàm đó hai lần một cách đồng bộ. Thứ hai, việc thêm các thuộc tính và thuộc tính mới là một việc tẻ nhạt vì đòi hỏi nhiều mã nguyên mẫu. Đây là lúc LitElement xuất hiện!

LitElement là lớp cơ sở của Lit để tạo các Thành phần web nhanh, nhẹ có thể dùng trên các khung và môi trường. Tiếp theo, hãy xem những việc LitElement có thể làm cho chúng ta trong rating-element bằng cách thay đổi cách triển khai để sử dụng!

Sử dụng LitElement

Bắt đầu bằng cách nhập và phân lớp con lớp cơ sở LitElement từ gói lit:

index.js

import {LitElement, html, css} from 'lit';

class RatingElement extends LitElement {
// remove connectedCallback()
...

Bạn sẽ nhập LitElement, đây là lớp cơ sở mới cho rating-element. Tiếp theo, bạn giữ nguyên dữ liệu nhập html và cuối cùng là css để cho phép chúng ta xác định các giá trị cố định của mẫu có gắn thẻ css cho toán học css, tạo mẫu và các tính năng khác nâng cao.

Tiếp theo, di chuyển các kiểu từ phương thức kết xuất sang biểu định kiểu tĩnh của Lit:

index.js

class RatingElement extends LitElement {
  static get styles() {
    return css`
      :host {
        display: inline-flex;
        align-items: center;
      }
      button {
        background: transparent;
        border: none;
        cursor: pointer;
      }

      :host([vote=up]) .thumb_up {
        fill: green;
      }

      :host([vote=down]) .thumb_down {
        fill: red;
      }
    `;
  }
 ...

Đây là nơi hầu hết các kiểu đều xuất hiện ở dạng Lit. Lit sẽ lấy các kiểu này và sử dụng các tính năng của trình duyệt như Biểu định kiểu có thể tạo để cung cấp thời gian hiển thị nhanh hơn cũng như chuyển biểu định kiểu thông qua polyfill Thành phần web trên các trình duyệt cũ hơn nếu cần.

Lifecycle

Lit giới thiệu một tập hợp các phương thức gọi lại trong vòng đời kết xuất bên cạnh các lệnh gọi lại Thành phần web gốc. Các lệnh gọi lại này được kích hoạt khi các thuộc tính Lit được khai báo thay đổi.

Để sử dụng tính năng này, bạn phải khai báo tĩnh thuộc tính nào sẽ kích hoạt vòng đời kết xuất.

chỉ mục.js

static get properties() {
  return {
    rating: {
      type: Number,
    },
    vote: {
      type: String,
      reflect: true,
    }
  };
}

// remove observedAttributes() and attributeChangedCallback()
// remove set rating() get rating()

Ở đây, bạn xác định rằng ratingvote sẽ kích hoạt vòng đời kết xuất LitElement, cũng như xác định các loại sẽ dùng để chuyển đổi thuộc tính chuỗi thành các thuộc tính.

<user-profile .name=${this.user.name} .age=${this.user.age}>
  ${this.user.family.map(member => html`
        <family-member
             .name=${member.name}
             .relation=${member.relation}>
        </family-member>`)}
</user-profile>

Ngoài ra, cờ reflect trên thuộc tính vote sẽ tự động cập nhật thuộc tính vote của phần tử lưu trữ mà bạn đã kích hoạt theo cách thủ công trong phương thức setter vote.

Giờ đây, khi đã có khối thuộc tính tĩnh, bạn có thể xoá tất cả logic cập nhật hiển thị thuộc tính và thuộc tính. Điều này có nghĩa là bạn có thể xoá các phương thức sau:

  • connectedCallback
  • observedAttributes
  • attributeChangedCallback
  • rating (phương thức setter và getter)
  • vote (phương thức setter và getter nhưng giữ logic thay đổi từ phương thức setter)

Những gì bạn giữ lại là constructor cũng như thêm một phương thức vòng đời willUpdate mới:

index.js

constructor() {
  super();
  this.rating = 0;
  this.vote = null;
}

willUpdate(changedProps) {
  if (changedProps.has('vote')) {
    const newValue = this.vote;
    const oldValue = changedProps.get('vote');

    if (newValue === 'up') {
      if (oldValue === 'down') {
        this.rating += 2;
      } else {
        this.rating += 1;
      }
    } else if (newValue === 'down') {
      if (oldValue === 'up') {
        this.rating -= 2;
      } else {
        this.rating -= 1;
      }
    }
  }
}

// remove set vote() and get vote()

Ở đây, bạn chỉ cần khởi chạy ratingvote, rồi di chuyển logic setter vote vào phương thức vòng đời willUpdate. Phương thức willUpdate được gọi trước render bất cứ khi nào thuộc tính cập nhật được thay đổi, vì LitElement thay đổi theo lô thuộc tính và khiến việc hiển thị không đồng bộ. Các thay đổi đối với thuộc tính phản ứng (như this.rating) trong willUpdate sẽ không kích hoạt các lệnh gọi vòng đời render không cần thiết.

Cuối cùng, render là một phương thức vòng đời LitElement yêu cầu chúng ta phải trả về mẫu Lit:

index.js

render() {
  return html`
    <button
        class="thumb_down"
        @click=${() => {this.vote = 'down'}}>
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
    </button>
    <span class="rating">${this.rating}</span>
    <button
        class="thumb_up"
        @click=${() => {this.vote = 'up'}}>
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
    </button>`;
}

Bạn không còn phải kiểm tra thư mục gốc của bóng đổ và cũng không còn phải gọi hàm render đã nhập trước đó từ gói 'lit'.

Phần tử của bạn sẽ hiển thị trong bản xem trước ngay bây giờ; hãy nhấp vào!

9. Xin chúc mừng

Xin chúc mừng! Bạn đã tạo thành công một Thành phần web từ đầu và phát triển nó thành một LitElement!

Văn bản siêu nhỏ (< 5kb rút gọn + nén), siêu nhanh và thực sự thú vị khi viết mã! Bạn có thể tạo các thành phần để các khung khác sử dụng hoặc bạn có thể xây dựng ứng dụng hoàn chỉnh bằng thành phần đó!

Giờ đây, bạn đã biết Thành phần web là gì, cách tạo cũng như cách Lit giúp xây dựng thành phần đó dễ dàng hơn!

Điểm kiểm tra mã

Bạn có muốn kiểm tra mã cuối cùng của bạn so với mã của chúng tôi không? Hãy so sánh tại đây.

Tiếp theo là gì?

Hãy tham khảo một số lớp học lập trình khác!

Tài liệu đọc thêm

Cộng đồng