จากคอมโพเนนต์เว็บไปยังองค์ประกอบ Lit

1. บทนำ

อัปเดตล่าสุด: 10-08-2021

Web Components

Web Components คือชุด API ของแพลตฟอร์มเว็บที่ช่วยให้คุณสร้างแท็ก HTML ใหม่ที่กำหนดเอง นำกลับมาใช้ซ้ำได้ และแคปซูลเพื่อใช้ในหน้าเว็บและเว็บแอป คอมโพเนนต์และวิดเจ็ตที่กำหนดเองซึ่งสร้างขึ้นตามมาตรฐาน Web Component จะทำงานได้ในเบราว์เซอร์ที่ทันสมัย และใช้ได้กับไลบรารีหรือเฟรมเวิร์ก JavaScript ใดก็ได้ที่ทำงานร่วมกับ HTML

Lit คืออะไร

Lit เป็นไลบรารีที่เรียบง่ายสำหรับการสร้างคอมโพเนนต์ของเว็บที่รวดเร็วและมีขนาดเล็กซึ่งทำงานได้ในทุกเฟรมเวิร์กหรือไม่มีเฟรมเวิร์กเลย Lit ช่วยให้คุณสร้างคอมโพเนนต์ แอปพลิเคชัน ระบบการออกแบบ และอื่นๆ ที่แชร์ได้

Lit มี API ที่ช่วยลดความซับซ้อนของงาน Web Components ทั่วไป เช่น การจัดการพร็อพเพอร์ตี้ แอตทริบิวต์ และการแสดงผล

สิ่งที่คุณจะได้เรียนรู้

  • Web Component คืออะไร
  • แนวคิดของ Web Component
  • วิธีสร้าง Web Component
  • lit-html และ LitElement คืออะไร
  • สิ่งที่ Lit ทำบน Web Component

สิ่งที่คุณจะสร้าง

  • คอมโพเนนต์เว็บสำหรับปุ่มชอบ / ไม่ชอบแบบธรรมดา
  • Web Component ที่ใช้ Lit สำหรับปุ่มชอบ / ไม่ชอบ

สิ่งที่คุณต้องมี

  • เบราว์เซอร์รุ่นใหม่ที่อัปเดตแล้ว (Chrome, Safari, Firefox, Chromium Edge) Web Components ทำงานในเบราว์เซอร์สมัยใหม่ทั้งหมด และมี Polyfill สำหรับ Microsoft Internet Explorer 11 และ Microsoft Edge ที่ไม่ใช่ Chromium
  • ความรู้เกี่ยวกับ HTML, CSS, JavaScript และเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome

2. การตั้งค่าและการสำรวจ Playground

การเข้าถึงรหัส

ในตลอดทั้ง Codelab จะมีลิงก์ไปยัง Lit Playground ดังนี้

สนามเด็กเล่นคือแซนด์บ็อกซ์โค้ดที่ทำงานในเบราว์เซอร์ของคุณโดยสมบูรณ์ โดยสามารถคอมไพล์และเรียกใช้ไฟล์ TypeScript และ JavaScript รวมถึงสามารถแก้ไขการนำเข้าไปยังโมดูล Node ได้โดยอัตโนมัติ เช่น

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

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

คุณสามารถทำตามบทแนะนำทั้งหมดใน Lit Playground โดยใช้จุดตรวจสอบเหล่านี้เป็นจุดเริ่มต้น หากใช้ VS Code คุณสามารถใช้จุดตรวจสอบเหล่านี้เพื่อดาวน์โหลดโค้ดเริ่มต้นสำหรับขั้นตอนใดก็ได้ รวมถึงใช้เพื่อตรวจสอบงานของคุณ

การสำรวจ UI ของฟีเจอร์ Most Searched Playground

แถบแท็บตัวเลือกไฟล์มีป้ายกำกับเป็นส่วนที่ 1 ส่วนการแก้ไขโค้ดเป็นส่วนที่ 2 ตัวอย่างเอาต์พุตเป็นส่วนที่ 3 และปุ่มโหลดตัวอย่างซ้ำเป็นส่วนที่ 4

ภาพหน้าจอ UI ของ Lit Playground จะไฮไลต์ส่วนที่คุณจะใช้ในโค้ดแล็บนี้

  1. ตัวเลือกไฟล์ โปรดสังเกตปุ่มบวก...
  2. โปรแกรมแก้ไขไฟล์
  3. ตัวอย่างโค้ด
  4. ปุ่มโหลดซ้ำ
  5. ปุ่มดาวน์โหลด

การตั้งค่า VS Code (ขั้นสูง)

ประโยชน์ของการใช้การตั้งค่า VS Code นี้มีดังนี้

  • การตรวจสอบประเภทเทมเพลต
  • IntelliSense และการเติมข้อความอัตโนมัติของเทมเพลต

หากคุณติดตั้ง NPM, VS Code (พร้อมปลั๊กอิน lit-plugin) ไว้แล้วและรู้วิธีใช้สภาพแวดล้อมดังกล่าว คุณก็เพียงแค่ดาวน์โหลดและเริ่มโปรเจ็กต์เหล่านี้ได้โดยทำดังนี้

  • กดปุ่มดาวน์โหลด
  • แตกเนื้อหาของไฟล์ tar ลงในไดเรกทอรี
  • ติดตั้งเซิร์ฟเวอร์สำหรับนักพัฒนาซอฟต์แวร์ที่สามารถระบุโมดูลเปล่า (ทีม Lit ขอแนะนำ @web/dev-server)
  • เรียกใช้เซิร์ฟเวอร์สำหรับนักพัฒนาซอฟต์แวร์และเปิดเบราว์เซอร์ (หากใช้ @web/dev-server คุณจะใช้ npx web-dev-server --node-resolve --watch --open ได้)
    • หากคุณใช้ตัวอย่าง package.json ให้ใช้ npm run serve

3. กำหนดองค์ประกอบที่กำหนดเอง

Custom Elements

Web Components คือชุดของเว็บ API ที่มาพร้อมเครื่อง 4 รายการ ดังนี้

  • โมดูล ES
  • Custom Elements
  • Shadow DOM
  • เทมเพลต HTML

คุณได้ใช้ข้อกำหนดของโมดูล ES อยู่แล้ว ซึ่งช่วยให้คุณสร้างโมดูล JavaScript ด้วยการนำเข้าและส่งออกที่โหลดลงในหน้าเว็บด้วย <script type="module"> ได้

การกำหนดองค์ประกอบที่กำหนดเอง

ข้อกำหนดขององค์ประกอบที่กำหนดเองช่วยให้ผู้ใช้กำหนดองค์ประกอบ HTML ของตนเองได้โดยใช้ JavaScript ชื่อต้องมีขีดกลาง (-) เพื่อแยกความแตกต่างจากองค์ประกอบของเบราว์เซอร์ดั้งเดิม ล้างไฟล์ index.js แล้วกำหนดคลาสองค์ประกอบที่กำหนดเอง

index.js

class RatingElement extends HTMLElement {}

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

องค์ประกอบที่กำหนดเองจะกำหนดโดยการเชื่อมโยงคลาสที่ขยาย HTMLElement กับชื่อแท็กที่มีขีดกลาง การเรียกใช้ customElements.define จะบอกให้เบราว์เซอร์เชื่อมโยงคลาส RatingElement กับ tagName ‘rating-element' ซึ่งหมายความว่าองค์ประกอบทุกอย่างในเอกสารที่มีชื่อ <rating-element> จะเชื่อมโยงกับคลาสนี้

วาง <rating-element> ในเนื้อหาของเอกสาร แล้วดูว่าระบบแสดงผลอย่างไร

index.html

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

ตอนนี้เมื่อดูที่เอาต์พุต คุณจะเห็นว่าไม่มีการแสดงผล ซึ่งเป็นเรื่องปกติเนื่องจากคุณยังไม่ได้บอกเบราว์เซอร์ว่าจะแสดงผล <rating-element> อย่างไร คุณยืนยันได้ว่าการกำหนด Custom Element สำเร็จแล้วโดยเลือก <rating-element> ในตัวเลือกองค์ประกอบของ Chrome Dev Tools และเรียกใช้คำสั่งต่อไปนี้ในคอนโซล

$0.constructor

ซึ่งควรแสดงผลดังนี้

class RatingElement extends HTMLElement {}

วงจรของ Custom Element

Custom Elements มาพร้อมชุดฮุกวงจร ดังนี้

  • constructor
  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

ระบบจะเรียกใช้ constructor เมื่อสร้างองค์ประกอบเป็นครั้งแรก เช่น โดยการเรียกใช้ document.createElement(‘rating-element') หรือ new RatingElement() Constructor เป็นที่ที่เหมาะสำหรับการตั้งค่าองค์ประกอบ แต่โดยทั่วไปถือเป็นแนวทางปฏิบัติที่ไม่ดีในการดำเนินการ DOM ใน Constructor ด้วยเหตุผลด้านประสิทธิภาพการ "บูต" องค์ประกอบ

ระบบจะเรียกใช้ connectedCallback เมื่อแนบองค์ประกอบที่กำหนดเองกับ DOM ซึ่งโดยปกติแล้วจะเป็นที่ที่เกิดการจัดการ DOM ครั้งแรก

ระบบจะเรียกใช้ disconnectedCallback หลังจากนำองค์ประกอบที่กำหนดเองออกจาก DOM

ระบบจะเรียกใช้ attributeChangedCallback(attrName, oldValue, newValue) เมื่อแอตทริบิวต์ที่ผู้ใช้ระบุมีการเปลี่ยนแปลง

ระบบจะเรียกใช้ adoptedCallback เมื่อมีการนำองค์ประกอบที่กำหนดเองจาก documentFragment อื่นมาใช้ในเอกสารหลักผ่าน adoptNode เช่น ใน HTMLTemplateElement

แสดงผล DOM

ตอนนี้กลับไปที่องค์ประกอบที่กำหนดเองและเชื่อมโยง DOM บางส่วนกับองค์ประกอบนั้น ตั้งค่าเนื้อหาขององค์ประกอบเมื่อแนบกับ 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);

ใน constructor คุณจะจัดเก็บพร็อพเพอร์ตี้อินสแตนซ์ที่ชื่อ rating ไว้ในองค์ประกอบ ใน connectedCallback คุณจะเพิ่มองค์ประกอบย่อยของ DOM ลงใน <rating-element> เพื่อแสดงคะแนนปัจจุบัน พร้อมกับปุ่มชอบและไม่ชอบ

4. Shadow DOM

เหตุใดจึงต้องใช้ Shadow DOM

ในขั้นตอนก่อนหน้า คุณจะเห็นว่าตัวเลือกในแท็กรูปแบบที่คุณแทรกจะเลือกองค์ประกอบการให้คะแนนใดก็ได้ในหน้า รวมถึงปุ่มใดก็ได้ ซึ่งอาจส่งผลให้สไตล์หลุดออกจากองค์ประกอบและเลือกโหนดอื่นๆ ที่คุณอาจไม่ได้ตั้งใจจะจัดรูปแบบ นอกจากนี้ สไตล์อื่นๆ ที่อยู่นอกองค์ประกอบที่กำหนดเองนี้อาจจัดรูปแบบโหนดภายในองค์ประกอบที่กำหนดเองโดยไม่ตั้งใจ เช่น ลองใส่แท็กรูปแบบในส่วนหัวของเอกสารหลัก

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>

เอาต์พุตควรมีกรอบสีแดงรอบช่วงสำหรับการจัดประเภท นี่เป็นกรณีที่ง่าย แต่การไม่มีการแคปซูล DOM อาจทำให้เกิดปัญหาที่ใหญ่ขึ้นสำหรับแอปพลิเคชันที่ซับซ้อนกว่า Shadow DOM จึงเข้ามามีบทบาทในจุดนี้

การแนบ Shadow Root

แนบ Shadow Root กับองค์ประกอบและแสดงผล DOM ภายในรูทนั้น

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);

เมื่อรีเฟรชหน้าเว็บ คุณจะเห็นว่าสไตล์ในเอกสารหลักไม่สามารถเลือกโหนดภายใน Shadow Root ได้อีกต่อไป

คุณทำได้อย่างไร ใน connectedCallback ที่คุณเรียก this.attachShadow ซึ่งแนบรูทเงาเข้ากับองค์ประกอบ open โหมดหมายความว่าเนื้อหา Shadow สามารถตรวจสอบได้และทำให้เข้าถึง Shadow Root ผ่าน this.shadowRoot ได้ด้วย ดูคอมโพเนนต์เว็บในเครื่องมือตรวจสอบของ Chrome ด้วย

แผนผัง DOM ในเครื่องมือตรวจสอบของ Chrome มี <rating-element> ที่มี#shadow-root (open) เป็นองค์ประกอบย่อย และ DOM จากก่อนหน้าภายใน shadowroot นั้น

ตอนนี้คุณควรเห็นรูทเงาที่ขยายได้ซึ่งมีเนื้อหาอยู่ ทุกอย่างภายใน Shadow Root นั้นเรียกว่า Shadow DOM หากคุณเลือกองค์ประกอบการให้คะแนนในเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ Chrome และเรียกใช้ $0.children คุณจะเห็นว่าไม่มีองค์ประกอบย่อย เนื่องจาก Shadow DOM ไม่ถือเป็นส่วนหนึ่งของแผนผัง DOM เดียวกันกับองค์ประกอบย่อยโดยตรง แต่เป็น Shadow Tree

Light DOM

การทดลอง: เพิ่มโหนดเป็นโหนดลูกโดยตรงของ <rating-element>

index.html

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

รีเฟรชหน้าเว็บ แล้วคุณจะเห็นว่าโหนด DOM ใหม่นี้ใน Light DOM ของ Custom Element นี้ไม่ปรากฏในหน้าเว็บ เนื่องจาก Shadow DOM มีฟีเจอร์ในการควบคุมวิธีฉายโหนด Light DOM ลงใน Shadow DOM ผ่านองค์ประกอบ <slot>

5. เทมเพลต HTML

เหตุผลที่ควรใช้เทมเพลต

การใช้ innerHTML และสตริงตามรูปแบบเทมเพลตโดยไม่มีการล้างข้อมูลอาจทำให้เกิดปัญหาด้านความปลอดภัยจากการแทรกสคริปต์ ในอดีต วิธีการต่างๆ รวมถึงการใช้ DocumentFragment แต่ก็มีปัญหาอื่นๆ เช่น รูปภาพโหลดและสคริปต์ทำงานเมื่อกำหนดเทมเพลต รวมถึงเป็นอุปสรรคต่อการนำกลับมาใช้ซ้ำ <template> องค์ประกอบนี้จึงมีบทบาทสำคัญ เทมเพลตมี DOM ที่ไม่มีการใช้งาน ซึ่งเป็นวิธีที่มีประสิทธิภาพสูงในการโคลนโหนด และการสร้างเทมเพลตที่นำกลับมาใช้ใหม่ได้

การใช้เทมเพลต

จากนั้นเปลี่ยนคอมโพเนนต์ให้ใช้เทมเพลต 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>

ในที่นี้ คุณได้ย้ายเนื้อหา DOM ไปยังแท็กเทมเพลตใน DOM ของเอกสารหลัก ตอนนี้ให้ปรับโครงสร้างคำจำกัดความขององค์ประกอบที่กำหนดเอง

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);

หากต้องการใช้องค์ประกอบเทมเพลตนี้ คุณต้องค้นหาเทมเพลต รับเนื้อหาของเทมเพลต และโคลนโหนดเหล่านั้นด้วย templateContent.cloneNode โดยที่อาร์กิวเมนต์ true จะทำการโคลนแบบลึก จากนั้นคุณจะเริ่มต้น DOM ด้วยข้อมูล

ขอแสดงความยินดี ตอนนี้คุณมีคอมโพเนนต์ของเว็บแล้ว แต่ตอนนี้ยังไม่มีอะไรเกิดขึ้น ดังนั้นต่อไปให้เพิ่มฟังก์ชันการทำงาน

6. การเพิ่มฟังก์ชันการทำงาน

การเชื่อมโยงพร็อพเพอร์ตี้

ปัจจุบันวิธีเดียวในการตั้งค่าคะแนนในองค์ประกอบการให้คะแนนคือการสร้างองค์ประกอบ ตั้งค่าพร็อพเพอร์ตี้ rating ในออบเจ็กต์ แล้ววางไว้ในหน้าเว็บ แต่โดยปกติแล้วองค์ประกอบ HTML ดั้งเดิมไม่ได้ทำงานในลักษณะนี้ องค์ประกอบ HTML ดั้งเดิมมักจะอัปเดตตามการเปลี่ยนแปลงทั้งพร็อพเพอร์ตี้และแอตทริบิวต์

ทําให้องค์ประกอบที่กําหนดเองอัปเดตมุมมองเมื่อพร็อพเพอร์ตี้ rating เปลี่ยนแปลงโดยการเพิ่มบรรทัดต่อไปนี้

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;
}

คุณเพิ่ม Setter และ Getter สำหรับพร็อพเพอร์ตี้การให้คะแนน จากนั้นอัปเดตข้อความขององค์ประกอบการให้คะแนนหากมี ซึ่งหมายความว่าหากคุณตั้งค่าพร็อพเพอร์ตี้การให้คะแนนในองค์ประกอบ มุมมองจะอัปเดต ให้ลองทดสอบอย่างรวดเร็วในคอนโซลเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์

การเชื่อมโยงแอตทริบิวต์

ตอนนี้ให้อัปเดตมุมมองเมื่อแอตทริบิวต์เปลี่ยนแปลง ซึ่งคล้ายกับการอัปเดตมุมมองของอินพุตเมื่อคุณตั้งค่า <input value="newValue"> โชคดีที่วงจรของคอมโพเนนต์เว็บมี attributeChangedCallback อัปเดตคะแนนโดยเพิ่มบรรทัดต่อไปนี้

index.js

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

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

หากต้องการให้ attributeChangedCallback ทริกเกอร์ คุณต้องตั้งค่าตัวรับแบบคงที่สำหรับ RatingElement.observedAttributes which defines the attributes to be observed for changes จากนั้นคุณจะตั้งค่าการให้คะแนนแบบประกาศใน DOM ลองเลย

index.html

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

ตอนนี้การจัดประเภทควรจะอัปเดตแบบประกาศได้แล้ว

ฟังก์ชันการทำงานของปุ่ม

ตอนนี้เหลือเพียงฟังก์ชันการทำงานของปุ่มเท่านั้น ลักษณะการทำงานของคอมโพเนนต์นี้ควรอนุญาตให้ผู้ใช้ให้คะแนนโหวตขึ้นหรือลงได้เพียงครั้งเดียว และแสดงความคิดเห็นด้วยภาพแก่ผู้ใช้ คุณสามารถใช้ฟีเจอร์นี้กับเครื่องมือฟังเหตุการณ์และพร็อพเพอร์ตี้การแสดงผลได้ แต่ก่อนอื่นให้อัปเดตรูปแบบเพื่อให้ความคิดเห็นด้วยภาพโดยการต่อท้ายบรรทัดต่อไปนี้

index.html

<style>
...

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

ใน Shadow DOM ตัวเลือก :host จะอ้างอิงถึงโหนดหรือองค์ประกอบที่กำหนดเองที่แนบ Shadow Root ไว้ ในกรณีนี้ หากแอตทริบิวต์ vote เป็น "up" ระบบจะเปลี่ยนปุ่มยกนิ้วเป็นสีเขียว แต่หาก vote เป็น "down", then it will turn the thumb-down button red ตอนนี้ ให้ใช้ตรรกะสำหรับกรณีนี้โดยการสร้างพร็อพเพอร์ตี้ / แอตทริบิวต์ที่แสดงสำหรับ vote ในลักษณะเดียวกับที่คุณใช้ rating เริ่มต้นด้วยตัวตั้งค่าและตัวรับค่าของพร็อพเพอร์ตี้

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;
}

คุณเริ่มต้นพร็อพเพอร์ตี้อินสแตนซ์ _vote ด้วย null ใน constructor และใน Setter คุณจะตรวจสอบว่าค่าใหม่แตกต่างกันหรือไม่ หากเป็นเช่นนั้น คุณจะปรับคะแนนตามนั้น และที่สำคัญคือจะแสดงแอตทริบิวต์ vote กลับไปยังโฮสต์พร้อมกับ this.setAttribute

จากนั้นตั้งค่าการเชื่อมโยงแอตทริบิวต์โดยทำดังนี้

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;
  }
}

อีกครั้ง นี่คือกระบวนการเดียวกันกับที่คุณดำเนินการกับการเชื่อมโยงแอตทริบิวต์ rating คุณเพิ่ม vote ลงใน observedAttributes และตั้งค่าพร็อพเพอร์ตี้ vote ใน attributeChangedCallback และสุดท้าย ให้เพิ่ม Listener เหตุการณ์กิจกรรมการคลิกเพื่อให้ปุ่มทำงานได้

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';
}

ใน constructor คุณจะเชื่อมโยงเครื่องมือฟังการคลิกบางรายการกับองค์ประกอบและเก็บการอ้างอิงไว้ ใน connectedCallback คุณจะฟังเหตุการณ์คลิกบนปุ่ม ใน disconnectedCallback คุณจะล้างข้อมูลผู้ฟังเหล่านี้ และในเครื่องมือตรวจคลิกเอง คุณจะตั้งค่า vote อย่างเหมาะสม

ขอแสดงความยินดี ตอนนี้คุณมีคอมโพเนนต์ของเว็บที่มีฟีเจอร์ครบถ้วนแล้ว ลองคลิกปุ่มบางปุ่มดู ตอนนี้ปัญหาคือไฟล์ JS ของฉันมี 96 บรรทัด ไฟล์ HTML มี 43 บรรทัด และโค้ดค่อนข้างยาวและจำเป็นสำหรับคอมโพเนนต์ที่เรียบง่ายเช่นนี้ และนี่คือจุดที่โปรเจ็กต์ Lit ของ Google เข้ามามีบทบาท

7. Lit-html

จุดตรวจสอบโค้ด

เหตุผลที่ควรใช้ lit-html

ประการแรก <template> แท็กมีประโยชน์และมีประสิทธิภาพ แต่ไม่ได้มาพร้อมกับตรรกะของคอมโพเนนต์ จึงทำให้การเผยแพร่เทมเพลตพร้อมกับตรรกะอื่นๆ เป็นเรื่องยาก นอกจากนี้ วิธีใช้องค์ประกอบเทมเพลตยังทำให้โค้ดเป็นแบบคำสั่งโดยธรรมชาติ ซึ่งในหลายๆ กรณีจะทำให้โค้ดอ่านยากกว่ารูปแบบการเขียนโค้ดแบบประกาศ

lit-html จึงเข้ามามีบทบาทในจุดนี้ Lit HTML คือระบบการแสดงผลของ Lit ที่ช่วยให้คุณเขียนเทมเพลต HTML ใน JavaScript จากนั้นแสดงผลและแสดงผลเทมเพลตเหล่านั้นร่วมกับข้อมูลได้อย่างมีประสิทธิภาพเพื่อสร้างและอัปเดต DOM ซึ่งคล้ายกับไลบรารี JSX และ VDOM ยอดนิยม แต่จะทำงานในเบราว์เซอร์โดยตรงและมีประสิทธิภาพมากกว่าในหลายๆ กรณี

การใช้ Lit HTML

จากนั้น ให้ย้ายข้อมูล Web Component rating-element ดั้งเดิมไปใช้เทมเพลต Lit ซึ่งใช้ Tagged Template Literals ซึ่งเป็นฟังก์ชันที่ใช้สตริงเทมเพลตเป็นอาร์กิวเมนต์ที่มีไวยากรณ์พิเศษ จากนั้น Lit จะใช้องค์ประกอบเทมเพลตเบื้องหลังเพื่อให้การแสดงผลรวดเร็ว รวมถึงมีฟีเจอร์การล้างข้อมูลบางอย่างเพื่อความปลอดภัย เริ่มต้นด้วยการย้ายข้อมูล <template> ใน index.html ไปยังเทมเพลต Lit โดยเพิ่มเมธอด render() ลงใน Web Component ดังนี้

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);
  }
}

นอกจากนี้ คุณยังลบเทมเพลตออกจาก index.html ได้ด้วย ในเมธอดการแสดงผลนี้ คุณจะกำหนดตัวแปรที่ชื่อ template และเรียกใช้ฟังก์ชันแท็กเทมเพลตลิเทอรัล html นอกจากนี้ คุณจะเห็นว่าคุณได้ทำการเชื่อมโยงข้อมูลอย่างง่ายภายในองค์ประกอบ span.rating โดยใช้ไวยากรณ์การแทรกตัวอักษรของเทมเพลต ${...} ซึ่งหมายความว่าในที่สุดคุณก็ไม่จำเป็นต้องอัปเดตโหนดนั้นอีกต่อไป นอกจากนี้ คุณยังเรียกใช้เมธอด render ของ Lit ซึ่งจะแสดงผลเทมเพลตแบบซิงโครนัสใน Shadow Root

การย้ายข้อมูลไปยังไวยากรณ์เชิงประกาศ

ตอนนี้คุณได้กำจัดองค์ประกอบ <template> แล้ว ให้ปรับโครงสร้างโค้ดเพื่อเรียกใช้เมธอด render ที่กำหนดใหม่แทน คุณเริ่มต้นได้โดยใช้ประโยชน์จากการเชื่อมโยง Listener เหตุการณ์ของ Lit เพื่อล้างโค้ด Listener

index.js

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

เทมเพลต Lit สามารถเพิ่ม Listener เหตุการณ์ ไปยังโหนดด้วยไวยากรณ์การเชื่อมโยง @EVENT_NAME ซึ่งในกรณีนี้ คุณจะอัปเดตพร็อพเพอร์ตี้ vote ทุกครั้งที่มีการคลิกปุ่มเหล่านี้

จากนั้น ให้ล้างโค้ดการเริ่มต้น Listener เหตุการณ์ใน constructor และ connectedCallback และ disconnectedCallback ดังนี้

index.js

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

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

// remove disonnectedCallback and _onUpClick and _onDownClick

คุณสามารถนำตรรกะของ Listener การคลิกออกจากทั้ง 3 Callback และยังนำ disconnectedCallback ออกทั้งหมดได้ด้วย นอกจากนี้ คุณยังนำโค้ดการเริ่มต้น DOM ทั้งหมดออกจาก connectedCallback ได้ด้วย ซึ่งทำให้ดูดีขึ้นมาก ซึ่งหมายความว่าคุณสามารถกำจัดเมธอด Listener ของ _onUpClick และ _onDownClick ได้ด้วย

สุดท้าย ให้อัปเดตตัวตั้งค่าพร็อพเพอร์ตี้เพื่อใช้เมธอด render เพื่อให้ DOM อัปเดตได้เมื่อมีการเปลี่ยนแปลงพร็อพเพอร์ตี้หรือแอตทริบิวต์

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();
}

ในที่นี้ คุณสามารถนำตรรกะการอัปเดต DOM ออกจาก rating setter และเพิ่มการเรียกไปยัง render จาก vote setter ตอนนี้เทมเพลตอ่านง่ายขึ้นมาก เนื่องจากคุณจะเห็นตำแหน่งที่ใช้การเชื่อมโยงและ Listener เหตุการณ์

รีเฟรชหน้าเว็บ แล้วคุณจะเห็นปุ่มให้คะแนนที่ใช้งานได้ซึ่งจะมีลักษณะดังนี้เมื่อกดโหวตขึ้น

แถบเลื่อนการให้คะแนนชอบและไม่ชอบที่มีค่าเป็น 6 และนิ้วโป้งขึ้นเป็นสีเขียว

8. LitElement

ทำไมต้อง LitElement

โค้ดยังคงมีปัญหาอยู่ ประการแรก หากคุณเปลี่ยนพร็อพเพอร์ตี้หรือแอตทริบิวต์ vote พร็อพเพอร์ตี้ rating อาจเปลี่ยนแปลง ซึ่งจะส่งผลให้มีการเรียกใช้ render 2 ครั้ง แม้ว่าการเรียกใช้ฟังก์ชันการแสดงผลซ้ำๆ จะไม่มีผลและมีประสิทธิภาพ แต่ VM ของ JavaScript ก็ยังคงใช้เวลาในการเรียกใช้ฟังก์ชันดังกล่าว 2 ครั้งพร้อมกัน ประการที่สอง การเพิ่มพร็อพเพอร์ตี้และแอตทริบิวต์ใหม่ๆ เป็นเรื่องน่าเบื่อเนื่องจากต้องใช้โค้ด Boilerplate จำนวนมาก LitElement จึงเข้ามามีบทบาทในจุดนี้

LitElement คือคลาสพื้นฐานของ Lit สำหรับสร้าง Web Component ที่รวดเร็วและมีน้ำหนักเบาซึ่งใช้ได้ในเฟรมเวิร์กและสภาพแวดล้อมต่างๆ จากนั้น มาดูกันว่า LitElement ทำอะไรให้เราได้บ้างใน rating-element โดยเปลี่ยนการติดตั้งใช้งานให้ใช้ LitElement

การใช้ LitElement

เริ่มต้นด้วยการนำเข้าและสร้างคลาสย่อยของLitElementคลาสพื้นฐานจากแพ็กเกจlit

index.js

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

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

คุณนำเข้า LitElement ซึ่งเป็นคลาสพื้นฐานใหม่สำหรับ rating-element จากนั้นคุณจะhtmlนำเข้าcss ซึ่งจะช่วยให้เรากำหนดแท็กเทมเพลตลิเทอรัล CSS สำหรับคณิตศาสตร์ CSS, การสร้างเทมเพลต และฟีเจอร์อื่นๆ ที่อยู่เบื้องหลังได้

จากนั้นย้ายสไตล์จากเมธอดการแสดงผลไปยังสไตล์ชีตแบบคงที่ของ 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;
      }
    `;
  }
 ...

ซึ่งเป็นที่เก็บสไตล์ส่วนใหญ่ใน Lit Lit จะใช้สไตล์เหล่านี้และใช้ฟีเจอร์ของเบราว์เซอร์ เช่น Constructable Stylesheets เพื่อให้แสดงผลได้เร็วขึ้น รวมถึงส่งผ่านไปยัง Polyfill ของ Web Components ในเบราว์เซอร์รุ่นเก่าหากจำเป็น

วงจร

Lit มีชุดเมธอด Lifecycle Callback ของการแสดงผลอยู่เหนือการเรียกกลับของ Web Component ดั้งเดิม ระบบจะทริกเกอร์ Callback เหล่านี้เมื่อมีการเปลี่ยนแปลงพร็อพเพอร์ตี้ Lit ที่ประกาศไว้

หากต้องการใช้ฟีเจอร์นี้ คุณต้องประกาศแบบคงที่ว่าพร็อพเพอร์ตี้ใดที่จะทริกเกอร์วงจรการแสดงผล

index.js

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

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

ในที่นี้ คุณกําหนดว่า rating และ vote จะทริกเกอร์วงจรการแสดงผล LitElement รวมถึงกําหนดประเภทที่จะใช้เพื่อแปลงแอตทริบิวต์สตริงเป็นพร็อพเพอร์ตี้

<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>

นอกจากนี้ reflect แฟล็กในพร็อพเพอร์ตี้ vote จะอัปเดตแอตทริบิวต์ vote ขององค์ประกอบโฮสต์โดยอัตโนมัติ ซึ่งคุณทริกเกอร์ด้วยตนเองใน Setter vote

ตอนนี้คุณมีบล็อกพร็อพเพอร์ตี้แบบคงที่แล้ว คุณจึงนำตรรกะการอัปเดตการแสดงผลแอตทริบิวต์และพร็อพเพอร์ตี้ทั้งหมดออกได้ ซึ่งหมายความว่าคุณสามารถนำวิธีการต่อไปนี้ออกได้

  • connectedCallback
  • observedAttributes
  • attributeChangedCallback
  • rating (ตัวตั้งค่าและตัวรับค่า)
  • vote (setters and getters but keep the change logic from the setter)

สิ่งที่คุณจะเก็บไว้คือ constructor รวมถึงเพิ่มเมธอดวงจรใหม่ของ willUpdate ดังนี้

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()

ในที่นี้ คุณเพียงแค่เริ่มต้น rating และ vote แล้วย้ายตรรกะของตัวตั้งค่า vote ไปยังเมธอดวงจร willUpdate ระบบจะเรียกใช้เมธอด willUpdate ก่อน render ทุกครั้งที่มีการเปลี่ยนแปลงพร็อพเพอร์ตี้ที่อัปเดต เนื่องจาก LitElement จะจัดกลุ่มการเปลี่ยนแปลงพร็อพเพอร์ตี้และทําให้การแสดงผลแบบไม่พร้อมกัน การเปลี่ยนแปลงพร็อพเพอร์ตี้แบบรีแอกทีฟ (เช่น this.rating) ใน willUpdate จะไม่ทําให้เกิดการเรียกวงจร render ที่ไม่จําเป็น

สุดท้าย render เป็นเมธอดวงจรการใช้งานของ LitElement ซึ่งกำหนดให้เราต้องส่งคืนเทมเพลต 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>`;
}

คุณไม่ต้องตรวจสอบ Shadow Root และไม่ต้องเรียกใช้ฟังก์ชัน render ที่นำเข้าจากแพ็กเกจ 'lit' ก่อนหน้านี้อีกต่อไป

ตอนนี้องค์ประกอบควรแสดงในตัวอย่างแล้ว ลองคลิกดู

9. ขอแสดงความยินดี

ยินดีด้วย คุณสร้าง Web Component จากศูนย์และพัฒนาให้เป็น LitElement ได้สำเร็จแล้ว

Lit มีขนาดเล็กมาก (น้อยกว่า 5 KB เมื่อลดขนาดและบีบอัดด้วย Gzip) รวดเร็วมาก และสนุกกับการเขียนโค้ดมากจริงๆ คุณสามารถสร้างคอมโพเนนต์เพื่อให้เฟรมเวิร์กอื่นๆ ใช้ หรือจะสร้างแอปที่สมบูรณ์แบบด้วยก็ได้

ตอนนี้คุณทราบแล้วว่า Web Component คืออะไร วิธีสร้าง และวิธีที่ Lit ช่วยให้การสร้างง่ายขึ้น

จุดตรวจสอบโค้ด

คุณต้องการตรวจสอบโค้ดสุดท้ายกับโค้ดของเราไหม เปรียบเทียบที่นี่

สิ่งต่อไปที่ควรทำ

ลองใช้ Codelab อื่นๆ

อ่านเพิ่มเติม

ชุมชน