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 8 năm 2021

Thành phần web

Thành phần web là một nhóm API nền tảng web cho phép bạn tạo các thẻ HTML tuỳ chỉnh, có thể sử dụng lại và được đóng gói để dùng trong các 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 dựa trên các tiêu chuẩn Thành phần web sẽ hoạt động trên các trình duyệt hiện đại và có thể được dùng với mọi thư viện hoặc khung JavaScript 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 nhanh, gọn nhẹ, hoạt động trong mọi khung hoặc không có khung nào. Với Lit, bạn có thể tạo các thành phần, ứng dụng, hệ thống thiết kế có thể chia sẻ và nhiều nội dung khác.

Lit cung cấp các API để đơn giản hoá các tác vụ Web Components phổ biến như quản lý thuộc tính, thuộc tính và kết xuất.

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

  • Web Component là gì
  • Các khái niệm về Web Component
  • Cách tạo một Thành phần web
  • lit-html và LitElement là gì
  • Những gì Lit làm trên một thành phần web

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

  • Thành phần Web thuần có nút thích / không thích
  • Web Component dựa trên Lit có biểu tượng 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). Web Components hoạt động trong tất cả các trình duyệt hiện đại và có sẵn polyfill cho Microsoft Internet Explorer 11 và Microsoft Edge không phải Chromium.
  • Có kiến thức về HTML, CSS, JavaScript và Chrome DevTools.

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

Truy cập vào mã

Trong suốt lớp học lập trình, sẽ có các đường liên kết đến Lit Playground như sau:

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

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

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

Bạn có thể thực hiện toàn bộ hướng dẫn trong Lit Playground, sử dụng các điểm kiểm tra này làm điểm bắt đầu. Nếu đang dùng VS Code, bạn có thể sử dụng các điểm kiểm tra này để tải mã khởi đầu cho bất kỳ bước nào, cũng như sử dụng các điểm kiểm tra này để kiểm tra bài làm của mình.

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

Thanh thẻ bộ chọn tệp được gắn nhãn là 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 bản xem trước là Phần 4

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

  1. Bộ chọn tệp. Lưu ý 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 VS Code (Nâng cao)

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

  • Kiểm tra loại mẫu
  • Tính năng gợi ý thông minh và tự động hoàn thành mẫu

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

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

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

Phần tử tuỳ chỉnh

Thành phần web là một tập hợp gồm 4 API web gốc. Các loại chiến dịch phụ đó là:

  • Mô-đun ES
  • Phần tử tuỳ chỉnh
  • DOM ảo
  • Mẫu HTML

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

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

Quy cách Phần tử tuỳ 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 ngang (-) để phân biệt với các phần tử trình duyệt gốc. 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 ngang. 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 một <rating-element> vào nội dung tài liệu và xem nội dung nào sẽ hiển thị.

index.html

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

Bây giờ, khi xem đầu ra, bạn sẽ thấy rằng không có gì được kết xuất. Điều này là 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ử tuỳ chỉnh đã thành công bằng cách chọn <rating-element> trong bộ chọn phần tử của Chrome Dev Tools và trong bảng điều khiển, hãy gọi:

$0.constructor

Kết quả sẽ là:

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 nhóm các hook vòng đời. Các loại chiến dịch phụ đó là:

  • constructor
  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

constructor được gọi khi phần tử được tạo lần đầu tiên: ví dụ: bằng cách gọi document.createElement(‘rating-element') hoặc new RatingElement(). Hàm khởi tạo là nơi thích hợp để thiết lập phần tử, nhưng thường được coi là phương pháp không hay khi thực hiện các thao tác DOM trong hàm khởi tạo vì lý do hiệu suất "khởi động" phần 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 diễn ra các thao tác DOM ban đầu.

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 có 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 chuyển từ một documentFragment khác vào tài liệu chính thông 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 các phần tử con DOM vào <rating-element> để hiển thị điểm xếp hạng hiện tại, cùng với nút thích và không thích.

4. DOM ảo

Tại sao lại là DOM tối?

Ở bước trước, bạn sẽ nhận thấy rằng các bộ chọn trong thẻ kiểu mà bạn đã chèn sẽ chọn mọi phần tử xếp hạng trên trang cũng như mọi nút. Điều này có thể khiến các 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 muốn tạo kiểu. Ngoài ra, các kiểu khác bên ngoài thành phần tuỳ chỉnh này có thể vô tình tạo kiểu cho các nút bên trong thành phần tuỳ chỉnh của bạn. Ví dụ: hãy thử đặt thẻ kiểu trong 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>

Đầu ra của bạn phải có một hộp đường viền màu đỏ xung quanh khoảng thời gian 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 tính đóng gói DOM có thể dẫn đến các vấn đề lớn hơn đối với các ứng dụng phức tạp hơn. Đây là lúc Shadow DOM phát huy tác dụng.

Đính kèm một Shadow Root

Đí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ẽ nhận thấy rằng các kiểu trong tài liệu chính không còn chọn được các nút bên trong Shadow Root nữa.

Bạn đã làm điều này như thế nào? Trong connectedCallback, bạn đã gọi this.attachShadow để đính kèm một gốc bóng vào một phần tử. Chế độ open có nghĩa là nội dung bóng có thể kiểm tra và giúp gốc bóng có thể truy cập thông qua this.shadowRoot. 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> có#shadow-root (open) làm phần tử con và DOM từ trước bên trong shadowroot đó.

Giờ đây, 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, bạn sẽ thấy rằng phần tử này không trả về phần tử con nào. Điều này là do DOM tối không được coi là một phần của cùng một cây DOM như các phần tử con trực tiếp mà là Cây bóng.

Light DOM

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 nút DOM mới này trong DOM nhẹ của Phần tử tuỳ chỉnh này không xuất hiện trên trang. Lý do là vì Shadow DOM có các tính năng kiểm soát cách các nút Light DOM được chiếu vào shadow DOM thông qua các phần tử <slot>.

5. Mẫu HTML

Lý do nên sử dụng Mẫu

Việc sử dụng innerHTML và chuỗi ký tự theo nghĩa đen của mẫu mà không có quy trình dọn dẹp có thể gây ra các vấn đề bảo mật liên quan đến việc 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 những phương thức này cũng đi kèm với các vấn đề khác như hình ảnh tải và tập lệnh chạy khi các mẫu được xác định cũng như gây ra trở ngại cho khả năng sử dụng lại. Đây là nơi xuất hiện phần tử <template>; các mẫu cung cấp DOM trơ, một phương thức hiệu suất cao để sao chép các nút và tạo mẫu có thể dùng lại.

Sử dụng mẫu

Tiếp theo, chuyển đổi thành phần để 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>

Ở đây, bạn đã di 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 truy vấn mẫu, lấy nội dung của mẫu và sao chép các nút đó bằng templateContent.cloneNode, trong đó đối số true thực hiện một bản sao sâu. Sau đó, bạn khởi động dom bằng dữ liệu.

Xin chúc mừng! Bạn hiện đã có một Thành phần web! Rất tiếc, thao tác này chưa thực hiện hành động nào, 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 trên phần tử xếp hạng là tạo phần tử, đặt thuộc tính rating trên đối tượng, rồi đặt phần tử đó trên trang. Rất tiếc, đây không phải là cách các phần tử HTML gốc thường hoạt động. Các phần tử HTML gốc thường cập nhật cả thuộc tính và thay đổi thuộc tính.

Hãy để phần tử tuỳ chỉnh cập nhật khung hiển thị 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à phương thức 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ử xếp hạng nếu có. Điều này có nghĩa là nếu bạn đặt thuộc tính xếp hạng trên phần tử, 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ụ cho nhà phát triển!

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

Giờ đây, 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ư việc một thành phần đầu vào cập nhật khung hiển thị khi bạn đặt <input value="newValue">. Thật may mắn là vòng đời của Thành phần web có cả 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 điểm xếp hạng một cách khai báo trong DOM. Hãy thử:

index.html

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

Giờ đây, điểm xếp hạng sẽ được cập nhật một cách khai báo!

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

Giờ đây, chỉ còn thiếu 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 đánh giá bằng cách bỏ phiếu lên hoặc xuống và cung cấp thông tin phản hồi trực quan cho người dùng. Bạn có thể triển khai việc này bằng một số trình nghe sự kiện và một thuộc tính phản chiếu, nhưng trước tiên hãy cập nhật các kiểu để đưa ra phản hồi trực quan 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 một thuộc tính / thuộc tính phản chiếu cho vote tương tự như cách bạn triển khai rating. Bắt đầu bằng phương thức setter và phương thức 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 phiên bản _vote bằng null trong constructor và trong phương thức thiết lập, bạn sẽ kiểm tra xem giá trị mới có khác hay không. Nếu có, bạn sẽ điều chỉnh mức phân loại 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;
  }
}

Tương tự, đây cũng là quy trình 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 và đặt thuộc tính vote trong attributeChangedCallback. Và 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 cho 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ử và giữ lại các tham chiếu. Trong connectedCallback, bạn theo dõi các sự kiện nhấp chuột trên các nút. Trong disconnectedCallback, bạn sẽ dọn dẹp các trình nghe này và trên chính trình nghe lượt nhấp, bạn sẽ đặt 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 đầy đủ tính năng; hãy thử nhấp vào một số nút! Vấn đề hiện tại là tệp JS của tôi hiện có 96 dòng, tệp HTML có 43 dòng và mã khá dài dòng và bắt buộc đối với một thành phần đơn giản như vậy. Đây là lúc dự án Lit của Google phát huy tác dụng!

7. Lit-html

Điểm kiểm tra mã

Lý do nên dùng lit-html

Trước hết, thẻ <template> rất hữu ích và hiệu quả, nhưng thẻ này không được đóng gói cùng với logic của thành phần, do đó, bạn khó có thể phân phối mẫu cùng 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 có xu hướng dẫn đến mã mệnh lệnh. Trong nhiều trường hợp, điều này khiến mã khó đọc hơn so với các mẫu mã khai báo.

Đây là lúc lit-html phát huy tác dụng! Lit html là hệ thống kết xuất của Lit, cho phép bạn viết các mẫu HTML bằng JavaScript, sau đó kết xuất và kết xuất lại các mẫu đó một cách hiệu quả cùng với dữ liệu để tạo và cập nhật DOM. Thư viện này tương tự như các thư viện JSX và VDOM phổ biến, nhưng chạy nguyên bả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 rating-element Web Component gốc để sử dụng mẫu Lit. Mẫu này sử dụng Tagged Template Literals (Chuỗi mẫu được gắn thẻ). Đây là các hàm lấy chuỗi mẫu làm đối số bằng một cú pháp đặc biệt. Sau đó, Lit sử dụng các phần tử mẫu ở chế độ nền để kết xuất nhanh cũng như cung cấp một số tính năng dọn dẹp để đảm bảo an toàn. Bắt đầu bằng cách di chuyển <template> trong index.html vào một mẫu Lit bằng cách thêm phương thức render() vào webcomponent:

index.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 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 html theo nghĩa đen của mẫu được gắn thẻ. Bạn cũng sẽ nhận thấy rằng bạn đã thực hiện một quy trình 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 chuỗi ký tự mẫu của ${...}. Điều này có nghĩa là cuối cùng bạn sẽ không cần phải cập nhật bắt buộc nút đó nữa. Ngoài ra, bạn gọi phương thức render lit. Phương thức này sẽ kết xuất mẫu vào gốc bóng một cách đồng bộ.

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

Bây giờ, bạn đã loại bỏ phần tử <template>, hãy tái cấu trúc mã để thay vào đó gọi phương thức render mới xác định. Bạn có thể bắt đầu bằng cách tận dụng tính nă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 Lit có thể thêm một trình nghe sự kiện vào một nút bằng 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 tạo trình nghe sự kiện trong constructorconnectedCallback cũng như disconnectedCallback:

index.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 để 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ỏ các phương thức trình nghe _onUpClick_onDownClick!

Cuối cùng, hãy cập nhật các phương thức đặt thuộc tính để sử dụng phương thức render để 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();
}

Ở đâ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 từ phương thức setter vote. Giờ đây, mẫu này dễ đọc hơn nhiều vì bạn có thể thấy nơi áp dụng các liên kết và trình nghe sự kiện.

Làm mới trang và bạn sẽ có một nút xếp hạng hoạt động được. Nút này sẽ có dạng như sau khi người dùng nhấn nút biểu quyết tán thành!

Thanh trượt đánh giá bằng ngón tay cái lên và xuống có giá trị là 6, ngón tay cái lên có màu xanh lục

8. LitElement

Tại sao nên chọn LitElement

Mã 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ì thuộc tính 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 lặp lại của render về cơ bản là không có tác dụng và hiệu quả, nhưng VM javascript vẫn 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à đặc điểm mới rất tẻ nhạt vì cần nhiều mã nguyên mẫu. Đây là lúc LitElement phát huy tác dụng!

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

Sử dụng LitElement

Bắt đầu bằng cách nhập và phân lớp phụ 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 nhập LitElement, đây là lớp cơ sở mới cho rating-element. Tiếp theo, bạn giữ lại html import và cuối cùng là css cho phép chúng ta xác định các chuỗi ký tự mẫu được gắn thẻ css cho toán học, tạo mẫu css và các tính năng khác.

Tiếp theo, hãy 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 nằm trong 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 (chẳng hạn như Constructable Stylesheets) để tăng tốc độ kết xuất cũng như truyền kiểu qua polyfill Web Components trên các trình duyệt cũ 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 dựa trên các lệnh gọi lại của 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 đã khai báo thay đổi.

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

index.js

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

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

Tại đâ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ẽ được dùng để chuyển đổi các thuộc tính chuỗi thành 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 thuộc tính và kết xuất 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 (setter và getter)
  • vote (setter và getter nhưng vẫn giữ logic thay đổi từ setter)

Bạn sẽ giữ lại 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, đồng thời di chuyển logic của phương thức setter vote sang 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 có thuộc tính cập nhật nào thay đổi, vì LitElement xử lý hàng loạt các thay đổi về thuộc tính và kết xuất không đồng bộ. Các thay đổi đối với những thuộc tính phản ứng (chẳng hạn 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 trả về một 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 gốc bóng và không còn phải gọi hàm render đã nhập trước đó từ gói 'lit' nữa.

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

9. Xin chúc mừng

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

Lit có kích thước siêu nhỏ (< 5 KB khi được giảm thiểu và nén bằng gzip), siêu nhanh và rất thú vị khi lập trình! 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ể tạo các ứng dụng đầy đủ tính năng bằng khung này!

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

Điểm kiểm tra mã

Bạn có muốn kiểm tra mã cuối cùng của mình 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 xem một số lớp học lập trình khác!

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

Cộng đồng