1. บทนำ
Last Updated: 2021-08-10
Web Components
Web Components คือชุด API ของแพลตฟอร์มเว็บที่ช่วยให้คุณสร้างแท็ก HTML ใหม่ที่กำหนดเอง นำกลับมาใช้ซ้ำได้ และแคปซูลเพื่อใช้ในหน้าเว็บและเว็บแอป คอมโพเนนต์และวิดเจ็ตที่กำหนดเองซึ่งสร้างขึ้นตามมาตรฐาน Web Component จะทำงานได้ในเบราว์เซอร์ที่ทันสมัย และใช้ได้กับไลบรารีหรือเฟรมเวิร์ก JavaScript ใดก็ได้ที่ทำงานร่วมกับ HTML
Lit คืออะไร
Lit เป็นไลบรารีที่เรียบง่ายสำหรับการสร้างคอมโพเนนต์ของเว็บที่รวดเร็วและมีขนาดเล็ก ซึ่งทำงานได้ในทุกเฟรมเวิร์กหรือไม่มีเฟรมเวิร์กเลย Lit ช่วยให้คุณสร้างคอมโพเนนต์ แอปพลิเคชัน ระบบการออกแบบ และอื่นๆ ที่แชร์ได้
Lit มี API ที่ช่วยลดความซับซ้อนของงาน Web Components ทั่วไป เช่น การจัดการพร็อพเพอร์ตี้ แอตทริบิวต์ และการแสดงผล
สิ่งที่คุณจะได้เรียนรู้
- Web Component คืออะไร
- แนวคิดของ Web Components
- วิธีสร้าง Web Component
- lit-html และ LitElement คืออะไร
- สิ่งที่ Lit ทำบน Web Component
สิ่งที่คุณจะสร้าง
- คอมโพเนนต์เว็บแบบธรรมดาสำหรับปุ่มชอบ / ไม่ชอบ
- Web Component ที่ใช้ Lit ซึ่งมีปุ่มชอบ / ไม่ชอบ
สิ่งที่คุณต้องมี
- เบราว์เซอร์รุ่นใหม่ที่อัปเดตแล้ว (Chrome, Safari, Firefox, Chromium Edge) คอมโพเนนต์ของเว็บทำงานในเบราว์เซอร์สมัยใหม่ทั้งหมด และมี 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 ของ Playground ที่สว่าง

ภาพหน้าจอ UI ของ Lit Playground ไฮไลต์ส่วนที่คุณจะใช้ในโค้ดแล็บนี้
- ตัวเลือกไฟล์ สังเกตปุ่มบวก...
- โปรแกรมแก้ไขไฟล์
- ตัวอย่างโค้ด
- ปุ่มโหลดซ้ำ
- ปุ่มดาวน์โหลด
การตั้งค่า VS Code (ขั้นสูง)
ประโยชน์ของการใช้การตั้งค่า VS Code นี้มีดังนี้
- การตรวจสอบประเภทเทมเพลต
- IntelliSense และการเติมข้อความอัตโนมัติของเทมเพลต
หากคุณติดตั้ง NPM, VS Code (พร้อมปลั๊กอิน lit-plugin) ไว้แล้วและรู้วิธีใช้สภาพแวดล้อมดังกล่าว คุณก็เพียงดาวน์โหลดและเริ่มโปรเจ็กต์เหล่านี้ได้โดยทำดังนี้
- กดปุ่มดาวน์โหลด
- แตกเนื้อหาของไฟล์ tar ลงในไดเรกทอรี
- ติดตั้งเซิร์ฟเวอร์สำหรับนักพัฒนาซอฟต์แวร์ที่สามารถระบุโมดูลเปล่าได้ (ทีม Lit ขอแนะนำ @web/dev-server)
- ตัวอย่างเช่น
package.json
- ตัวอย่างเช่น
- เรียกใช้เซิร์ฟเวอร์สำหรับนักพัฒนาซอฟต์แวร์และเปิดเบราว์เซอร์ (หากใช้
@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 และเรียกใช้คำสั่งต่อไปนี้ในคอนโซล
$0.constructor
ซึ่งควรแสดงผลดังนี้
class RatingElement extends HTMLElement {}
วงจรของ Custom Element
Custom Elements มาพร้อมชุดฮุกวงจร ดังนี้
constructorconnectedCallbackdisconnectedCallbackattributeChangedCallbackadoptedCallback
ระบบจะเรียกใช้ constructor เมื่อสร้างองค์ประกอบเป็นครั้งแรก เช่น โดยการเรียกใช้ document.createElement(‘rating-element') หรือ new RatingElement() ตัวสร้างเป็นที่ที่เหมาะสำหรับการตั้งค่าองค์ประกอบ แต่โดยทั่วไปแล้วถือเป็นแนวทางปฏิบัติที่ไม่ดีในการดำเนินการ DOM ในตัวสร้างด้วยเหตุผลด้านประสิทธิภาพ "การเริ่มต้น" ขององค์ประกอบ
ระบบจะเรียกใช้ connectedCallback เมื่อแนบ Custom Element กับ 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 ด้วย

ตอนนี้คุณควรจะเห็นรูทเงาที่ขยายได้ซึ่งมีเนื้อหาอยู่ ทุกอย่างที่อยู่ภายใน 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 ด้วยข้อมูล
ขอแสดงความยินดี ตอนนี้คุณมี Web Component แล้ว แต่ตอนนี้ยังไม่มีอะไรเกิดขึ้น ดังนั้นต่อไปเราจะเพิ่มฟังก์ชันการทำงานกัน
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 ทริกเกอร์ คุณต้องตั้งค่า Getter แบบคงที่สำหรับ 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 และสุดท้าย ให้เพิ่มเครื่องมือฟังเหตุการณ์คลิกเพื่อให้ปุ่มทำงานได้
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
คุณสามารถนำตรรกะของฟังก์ชันรอคลิกออกจากทั้ง 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 ออกจาก Setter rating และเพิ่มการเรียกไปยัง render จาก Setter vote ตอนนี้เทมเพลตอ่านง่ายขึ้นมาก เนื่องจากคุณจะเห็นตำแหน่งที่ใช้การเชื่อมโยงและ Listener เหตุการณ์
รีเฟรชหน้าเว็บ แล้วคุณจะเห็นปุ่มให้คะแนนที่ใช้งานได้ซึ่งจะมีลักษณะดังนี้เมื่อกดโหวตขึ้น

8. LitElement
ทำไมต้อง LitElement
โค้ดยังคงมีปัญหาบางอย่าง ประการแรก หากคุณเปลี่ยนพร็อพเพอร์ตี้หรือแอตทริบิวต์ vote พร็อพเพอร์ตี้ rating อาจเปลี่ยนแปลง ซึ่งจะส่งผลให้มีการเรียกใช้ render 2 ครั้ง แม้ว่าการเรียกใช้การแสดงผลซ้ำๆ จะไม่มีผลและมีประสิทธิภาพ แต่ VM ของ JavaScript ก็ยังคงใช้เวลาในการเรียกใช้ฟังก์ชันนั้น 2 ครั้งพร้อมกัน ประการที่สอง การเพิ่มพร็อพเพอร์ตี้และแอตทริบิวต์ใหม่ๆ เป็นเรื่องน่าเบื่อเนื่องจากต้องใช้โค้ด Boilerplate จำนวนมาก LitElement จึงเข้ามามีบทบาทในจุดนี้
LitElement คือคลาสพื้นฐานของ Lit สำหรับสร้างคอมโพเนนต์เว็บที่รวดเร็วและมีน้ำหนักเบาซึ่งใช้ได้ในเฟรมเวิร์กและสภาพแวดล้อมต่างๆ จากนั้น มาดูกันว่า LitElement ทำอะไรให้เราได้บ้างใน rating-element โดยเปลี่ยนการติดตั้งใช้งานให้ใช้
การใช้ 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 ดั้งเดิม ระบบจะทริกเกอร์ฟังก์ชันเรียกกลับเหล่านี้เมื่อมีการเปลี่ยนแปลงพร็อพเพอร์ตี้ 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
ตอนนี้คุณมีบล็อกพร็อพเพอร์ตี้แบบคงที่แล้ว คุณจึงนำตรรกะการอัปเดตการแสดงผลแอตทริบิวต์และพร็อพเพอร์ตี้ทั้งหมดออกได้ ซึ่งหมายความว่าคุณนำวิธีการต่อไปนี้ออกได้
connectedCallbackobservedAttributesattributeChangedCallbackrating(ตัวตั้งค่าและตัวรับค่า)vote(ตัวตั้งค่าและตัวรับค่า แต่เก็บตรรกะการเปลี่ยนแปลงจากตัวตั้งค่าไว้)
สิ่งที่คุณจะยังคงมีคือ 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 แล้วย้ายตรรกะของ Setter 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 DOM และไม่ต้องเรียกใช้ฟังก์ชัน render ที่นำเข้าจากแพ็กเกจ 'lit' ก่อนหน้านี้อีกต่อไป
ตอนนี้องค์ประกอบควรแสดงในการแสดงตัวอย่างแล้ว ลองคลิกดู
9. ขอแสดงความยินดี
ยินดีด้วย คุณสร้าง Web Component จากศูนย์และพัฒนาให้เป็น LitElement ได้สำเร็จแล้ว
Lit มีขนาดเล็กมาก (น้อยกว่า 5 KB เมื่อลดขนาดและบีบอัดด้วย Gzip) รวดเร็วมาก และสนุกกับการเขียนโค้ดด้วยมาก คุณสามารถสร้างคอมโพเนนต์เพื่อให้เฟรมเวิร์กอื่นๆ ใช้ หรือจะสร้างแอปที่สมบูรณ์แบบด้วยก็ได้
ตอนนี้คุณทราบแล้วว่า Web Component คืออะไร วิธีสร้าง และวิธีที่ Lit ช่วยให้การสร้างง่ายขึ้น
จุดตรวจโค้ด
คุณต้องการตรวจสอบโค้ดสุดท้ายกับโค้ดของเราไหม เปรียบเทียบที่นี่
สิ่งต่อไปที่ควรทำ
ลองดู Codelab อื่นๆ
- Lit สำหรับนักพัฒนา React
- สร้าง Brick Viewer ด้วย lit-element
- สร้างคอมโพเนนต์เรื่องราวด้วย lit-element
อ่านเพิ่มเติม
- บทแนะนำแบบอินเทอร์แอกทีฟของ Lit
- The Lit Docs
- Open Web Components - ชุมชนที่ให้คำแนะนำและเครื่องมือ
- WebComponents.dev - สร้างคอมโพเนนต์เว็บในเฟรมเวิร์กที่รู้จักทั้งหมด
ชุมชน
- Lit and Friends Slack - ชุมชน Web Components ที่ใหญ่ที่สุด
- @buildWithLit บน Twitter - บัญชี Twitter ของทีมที่สร้าง Lit
- Web Components SF - การพบปะเกี่ยวกับ Web Components สำหรับซานฟรานซิสโก