1. บทนำ
Lit คืออะไร
Lit เป็นไลบรารีที่เรียบง่ายสำหรับการสร้างคอมโพเนนต์ของเว็บที่รวดเร็วและมีขนาดเล็ก ซึ่งทำงานได้ในทุกเฟรมเวิร์กหรือไม่มีเฟรมเวิร์กเลยก็ได้ Lit ช่วยให้คุณสร้างคอมโพเนนต์ แอปพลิเคชัน ระบบการออกแบบ และอื่นๆ ที่แชร์ได้
สิ่งที่คุณจะได้เรียนรู้
วิธีแปลแนวคิดหลายอย่างของ React เป็น Lit เช่น
- JSX และการสร้างเทมเพลต
- คอมโพเนนต์และพร็อพ
- สถานะและวงจร
- ฮุก
- เด็ก
- Refs
- สถานะการไกล่เกลี่ย
สิ่งที่คุณจะสร้าง
เมื่อสิ้นสุด Codelab นี้ คุณจะสามารถแปลงแนวคิดของคอมโพเนนต์ React เป็นแนวคิดที่คล้ายกันใน Lit ได้
สิ่งที่คุณต้องมี
- Chrome, Safari, Firefox หรือ Edge เวอร์ชันล่าสุด
- ความรู้เกี่ยวกับ HTML, CSS, JavaScript และเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome
- ความรู้เกี่ยวกับ React
- (ขั้นสูง) หากต้องการประสบการณ์การพัฒนาแอปที่ดีที่สุด ให้ดาวน์โหลด VS Code นอกจากนี้ คุณยังต้องมี lit-plugin สำหรับ VS Code และ NPM ด้วย
2. Lit เทียบกับ React
แนวคิดและความสามารถหลักของ Lit คล้ายกับ React ในหลายๆ ด้าน แต่ Lit ก็มีความแตกต่างและจุดเด่นที่สำคัญบางประการด้วย ดังนี้
มีขนาดเล็ก
Lit มีขนาดเล็กมาก โดยมีขนาดประมาณ 5 KB เมื่อย่อและบีบอัดด้วย gzip เทียบกับ React + ReactDOM ที่มีขนาดมากกว่า 40 KB

รวดเร็ว
ในการเปรียบเทียบประสิทธิภาพแบบสาธารณะที่เปรียบเทียบระบบเทมเพลตของ Lit ซึ่งก็คือ lit-html กับ VDOM ของ React นั้น lit-html เร็วกว่า React 8-10% ในกรณีที่แย่ที่สุด และเร็วกว่า 50%ขึ้นไปในกรณีการใช้งานที่พบบ่อยกว่า
LitElement (คลาสฐานของคอมโพเนนต์ของ Lit) เพิ่มค่าใช้จ่ายขั้นต่ำให้กับ lit-html แต่มีประสิทธิภาพดีกว่า React 16-30% เมื่อเปรียบเทียบฟีเจอร์ของคอมโพเนนต์ เช่น การใช้หน่วยความจำ เวลาในการโต้ตอบ และเวลาเริ่มต้น

ไม่จำเป็นต้องสร้าง
Lit ไม่จำเป็นต้องคอมไพล์เพื่อเรียกใช้ด้วยฟีเจอร์ใหม่ๆ ของเบราว์เซอร์ เช่น โมดูล ES และแท็กเทมเพลตลิเทอรัล ซึ่งหมายความว่าคุณสามารถตั้งค่าสภาพแวดล้อมการพัฒนาด้วยแท็กสคริปต์ + เบราว์เซอร์ + เซิร์ฟเวอร์ และพร้อมใช้งานได้
เมื่อใช้โมดูล ES และ CDN สมัยใหม่ เช่น Skypack หรือ UNPKG คุณอาจไม่จำเป็นต้องใช้ NPM เพื่อเริ่มต้นด้วยซ้ำ
อย่างไรก็ตาม หากต้องการ คุณยังคงสร้างและเพิ่มประสิทธิภาพโค้ด Lit ได้ การรวมตัวของนักพัฒนาซอฟต์แวร์ล่าสุดเกี่ยวกับโมดูล ES ดั้งเดิมเป็นสิ่งที่ดีสำหรับ Lit เนื่องจาก Lit เป็นเพียง JavaScript ปกติและไม่จำเป็นต้องใช้ CLI เฉพาะเฟรมเวิร์กหรือการจัดการบิลด์
ไม่ยึดติดกับเฟรมเวิร์ก
คอมโพเนนต์ของ Lit สร้างขึ้นจากชุดมาตรฐานเว็บที่เรียกว่าคอมโพเนนต์เว็บ ซึ่งหมายความว่าการสร้างคอมโพเนนต์ใน Lit จะใช้งานได้ในเฟรมเวิร์กปัจจุบันและอนาคต หากรองรับองค์ประกอบ HTML ก็จะรองรับคอมโพเนนต์ของเว็บ
ปัญหาเดียวเกี่ยวกับการทำงานร่วมกันของเฟรมเวิร์กคือเมื่อเฟรมเวิร์กมีการรองรับ DOM ที่จำกัด React เป็นหนึ่งในเฟรมเวิร์กเหล่านี้ แต่ก็อนุญาตให้ใช้ทางออกผ่าน Refs และ Refs ใน React ไม่ใช่ประสบการณ์การใช้งานที่ดีสำหรับนักพัฒนาแอป
ทีม Lit ได้ทำงานในโปรเจ็กต์ทดลองที่ชื่อ @lit-labs/react ซึ่งจะแยกวิเคราะห์คอมโพเนนต์ Lit โดยอัตโนมัติและสร้าง Wrapper ของ React เพื่อให้คุณไม่ต้องใช้ Ref
นอกจากนี้ Custom Elements Everywhere จะแสดงเฟรมเวิร์กและไลบรารีที่ทำงานร่วมกับองค์ประกอบที่กำหนดเองได้อย่างราบรื่น
การรองรับ TypeScript ระดับเฟิร์สคลาส
แม้ว่าคุณจะเขียนโค้ด Lit ทั้งหมดใน JavaScript ได้ แต่ Lit เขียนด้วย TypeScript และทีม Lit ขอแนะนำให้นักพัฒนาซอฟต์แวร์ใช้ TypeScript ด้วย
ทีม Lit ได้ทำงานร่วมกับชุมชน Lit เพื่อช่วยดูแลโปรเจ็กต์ที่นำการตรวจสอบประเภท TypeScript และการเติมข้อความอัตโนมัติมายังเทมเพลต Lit ทั้งในเวลาที่พัฒนาและสร้างด้วย lit-analyzer และ lit-plugin


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

สร้างขึ้นโดยคำนึงถึงการแสดงผลฝั่งเซิร์ฟเวอร์ (SSR)
Lit 2 สร้างขึ้นโดยคำนึงถึงการรองรับ SSR ในขณะที่เขียน Codelab นี้ ทีม Lit ยังไม่ได้เปิดตัวเครื่องมือ SSR ในรูปแบบที่เสถียร แต่ทีม Lit ได้ติดตั้งใช้งานคอมโพเนนต์ที่ฝั่งเซิร์ฟเวอร์แสดงผลในผลิตภัณฑ์ต่างๆ ของ Google และได้ทดสอบ SSR ภายในแอปพลิเคชัน React แล้ว ทีม Lit คาดว่าจะเปิดตัวเครื่องมือเหล่านี้ภายนอกบน GitHub ในเร็วๆ นี้
ในระหว่างนี้ คุณสามารถติดตามความคืบหน้าของทีม Lit ได้ที่นี่
การลงทุนต่ำ
Lit ไม่จำเป็นต้องมีข้อผูกมัดที่สำคัญในการใช้งาน คุณสามารถสร้างคอมโพเนนต์ใน Lit และเพิ่มลงในโปรเจ็กต์ที่มีอยู่ได้ หากไม่ชอบ ก็ไม่จำเป็นต้องแปลงทั้งแอปในครั้งเดียว เนื่องจากคอมโพเนนต์เว็บจะทำงานในเฟรมเวิร์กอื่นๆ ได้
หากคุณสร้างแอปทั้งแอปใน Lit และต้องการเปลี่ยนไปใช้เฟรมเวิร์กอื่น ในกรณีนี้ คุณสามารถวางแอปพลิเคชัน Lit ปัจจุบันไว้ในเฟรมเวิร์กใหม่และย้ายข้อมูลที่ต้องการไปยังคอมโพเนนต์ของเฟรมเวิร์กใหม่ได้
นอกจากนี้ เฟรมเวิร์กสมัยใหม่หลายรายการรองรับเอาต์พุตในคอมโพเนนต์ของเว็บ ซึ่งหมายความว่าโดยปกติแล้วเฟรมเวิร์กเหล่านี้จะพอดีกับองค์ประกอบ Lit เอง
3. การตั้งค่าและการสำรวจ Playground
คุณสามารถทำ Codelab นี้ได้ 2 วิธีดังนี้
- คุณสามารถทำทุกอย่างทางออนไลน์ในเบราว์เซอร์ได้
- (ขั้นสูง) คุณสามารถทำได้ในเครื่องของคุณโดยใช้ VS Code
การเข้าถึงรหัส
ในตลอดทั้งโค้ดแล็บจะมีลิงก์ไปยัง Lit Playground ดังนี้
สนามเด็กเล่นคือแซนด์บ็อกซ์โค้ดที่ทำงานในเบราว์เซอร์ของคุณโดยสมบูรณ์ โดยสามารถคอมไพล์และเรียกใช้ไฟล์ TypeScript และ JavaScript รวมถึงสามารถแก้ไขการนำเข้าไปยังโมดูล Node ได้โดยอัตโนมัติ เช่น
// before
import './my-file.js';
import 'lit';
// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';
คุณสามารถทำตามบทแนะนำทั้งหมดใน Lit Playground โดยใช้จุดตรวจสอบเหล่านี้เป็นจุดเริ่มต้น หากใช้ VS Code คุณสามารถใช้จุดตรวจสอบเหล่านี้เพื่อดาวน์โหลดโค้ดเริ่มต้นสำหรับขั้นตอนใดก็ได้ รวมถึงใช้เพื่อตรวจสอบงานของคุณ
การสำรวจ UI ของฟีเจอร์ Most Searched Playground

ภาพหน้าจอ UI ของ Lit Playground จะไฮไลต์ส่วนที่คุณจะใช้ในโค้ดแล็บนี้
- ตัวเลือกไฟล์ โปรดสังเกตปุ่มบวก...
- โปรแกรมแก้ไขไฟล์
- ตัวอย่างโค้ด
- ปุ่มโหลดซ้ำ
- ปุ่มดาวน์โหลด
การตั้งค่า VS Code (ขั้นสูง)
ประโยชน์ของการใช้การตั้งค่า VS Code นี้มีดังนี้
- การตรวจสอบประเภทเทมเพลต
- IntelliSense และการเติมข้อความอัตโนมัติของเทมเพลต
หากคุณติดตั้ง NPM, VS Code (พร้อมปลั๊กอิน lit-plugin) ไว้แล้วและรู้วิธีใช้สภาพแวดล้อมดังกล่าว คุณก็เพียงดาวน์โหลดและเริ่มโปรเจ็กต์เหล่านี้ได้โดยทำดังนี้
- กดปุ่มดาวน์โหลด
- แตกเนื้อหาของไฟล์ tar ลงในไดเรกทอรี
- (หากเป็น TS) ตั้งค่า quick tsconfig ที่เอาต์พุตเป็นโมดูล ES และ es2015+
- ติดตั้งเซิร์ฟเวอร์สำหรับนักพัฒนาซอฟต์แวร์ที่สามารถระบุโมดูลเปล่า (ทีม Lit ขอแนะนำ @web/dev-server)
- ตัวอย่าง
package.json
- ตัวอย่าง
- เรียกใช้เซิร์ฟเวอร์สำหรับนักพัฒนาซอฟต์แวร์และเปิดเบราว์เซอร์ (หากใช้ @web/dev-server คุณจะใช้
npx web-dev-server --node-resolve --watch --openได้)- หากคุณใช้ตัวอย่าง
package.jsonให้ใช้npm run dev
- หากคุณใช้ตัวอย่าง
4. JSX และการสร้างเทมเพลต
ในส่วนนี้ คุณจะได้เรียนรู้พื้นฐานของการใช้เทมเพลตใน Lit
เทมเพลต JSX และ Lit
JSX เป็นส่วนขยายไวยากรณ์ของ JavaScript ที่ช่วยให้ผู้ใช้ React เขียนเทมเพลตในโค้ด JavaScript ได้อย่างง่ายดาย เทมเพลต Lit มีวัตถุประสงค์คล้ายกัน นั่นคือการแสดง UI ของคอมโพเนนต์เป็นฟังก์ชันของสถานะ
ไวยากรณ์พื้นฐาน
ใน React คุณจะแสดงผล JSX "Hello World" ได้ดังนี้
import 'react';
import ReactDOM from 'react-dom';
const name = 'Josh Perez';
const element = (
<>
<h1>Hello, {name}</h1>
<div>How are you?</div>
</>
);
ReactDOM.render(
element,
mountNode
);
ในตัวอย่างข้างต้น มีองค์ประกอบ 2 รายการและตัวแปร "name" ที่รวมอยู่ ใน Lit คุณจะดำเนินการดังนี้
import {html, render} from 'lit';
const name = 'Josh Perez';
const element = html`
<h1>Hello, ${name}</h1>
<div>How are you?</div>`;
render(
element,
mountNode
);
โปรดทราบว่าเทมเพลต Lit ไม่จำเป็นต้องมี React Fragment เพื่อจัดกลุ่มองค์ประกอบหลายรายการในเทมเพลต
ใน Lit เทมเพลตจะอยู่ใน html tagged template LITeral ซึ่งเป็นที่มาของชื่อ Lit
ค่าเทมเพลต
เทมเพลต Lit สามารถยอมรับเทมเพลต Lit อื่นๆ ซึ่งเรียกว่า TemplateResult เช่น ครอบ name ด้วยแท็กตัวเอียง (<i>) และครอบด้วยแท็กเทมเพลตลิเทอรัล N.B. อย่าลืมใช้เครื่องหมายแบ็กทิก (`) ไม่ใช่เครื่องหมายคำพูดเดี่ยว (')
import {html, render} from 'lit';
const name = html`<i>Josh Perez</i>`;
const element = html`
<h1>Hello, ${name}</h1>
<div>How are you?</div>`;
render(
element,
mountNode
);
Lit TemplateResults สามารถรับอาร์เรย์ สตริง TemplateResults อื่นๆ รวมถึงคำสั่งได้
ลองแปลงโค้ด React ต่อไปนี้เป็น Lit เพื่อเป็นการฝึก
const itemsToBuy = [
<li>Bananas</li>,
<li>oranges</li>,
<li>apples</li>,
<li>grapes</li>
];
const element = (
<>
<h1>Things to buy:</h1>
<ol>
{itemsToBuy}
</ol>
</>);
ReactDOM.render(
element,
mountNode
);
คำตอบ:
import {html, render} from 'lit';
const itemsToBuy = [
html`<li>Bananas</li>`,
html`<li>oranges</li>`,
html`<li>apples</li>`,
html`<li>grapes</li>`
];
const element = html`
<h1>Things to buy:</h1>
<ol>
${itemsToBuy}
</ol>`;
render(
element,
mountNode
);
การส่งและการตั้งค่าพร็อพ
ความแตกต่างที่สำคัญอย่างหนึ่งระหว่างไวยากรณ์ของ JSX กับ Lit คือไวยากรณ์การเชื่อมโยงข้อมูล ตัวอย่างเช่น ลองดูอินพุต React นี้ที่มีการเชื่อมโยง
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
<input
disabled={disabled}
className={`static-class ${myClass}`}
defaultValue={value}/>;
ReactDOM.render(
element,
mountNode
);
ในตัวอย่างข้างต้น มีการกำหนดอินพุตซึ่งทำสิ่งต่อไปนี้
- ตั้งค่า "ปิดใช้" เป็นตัวแปรที่กำหนด (ในกรณีนี้คือ "เท็จ")
- ตั้งค่าคลาสเป็น
static-classบวกตัวแปร (ในกรณีนี้คือ"static-class my-class") - ตั้งค่าเริ่มต้น
ใน Lit คุณจะดำเนินการดังนี้
import {html, render} from 'lit';
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
<input
?disabled=${disabled}
class="static-class ${myClass}"
.value=${value}>`;
render(
element,
mountNode
);
ในตัวอย่าง Lit มีการเพิ่มการเชื่อมโยงบูลีนเพื่อสลับแอตทริบิวต์ disabled
จากนั้นจะมีการเชื่อมโยงกับแอตทริบิวต์ class โดยตรงแทนที่จะเป็น className คุณเพิ่มการเชื่อมโยงหลายรายการลงในแอตทริบิวต์ class ได้ เว้นแต่จะใช้คำสั่ง classMap ซึ่งเป็นตัวช่วยแบบประกาศสำหรับการสลับคลาส
สุดท้ายก็ตั้งค่าพร็อพเพอร์ตี้ value ในอินพุต ซึ่งต่างจากใน React ตรงที่การดำเนินการนี้จะไม่ตั้งค่าองค์ประกอบอินพุตเป็นแบบอ่านอย่างเดียว เนื่องจากเป็นไปตามการใช้งานและลักษณะการทำงานของอินพุตแบบเนทีฟ
ไวยากรณ์การเชื่อมโยงพร็อพลิท
html`<my-element ?attribute-name=${booleanVar}>`;
- คำนำหน้า
?คือไวยากรณ์การเชื่อมโยงสำหรับการเปิด/ปิดแอตทริบิวต์ในองค์ประกอบ - เทียบเท่ากับ
inputRef.toggleAttribute('attribute-name', booleanVar) - มีประโยชน์สำหรับองค์ประกอบที่ใช้
disabledเนื่องจาก DOM ยังคงอ่านdisabled="false"เป็นจริงเพราะinputElement.hasAttribute('disabled') === true
html`<my-element .property-name=${anyVar}>`;
- คำนำหน้า
.คือไวยากรณ์การเชื่อมโยงสำหรับการตั้งค่าพร็อพเพอร์ตี้ขององค์ประกอบ - เทียบเท่ากับ
inputRef.propertyName = anyVar - เหมาะสำหรับการส่งข้อมูลที่ซับซ้อน เช่น ออบเจ็กต์ อาร์เรย์ หรือคลาส
html`<my-element attribute-name=${stringVar}>`;
- เชื่อมโยงกับแอตทริบิวต์ขององค์ประกอบ
- เทียบเท่ากับ
inputRef.setAttribute('attribute-name', stringVar) - เหมาะสำหรับค่าพื้นฐาน ตัวเลือกกฎสไตล์ และ querySelector
เครื่องจัดการการส่งต่อ
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
<input
onClick={() => console.log('click')}
onChange={e => console.log(e.target.value)} />;
ReactDOM.render(
element,
mountNode
);
ในตัวอย่างข้างต้น มีการกำหนดอินพุตซึ่งทำสิ่งต่อไปนี้
- บันทึกคำว่า "คลิก" เมื่อมีการคลิกอินพุต
- บันทึกค่าของอินพุตเมื่อผู้ใช้พิมพ์อักขระ
ใน Lit คุณจะดำเนินการดังนี้
import {html, render} from 'lit';
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
<input
@click=${() => console.log('click')}
@input=${e => console.log(e.target.value)}>`;
render(
element,
mountNode
);
ในตัวอย่าง Lit มีการเพิ่ม Listener ลงในเหตุการณ์ click ด้วย @click
จากนั้นแทนที่จะใช้ onChange จะมีการเชื่อมโยงกับเหตุการณ์ input ดั้งเดิมของ <input> เนื่องจากเหตุการณ์ change ดั้งเดิมจะเริ่มทำงานใน blur เท่านั้น (React จะสรุปเหตุการณ์เหล่านี้)
ไวยากรณ์ตัวแฮนเดิลเหตุการณ์ Lit
html`<my-element @event-name=${() => {...}}></my-element>`;
- คำนำหน้า
@คือไวยากรณ์การเชื่อมโยงสำหรับ Listener เหตุการณ์ - เทียบเท่ากับ
inputRef.addEventListener('event-name', ...) - ใช้ชื่อเหตุการณ์ DOM ดั้งเดิม
5. คอมโพเนนต์และพร็อพ
ในส่วนนี้ คุณจะได้เรียนรู้เกี่ยวกับคอมโพเนนต์และฟังก์ชันของคลาส Lit เราจะอธิบาย State และ Hook โดยละเอียดในส่วนต่อๆ ไป
คอมโพเนนต์ของคลาสและ LitElement
ส่วนประกอบคลาส React ที่เทียบเท่ากับ Lit คือ LitElement และแนวคิด "พร็อพเพอร์ตี้แบบรีแอกทีฟ" ของ Lit คือการรวมพร็อพและสถานะของ React เช่น
import React from 'react';
import ReactDOM from 'react-dom';
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = {name: ''};
}
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
const element = <Welcome name="Elliott"/>
ReactDOM.render(
element,
mountNode
);
ในตัวอย่างด้านบนมีคอมโพเนนต์ React ที่ทำสิ่งต่อไปนี้
- แสดงผล
name - กำหนดค่าเริ่มต้นของ
nameเป็นสตริงว่าง ("") - มอบหมาย
nameให้"Elliott"ใหม่
วิธีดำเนินการใน LitElement
ใน TypeScript
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
@property({type: String})
name = '';
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
}
ใน JavaScript
import {LitElement, html} from 'lit';
class WelcomeBanner extends LitElement {
static get properties() {
return {
name: {type: String}
}
}
constructor() {
super();
this.name = '';
}
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
}
customElements.define('welcome-banner', WelcomeBanner);
และในไฟล์ HTML
<!-- index.html -->
<head>
<script type="module" src="./index.js"></script>
</head>
<body>
<welcome-banner name="Elliott"></welcome-banner>
</body>
รีวิวสิ่งที่เกิดขึ้นในตัวอย่างด้านบน
@property({type: String})
name = '';
- กำหนดพร็อพเพอร์ตี้แบบรีแอกทีฟสาธารณะ ซึ่งเป็นส่วนหนึ่งของ API สาธารณะของคอมโพเนนต์
- แสดงแอตทริบิวต์ (โดยค่าเริ่มต้น) รวมถึงพร็อพเพอร์ตี้ในคอมโพเนนต์
- กำหนดวิธีแปลแอตทริบิวต์ของคอมโพเนนต์ (ซึ่งเป็นสตริง) เป็นค่า
static get properties() {
return {
name: {type: String}
}
}
- ซึ่งมีฟังก์ชันการทำงานเหมือนกับ
@propertyTS decorator แต่ทำงานใน JavaScript โดยตรง
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
- ระบบจะเรียกใช้ฟังก์ชันนี้เมื่อใดก็ตามที่มีการเปลี่ยนแปลงพร็อพเพอร์ตี้แบบรีแอกทีฟ
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
...
}
- ซึ่งจะเชื่อมโยงชื่อแท็กองค์ประกอบ HTML กับคำจำกัดความของคลาส
- ชื่อแท็กต้องมีเครื่องหมายยัติภังค์ (-) เนื่องจากมาตรฐาน Custom Elements
thisใน LitElement หมายถึงอินสแตนซ์ขององค์ประกอบที่กำหนดเอง (<welcome-banner>ในกรณีนี้)
customElements.define('welcome-banner', WelcomeBanner);
- นี่คือ JavaScript ที่เทียบเท่ากับตัวตกแต่ง
@customElementTS
<head>
<script type="module" src="./index.js"></script>
</head>
- นำเข้าคําจํากัดความของ Custom Element
<body>
<welcome-banner name="Elliott"></welcome-banner>
</body>
- เพิ่มองค์ประกอบที่กำหนดเองลงในหน้าเว็บ
- ตั้งค่าพร็อพเพอร์ตี้
nameเป็น'Elliott'
คอมโพเนนต์ฟังก์ชัน
Lit ไม่มีการตีความคอมโพเนนต์ฟังก์ชันแบบ 1:1 เนื่องจากไม่ได้ใช้ JSX หรือตัวประมวลผลล่วงหน้า อย่างไรก็ตาม การเขียนฟังก์ชันที่รับพร็อพเพอร์ตี้และแสดงผล DOM ตามพร็อพเพอร์ตี้เหล่านั้นนั้นทำได้ง่ายมาก เช่น
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Elliott"/>
ReactDOM.render(
element,
mountNode
);
ใน Lit จะเป็นดังนี้
import {html, render} from 'lit';
function Welcome(props) {
return html`<h1>Hello, ${props.name}</h1>`;
}
render(
Welcome({name: 'Elliott'}),
document.body.querySelector('#root')
);
6. สถานะและวงจร
ในส่วนนี้ คุณจะได้เรียนรู้เกี่ยวกับสถานะและวงจรของ Lit
รัฐ
แนวคิด "พร็อพเพอร์ตี้แบบรีแอกทีฟ" ของ Lit เป็นการผสมผสานระหว่างสถานะและพร็อพส์ของ React เมื่อมีการเปลี่ยนแปลงพร็อพเพอร์ตี้แบบรีแอกทีฟ ก็จะทริกเกอร์วงจรของคอมโพเนนต์ได้ พร็อพเพอร์ตี้แบบรีแอกทีฟมี 2 รูปแบบ ได้แก่
พร็อพเพอร์ตี้แบบรีแอกทีฟสาธารณะ
// React
import React from 'react';
class MyEl extends React.Component {
constructor(props) {
super(props)
this.state = {name: 'there'}
}
componentWillReceiveProps(nextProps) {
if (this.props.name !== nextProps.name) {
this.setState({name: nextProps.name})
}
}
}
// Lit (TS)
import {LitElement} from 'lit';
import {property} from 'lit/decorators.js';
class MyEl extends LitElement {
@property() name = 'there';
}
- กำหนดโดย
@property - คล้ายกับพร็อพและสถานะของ React แต่เปลี่ยนแปลงได้
- API สาธารณะที่ผู้ใช้คอมโพเนนต์เข้าถึงและตั้งค่า
สถานะรีแอกทีฟภายใน
// React
import React from 'react';
class MyEl extends React.Component {
constructor(props) {
super(props)
this.state = {name: 'there'}
}
}
// Lit (TS)
import {LitElement} from 'lit';
import {state} from 'lit/decorators.js';
class MyEl extends LitElement {
@state() name = 'there';
}
- กำหนดโดย
@state - คล้ายกับสถานะของ React แต่เปลี่ยนแปลงได้
- สถานะภายในส่วนตัวที่มักเข้าถึงได้จากภายในคอมโพเนนต์หรือคลาสย่อย
อายุการใช้งาน
วงจรของ Lit คล้ายกับของ React มาก แต่ก็มีความแตกต่างที่สำคัญบางอย่าง
constructor
// React (js)
import React from 'react';
class MyEl extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this._privateProp = 'private';
}
}
// Lit (ts)
class MyEl extends LitElement {
@property({type: Number}) counter = 0;
private _privateProp = 'private';
}
// Lit (js)
class MyEl extends LitElement {
static get properties() {
return { counter: {type: Number} }
}
constructor() {
this.counter = 0;
this._privateProp = 'private';
}
}
- โดยมีค่าเท่ากับ
constructor - ไม่จำเป็นต้องส่งอะไรไปยังการเรียกใช้ขั้นสูง
- เรียกใช้โดย (ไม่ครอบคลุมทั้งหมด):
document.createElementdocument.innerHTMLnew ComponentClass()- หากชื่อแท็กที่ไม่ได้อัปเกรดอยู่ในหน้าเว็บ และโหลดและลงทะเบียนคำจำกัดความด้วย
@customElementหรือcustomElements.define
- มีฟังก์ชันคล้ายกับ
constructorของ React
render
// React
render() {
return <div>Hello World</div>
}
// Lit
render() {
return html`<div>Hello World</div>`;
}
- โดยมีค่าเท่ากับ
render - สามารถแสดงผลลัพธ์ที่แสดงได้ เช่น
TemplateResultหรือstringเป็นต้น render()ควรเป็นฟังก์ชันที่ไม่มีผลข้างเคียงเช่นเดียวกับ React- จะแสดงผลไปยังโหนดที่
createRenderRoot()ส่งคืน (ShadowRootโดยค่าเริ่มต้น)
componentDidMount
componentDidMount คล้ายกับการรวมการเรียกกลับสำหรับวงจรของทั้ง firstUpdated และ connectedCallback ของ Lit
firstUpdated
import Chart from 'chart.js';
// React
componentDidMount() {
this._chart = new Chart(this.chartElRef.current, {...});
}
// Lit
firstUpdated() {
this._chart = new Chart(this.chartEl, {...});
}
- เรียกใช้ครั้งแรกเมื่อมีการแสดงผลเทมเพลตของคอมโพเนนต์ในรูทของคอมโพเนนต์
- จะเรียกใช้ก็ต่อเมื่อองค์ประกอบเชื่อมต่ออยู่เท่านั้น เช่น จะไม่เรียกใช้ผ่าน
document.createElement('my-component')จนกว่าจะเพิ่มโหนดนั้นลงในแผนผัง DOM - ซึ่งเป็นตำแหน่งที่เหมาะสมในการตั้งค่าคอมโพเนนต์ที่ต้องใช้ DOM ที่คอมโพเนนต์แสดงผล
- ซึ่งแตกต่างจาก React ที่การเปลี่ยนแปลงพร็อพเพอร์ตี้แบบรีแอกทีฟใน
firstUpdatedจะทําให้เกิดการแสดงผลซ้ำ แม้ว่าโดยปกติแล้วเบราว์เซอร์จะจัดกลุ่มการเปลี่ยนแปลงไว้ในเฟรมเดียวกันก็ตามcomponentDidMountหากการเปลี่ยนแปลงเหล่านั้นไม่จำเป็นต้องเข้าถึง DOM ของรูท โดยปกติแล้วการเปลี่ยนแปลงเหล่านั้นควรอยู่ในwillUpdate
connectedCallback
// React
componentDidMount() {
this.window.addEventListener('resize', this.boundOnResize);
}
// Lit
connectedCallback() {
super.connectedCallback();
this.window.addEventListener('resize', this.boundOnResize);
}
- เรียกใช้ทุกครั้งที่มีการแทรกองค์ประกอบที่กำหนดเองลงใน DOM Tree
- เมื่อองค์ประกอบที่กำหนดเองไม่ได้เชื่อมต่อกับ DOM ระบบจะไม่ทำลายองค์ประกอบดังกล่าว ซึ่งต่างจากคอมโพเนนต์ React จึงสามารถ "เชื่อมต่อ" ได้หลายครั้ง
- ระบบจะไม่เรียกใช้
firstUpdatedอีก
- ระบบจะไม่เรียกใช้
- มีประโยชน์สำหรับการเริ่มต้น DOM อีกครั้งหรือการแนบ Listener เหตุการณ์ที่ล้างข้อมูลเมื่อยกเลิกการเชื่อมต่อ
- หมายเหตุ:
connectedCallbackอาจเรียกใช้ก่อนfirstUpdatedดังนั้นในการเรียกใช้ครั้งแรก DOM อาจไม่พร้อมใช้งาน
componentDidUpdate
// React
componentDidUpdate(prevProps) {
if (this.props.title !== prevProps.title) {
this._chart.setTitle(this.props.title);
}
}
// Lit (ts)
updated(prevProps: PropertyValues<this>) {
if (prevProps.has('title')) {
this._chart.setTitle(this.title);
}
}
- คำที่เทียบเท่าคือ
updated(ใช้กริยาช่อง 2 ของคำว่า "อัปเดต" ในภาษาอังกฤษ) updatedจะเรียกใช้ในการแสดงผลครั้งแรกด้วย ซึ่งแตกต่างจาก React- มีฟังก์ชันคล้ายกับ
componentDidUpdateของ React
componentWillUnmount
// React
componentWillUnmount() {
this.window.removeEventListener('resize', this.boundOnResize);
}
// Lit
disconnectedCallback() {
super.disconnectedCallback();
this.window.removeEventListener('resize', this.boundOnResize);
}
- เทียบเท่ากับ
disconnectedCallback - เมื่อปลดองค์ประกอบที่กำหนดเองออกจาก DOM ระบบจะไม่ทำลายคอมโพเนนต์ ซึ่งต่างจากคอมโพเนนต์ React
disconnectedCallbackจะเรียกใช้หลังจากนำองค์ประกอบออกจากทรีแล้ว ซึ่งแตกต่างจากcomponentWillUnmount- DOM ภายในรูทยังคงเชื่อมต่อกับซับทรีของรูท
- มีประโยชน์ในการล้างข้อมูล Listener เหตุการณ์และการอ้างอิงที่รั่วไหลเพื่อให้เบราว์เซอร์สามารถรวบรวมขยะของคอมโพเนนต์ได้
การออกกำลังกาย
import React from 'react';
import ReactDOM from 'react-dom';
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
ในตัวอย่างด้านบน มีนาฬิกาแบบง่ายที่ทำสิ่งต่อไปนี้
- ซึ่งจะแสดงผล "Hello World! ตอนนี้เป็นเวลา" แล้วแสดงเวลา
- ทุกวินาทีจะอัปเดตนาฬิกา
- เมื่อถอดออก ระบบจะล้างช่วงเวลาที่เรียกการอัปเดต
ก่อนอื่นให้เริ่มด้วยการประกาศคลาสคอมโพเนนต์
// Lit (TS)
// some imports here are imported in advance
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';
@customElement('lit-clock')
class LitClock extends LitElement {
}
// Lit (JS)
// `html` is imported in advance
import {LitElement, html} from 'lit';
class LitClock extends LitElement {
}
customElements.define('lit-clock', LitClock);
จากนั้นเริ่มต้น date และประกาศให้เป็นพร็อพเพอร์ตี้รีแอกทีฟภายในด้วย @state เนื่องจากผู้ใช้คอมโพเนนต์จะไม่ตั้งค่า date โดยตรง
// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';
@customElement('lit-clock')
class LitClock extends LitElement {
@state() // declares internal reactive prop
private date = new Date(); // initialization
}
// Lit (JS)
import {LitElement, html} from 'lit';
class LitClock extends LitElement {
static get properties() {
return {
// declares internal reactive prop
date: {state: true}
}
}
constructor() {
super();
// initialization
this.date = new Date();
}
}
customElements.define('lit-clock', LitClock);
จากนั้นแสดงผลเทมเพลต
// Lit (JS & TS)
render() {
return html`
<div>
<h1>Hello, World!</h1>
<h2>It is ${this.date.toLocaleTimeString()}.</h2>
</div>
`;
}
ตอนนี้ให้ใช้เมธอดเครื่องหมายถูก
tick() {
this.date = new Date();
}
ขั้นตอนถัดมาคือการติดตั้งใช้งาน componentDidMount อีกครั้งที่ Lit Analog เป็นส่วนผสมของ firstUpdated และ connectedCallback ในกรณีของคอมโพเนนต์นี้ การเรียกใช้ tick ด้วย setInterval ไม่จำเป็นต้องเข้าถึง DOM ภายในรูท นอกจากนี้ ระบบจะล้างช่วงเวลาเมื่อนำองค์ประกอบออกจากโครงสร้างเอกสาร ดังนั้นหากมีการแนบองค์ประกอบอีกครั้ง คุณจะต้องเริ่มช่วงเวลาใหม่ ดังนั้น connectedCallback จึงเป็นตัวเลือกที่ดีกว่าในที่นี้
// Lit (TS)
@customElement('lit-clock')
class LitClock extends LitElement {
@state()
private date = new Date();
// initialize timerId for TS
private timerId = -1 as unknown as ReturnType<typeof setTimeout>;
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
...
}
// Lit (JS)
constructor() {
super();
// initialization
this.date = new Date();
this.timerId = -1; // initialize timerId for JS
}
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
สุดท้าย ให้ล้างช่วงเวลาเพื่อไม่ให้มีการดำเนินการ "tick" หลังจากที่องค์ประกอบถูกตัดการเชื่อมต่อจากแผนผังเอกสาร
// Lit (TS & JS)
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timerId);
}
เมื่อรวมทุกอย่างเข้าด้วยกันแล้ว โค้ดควรมีลักษณะดังนี้
// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';
@customElement('lit-clock')
class LitClock extends LitElement {
@state()
private date = new Date();
private timerId = -1 as unknown as ReturnType<typeof setTimeout>;
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
tick() {
this.date = new Date();
}
render() {
return html`
<div>
<h1>Hello, World!</h1>
<h2>It is ${this.date.toLocaleTimeString()}.</h2>
</div>
`;
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timerId);
}
}
// Lit (JS)
import {LitElement, html} from 'lit';
class LitClock extends LitElement {
static get properties() {
return {
date: {state: true}
}
}
constructor() {
super();
this.date = new Date();
}
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
tick() {
this.date = new Date();
}
render() {
return html`
<div>
<h1>Hello, World!</h1>
<h2>It is ${this.date.toLocaleTimeString()}.</h2>
</div>
`;
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timerId);
}
}
customElements.define('lit-clock', LitClock);
7. ฮุก
ในส่วนนี้ คุณจะได้เรียนรู้วิธีเปลี่ยนแนวคิดของ React Hook เป็น Lit
แนวคิดของ React Hooks
React Hook เป็นวิธีให้คอมโพเนนต์ฟังก์ชัน "เชื่อมต่อ" กับสถานะ ซึ่งมีประโยชน์หลายประการ
- ซึ่งจะลดความซับซ้อนของการนำตรรกะที่มีสถานะกลับมาใช้ซ้ำ
- ช่วยแยกคอมโพเนนต์ออกเป็นฟังก์ชันย่อยๆ
นอกจากนี้ การมุ่งเน้นที่คอมโพเนนต์แบบฟังก์ชันยังช่วยแก้ปัญหาบางอย่างเกี่ยวกับไวยากรณ์แบบคลาสของ React เช่น
- ต้องผ่าน
propsจากconstructorไปยังsuper - การเริ่มต้นพร็อพเพอร์ตี้ที่ไม่เป็นระเบียบใน
constructor- นี่เป็นเหตุผลที่ทีม React ระบุไว้ในขณะนั้น แต่ ES2019 ได้แก้ไขปัญหานี้แล้ว
- ปัญหาที่เกิดจาก
thisไม่ได้อ้างอิงถึงคอมโพเนนต์อีกต่อไป
แนวคิดเกี่ยวกับ React Hook ใน Lit
ดังที่กล่าวไว้ในส่วนคอมโพเนนต์และพร็อพส์ Lit ไม่มีวิธีสร้าง Custom Elements จากฟังก์ชัน แต่ LitElement จะแก้ปัญหาหลักส่วนใหญ่ของคอมโพเนนต์คลาส React เช่น
// React (at the time of making hooks)
import React from 'react';
import ReactDOM from 'react-dom';
class MyEl extends React.Component {
constructor(props) {
super(props); // Leaky implementation
this.state = {count: 0};
this._chart = null; // Deemed messy
}
render() {
return (
<>
<div>Num times clicked {count}</div>
<button onClick={this.clickCallback}>click me</button>
</>
);
}
clickCallback() {
// Errors because `this` no longer refers to the component
this.setState({count: this.count + 1});
}
}
// Lit (ts)
class MyEl extends LitElement {
@property({type: Number}) count = 0; // No need for constructor to set state
private _chart = null; // Public class fields introduced to JS in 2019
render() {
return html`
<div>Num times clicked ${count}</div>
<button @click=${this.clickCallback}>click me</button>`;
}
private clickCallback() {
// No error because `this` refers to component
this.count++;
}
}
Lit แก้ไขปัญหาเหล่านี้อย่างไร
constructorไม่รับอาร์กิวเมนต์- การเชื่อมโยง
@eventทั้งหมดจะเชื่อมโยงกับthisโดยอัตโนมัติ thisในกรณีส่วนใหญ่จะหมายถึงการอ้างอิงขององค์ประกอบที่กำหนดเอง- ตอนนี้คุณสามารถสร้างอินสแตนซ์ของพร็อพเพอร์ตี้ของคลาสเป็นสมาชิกของคลาสได้แล้ว ซึ่งจะล้างข้อมูลการใช้งานที่อิงตามตัวสร้าง
เครื่องมือควบคุมแบบรีแอ็กทีฟ
แนวคิดหลักเบื้องหลัง Hook มีอยู่ใน Lit ในรูปแบบตัวควบคุมแบบรีแอกทีฟ รูปแบบตัวควบคุมแบบรีแอกทีฟช่วยให้แชร์ตรรกะที่มีสถานะ แบ่งคอมโพเนนต์ออกเป็นส่วนเล็กๆ ที่เป็นโมดูลมากขึ้น รวมถึงเชื่อมต่อกับวงจรการอัปเดตขององค์ประกอบได้
ตัวควบคุมแบบรีแอ็กทีฟคืออินเทอร์เฟซออบเจ็กต์ที่เชื่อมต่อกับวงจรการอัปเดตของโฮสต์ตัวควบคุม เช่น LitElement
วงจรของ ReactiveController และ reactiveControllerHost มีดังนี้
interface ReactiveController {
hostConnected(): void;
hostUpdate(): void;
hostUpdated(): void;
hostDisconnected(): void;
}
interface ReactiveControllerHost {
addController(controller: ReactiveController): void;
removeController(controller: ReactiveController): void;
requestUpdate(): void;
readonly updateComplete: Promise<boolean>;
}
การสร้างคอนโทรลเลอร์แบบรีแอกทีฟและแนบคอนโทรลเลอร์กับโฮสต์ที่มี addController จะทำให้ระบบเรียกวงจรของคอนโทรลเลอร์พร้อมกับวงจรของโฮสต์ ตัวอย่างเช่น ลองนึกถึงตัวอย่างนาฬิกาจากส่วนสถานะและวงจรของคอมโพเนนต์
import React from 'react';
import ReactDOM from 'react-dom';
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
ในตัวอย่างด้านบน มีนาฬิกาแบบง่ายที่ทำสิ่งต่อไปนี้
- ซึ่งจะแสดงผล "Hello World! ตอนนี้เป็นเวลา" แล้วแสดงเวลา
- ทุกวินาทีจะอัปเดตนาฬิกา
- เมื่อถอดออก ระบบจะล้างช่วงเวลาที่เรียกการอัปเดต
การสร้างโครงร่างคอมโพเนนต์
ก่อนอื่นให้เริ่มด้วยการประกาศคลาสคอมโพเนนต์และเพิ่มฟังก์ชัน render
// Lit (TS) - index.ts
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';
@customElement('my-element')
class MyElement extends LitElement {
render() {
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${'time to get Lit'}.</h2>
</div>
`;
}
}
// Lit (JS) - index.js
import {LitElement, html} from 'lit';
class MyElement extends LitElement {
render() {
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${'time to get Lit'}.</h2>
</div>
`;
}
}
customElements.define('my-element', MyElement);
การสร้างตัวควบคุม
ตอนนี้ให้เปลี่ยนไปใช้ clock.ts แล้วสร้างชั้นเรียนสำหรับ ClockController และตั้งค่า constructor ดังนี้
// Lit (TS) - clock.ts
import {ReactiveController, ReactiveControllerHost} from 'lit';
export class ClockController implements ReactiveController {
private readonly host: ReactiveControllerHost;
constructor(host: ReactiveControllerHost) {
this.host = host;
host.addController(this);
}
hostConnected() {
}
private tick() {
}
hostDisconnected() {
}
}
// Lit (JS) - clock.js
export class ClockController {
constructor(host) {
this.host = host;
host.addController(this);
}
hostConnected() {
}
tick() {
}
hostDisconnected() {
}
}
คุณสร้างตัวควบคุมแบบรีแอกทีฟได้ทุกวิธีตราบใดที่แชร์อินเทอร์เฟซ ReactiveController แต่การใช้คลาสที่มี constructor ซึ่งรับอินเทอร์เฟซ ReactiveControllerHost รวมถึงพร็อพเพอร์ตี้อื่นๆ ที่จำเป็นในการเริ่มต้นตัวควบคุมได้เป็นรูปแบบที่ทีม Lit ชอบใช้ในกรณีพื้นฐานส่วนใหญ่
ตอนนี้คุณต้องแปล Callback ของวงจรการทำงานของ React เป็น Callback ของตัวควบคุม โดยสรุป
componentDidMount- ไปที่
connectedCallbackของ LitElement - ไปยัง
hostConnectedของตัวควบคุม
- ไปที่
ComponentWillUnmount- ไปที่
disconnectedCallbackของ LitElement - ไปยัง
hostDisconnectedของตัวควบคุม
- ไปที่
ดูข้อมูลเพิ่มเติมเกี่ยวกับการแปลวงจรของ React เป็นวงจรของ Lit ได้ที่ส่วนสถานะและวงจร
จากนั้นใช้เมธอด hostConnected และ tick รวมถึงล้างช่วงเวลาใน hostDisconnected ตามตัวอย่างในส่วนสถานะและวงจร
// Lit (TS) - clock.ts
export class ClockController implements ReactiveController {
private readonly host: ReactiveControllerHost;
private interval = 0 as unknown as ReturnType<typeof setTimeout>;
date = new Date();
constructor(host: ReactiveControllerHost) {
this.host = host;
host.addController(this);
}
hostConnected() {
this.interval = setInterval(() => this.tick(), 1000);
}
private tick() {
this.date = new Date();
}
hostDisconnected() {
clearInterval(this.interval);
}
}
// Lit (JS) - clock.js
export class ClockController {
interval = 0;
host;
date = new Date();
constructor(host) {
this.host = host;
host.addController(this);
}
hostConnected() {
this.interval = setInterval(() => this.tick(), 1000);
}
tick() {
this.date = new Date();
}
hostDisconnected() {
clearInterval(this.interval);
}
}
การใช้คอนโทรลเลอร์
หากต้องการใช้ตัวควบคุมนาฬิกา ให้นำเข้าตัวควบคุมและอัปเดตคอมโพเนนต์ใน index.ts หรือ index.js
// Lit (TS) - index.ts
import {LitElement, html, ReactiveController, ReactiveControllerHost} from 'lit';
import {customElement} from 'lit/decorators.js';
import {ClockController} from './clock.js';
@customElement('my-element')
class MyElement extends LitElement {
private readonly clock = new ClockController(this); // Instantiate
render() {
// Use controller
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
</div>
`;
}
}
// Lit (JS) - index.js
import {LitElement, html} from 'lit';
import {ClockController} from './clock.js';
class MyElement extends LitElement {
clock = new ClockController(this); // Instantiate
render() {
// Use controller
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
</div>
`;
}
}
customElements.define('my-element', MyElement);
หากต้องการใช้ตัวควบคุม คุณต้องสร้างอินสแตนซ์ของตัวควบคุมโดยส่งการอ้างอิงไปยังโฮสต์ตัวควบคุม (ซึ่งคือคอมโพเนนต์ <my-element>) จากนั้นใช้ตัวควบคุมในเมธอด render
การเรียกใช้การแสดงผลซ้ำในตัวควบคุม
โปรดสังเกตว่าเวลาจะแสดง แต่เวลาจะไม่ได้รับการอัปเดต เนื่องจากตัวควบคุมตั้งค่าวันที่ทุกวินาที แต่โฮสต์ไม่ได้อัปเดต เนื่องจาก date จะเปลี่ยนในคลาส ClockController และไม่ใช่ในคอมโพเนนต์อีกต่อไป ซึ่งหมายความว่าหลังจากตั้งค่า date ในตัวควบคุมแล้ว คุณต้องแจ้งให้โฮสต์เรียกใช้วงจรการอัปเดตด้วย host.requestUpdate()
// Lit (TS & JS) - clock.ts / clock.js
private tick() {
this.date = new Date();
this.host.requestUpdate();
}
ตอนนี้ตัวจับเวลาควรเริ่มทำงานแล้ว
ดูการเปรียบเทียบกรณีการใช้งานทั่วไปกับ Hook ในเชิงลึกเพิ่มเติมได้ที่ส่วนหัวข้อขั้นสูง - Hook
8. เด็ก
ในส่วนนี้ คุณจะได้เรียนรู้วิธีใช้ช่องเพื่อจัดการบุตรหลานใน Lit
สล็อตและเด็ก
สล็อตช่วยให้เขียนคอมโพสิชันได้โดยให้คุณซ้อนคอมโพเนนต์
ใน React จะรับค่าพร็อพของคอมโพเนนต์ลูกผ่านพร็อพ สล็อตเริ่มต้นคือ props.children และฟังก์ชัน render จะกำหนดตำแหน่งของสล็อตเริ่มต้น เช่น
const MyArticle = (props) => {
return <article>{props.children}</article>;
};
โปรดทราบว่า props.children เป็นคอมโพเนนต์ React ไม่ใช่องค์ประกอบ HTML
ใน Lit องค์ประกอบย่อยจะประกอบขึ้นในฟังก์ชันการแสดงผลด้วยองค์ประกอบช่อง โปรดทราบว่าระบบจะไม่รับค่าพร็อพขององค์ประกอบย่อยในลักษณะเดียวกับ React ใน Lit องค์ประกอบย่อยคือ HTMLElements ที่แนบมากับช่อง ไฟล์แนบนี้เรียกว่าการฉายภาพ
@customElement("my-article")
export class MyArticle extends LitElement {
render() {
return html`
<article>
<slot></slot>
</article>
`;
}
}
หลายช่อง
ใน React การเพิ่มช่องหลายช่องจะเหมือนกับการรับพร็อพเพิ่มเติม
const MyArticle = (props) => {
return (
<article>
<header>
{props.headerChildren}
</header>
<section>
{props.sectionChildren}
</section>
</article>
);
};
ในทำนองเดียวกัน การเพิ่มองค์ประกอบ <slot> จะสร้างช่องเพิ่มเติมใน Lit มีการกำหนดช่องหลายช่องด้วยแอตทริบิวต์ name: <slot name="slot-name"> ซึ่งจะช่วยให้บุตรหลานประกาศได้ว่าจะได้รับมอบหมายให้ใช้ช่องใด
@customElement("my-article")
export class MyArticle extends LitElement {
render() {
return html`
<article>
<header>
<slot name="headerChildren"></slot>
</header>
<section>
<slot name="sectionChildren"></slot>
</section>
</article>
`;
}
}
เนื้อหาในช่องเริ่มต้น
ช่องจะแสดง Subtree เมื่อไม่มีโหนดที่ฉายไปยังช่องนั้น เมื่อฉายโหนดไปยังช่อง ช่องนั้นจะไม่แสดงซับทรีของตัวเอง แต่จะแสดงโหนดที่ฉายแทน
@customElement("my-element")
export class MyElement extends LitElement {
render() {
return html`
<section>
<div>
<slot name="slotWithDefault">
<p>
This message will not be rendered when children are attached to this slot!
<p>
</slot>
</div>
</section>
`;
}
}
มอบหมายเด็กให้เข้าร่วมช่วงเวลา
ใน React ระบบจะกำหนดองค์ประกอบย่อยให้กับช่องผ่านพร็อพเพอร์ตี้ของคอมโพเนนต์ ในตัวอย่างด้านล่าง องค์ประกอบ React จะส่งไปยังพร็อพ headerChildren และ sectionChildren
const MyNewsArticle = () => {
return (
<MyArticle
headerChildren={<h3>Extry, Extry! Read all about it!</h3>}
sectionChildren={<p>Children are props in React!</p>}
/>
);
};
ใน Lit จะมีการกำหนดองค์ประกอบย่อยให้กับช่องโดยใช้แอตทริบิวต์ slot
@customElement("my-news-article")
export class MyNewsArticle extends LitElement {
render() {
return html`
<my-article>
<h3 slot="headerChildren">
Extry, Extry! Read all about it!
</h3>
<p slot="sectionChildren">
Children are composed with slots in Lit!
</p>
</my-article>
`;
}
}
หากไม่มีช่องเริ่มต้น (เช่น <slot>) และไม่มีช่องที่มีแอตทริบิวต์ name (เช่น <slot name="foo">) ซึ่งตรงกับแอตทริบิวต์ slot ขององค์ประกอบย่อยขององค์ประกอบที่กำหนดเอง (เช่น <div slot="foo">) ระบบจะไม่ฉายโหนดนั้นและจะไม่แสดง
9. Refs
บางครั้งนักพัฒนาแอปอาจต้องเข้าถึง API ของ HTMLElement
ในส่วนนี้ คุณจะได้เรียนรู้วิธีรับการอ้างอิงองค์ประกอบใน Lit
การอ้างอิง React
คอมโพเนนต์ React จะได้รับการแปลงเป็นชุดการเรียกใช้ฟังก์ชันที่สร้าง DOM เสมือนเมื่อมีการเรียกใช้ ReactDOM จะตีความ DOM เสมือนนี้และแสดงผล HTMLElements
ใน React, Refs คือพื้นที่ในหน่วยความจำที่ใช้เก็บ HTMLElement ที่สร้างขึ้น
const RefsExample = (props) => {
const inputRef = React.useRef(null);
const onButtonClick = React.useCallback(() => {
inputRef.current?.focus();
}, [inputRef]);
return (
<div>
<input type={"text"} ref={inputRef} />
<br />
<button onClick={onButtonClick}>
Click to focus on the input above!
</button>
</div>
);
};
ในตัวอย่างด้านบน คอมโพเนนต์ React จะทําสิ่งต่อไปนี้
- แสดงผลช่องป้อนข้อความว่างเปล่าและปุ่มที่มีข้อความ
- โฟกัสอินพุตเมื่อคลิกปุ่ม
หลังจากแสดงผลครั้งแรกแล้ว React จะตั้งค่า inputRef.current เป็น HTMLInputElement ที่สร้างขึ้นผ่านแอตทริบิวต์ ref
สว่างไสวด้วย "การอ้างอิง" กับ @query
Lit ทำงานใกล้กับเบราว์เซอร์และสร้างการแยกส่วนที่บางมากเหนือฟีเจอร์เบราว์เซอร์ดั้งเดิม
ใน React refs เทียบเท่ากับ HTMLElement ใน Lit ซึ่งส่งคืนโดยตัวตกแต่ง @query และ @queryAll
@customElement("my-element")
export class MyElement extends LitElement {
@query('input') // Define the query
inputEl!: HTMLInputElement; // Declare the prop
// Declare the click event listener
onButtonClick() {
// Use the query to focus
this.inputEl.focus();
}
render() {
return html`
<input type="text">
<br />
<!-- Bind the click listener -->
<button @click=${this.onButtonClick}>
Click to focus on the input above!
</button>
`;
}
}
ในตัวอย่างด้านบน คอมโพเนนต์ Lit จะทำสิ่งต่อไปนี้
- กําหนดพร็อพเพอร์ตี้ใน
MyElementโดยใช้ตัวตกแต่ง@query(สร้าง Getter สําหรับHTMLInputElement) - ประกาศและแนบการเรียกกลับของเหตุการณ์คลิกที่ชื่อ
onButtonClick - โฟกัสอินพุตเมื่อคลิกปุ่ม
ใน JavaScript ตัวตกแต่ง @query และ @queryAll จะดำเนินการ querySelector และ querySelectorAll ตามลำดับ นี่คือ JavaScript ที่เทียบเท่ากับ @query('input') inputEl!: HTMLInputElement;
get inputEl() {
return this.renderRoot.querySelector('input');
}
หลังจากคอมโพเนนต์ Lit ยืนยันเทมเพลตของเมธอด render ไปยังรูทของ my-element แล้ว ตอนนี้ตัวตกแต่ง @query จะอนุญาตให้ inputEl แสดงผลองค์ประกอบ input แรกที่พบในรูทการแสดงผล โดยจะแสดง null หาก @query ไม่พบองค์ประกอบที่ระบุ
หากมีองค์ประกอบ input หลายรายการในรูทการแสดงผล @queryAll จะแสดงผลรายการโหนด
10. สถานะการไกล่เกลี่ย
ในส่วนนี้ คุณจะได้เรียนรู้วิธีจัดการสถานะระหว่างคอมโพเนนต์ใน Lit
คอมโพเนนต์ที่นำกลับมาใช้ใหม่ได้
React จำลองไปป์ไลน์การแสดงผลแบบฟังก์ชันด้วยการไหลของข้อมูลจากบนลงล่าง ผู้ปกครองจะแสดงสถานะให้เด็กๆ เห็นผ่านอุปกรณ์ประกอบ บุตรหลานจะสื่อสารกับผู้ปกครองผ่านฟังก์ชันเรียกกลับที่อยู่ในพร็อพ
const CounterButton = (props) => {
const label = props.step < 0
? `- ${-1 * props.step}`
: `+ ${props.step}`;
return (
<button
onClick={() =>
props.addToCounter(props.step)}>{label}</button>
);
};
ในตัวอย่างด้านบน คอมโพเนนต์ React จะทําสิ่งต่อไปนี้
- สร้างป้ายกำกับตามค่า
props.step - แสดงปุ่มที่มี +step หรือ -step เป็นป้ายกำกับ
- อัปเดตคอมโพเนนต์ระดับบนสุดโดยเรียกใช้
props.addToCounterโดยมีprops.stepเป็นอาร์กิวเมนต์เมื่อคลิก
แม้ว่าจะส่งการเรียกกลับใน Lit ได้ แต่รูปแบบทั่วไปจะแตกต่างกัน คอมโพเนนต์ React ในตัวอย่างด้านบนสามารถเขียนเป็นคอมโพเนนต์ Lit ในตัวอย่างด้านล่างได้ดังนี้
@customElement('counter-button')
export class CounterButton extends LitElement {
@property({type: Number}) step: number = 0;
onClick() {
const event = new CustomEvent('update-counter', {
bubbles: true,
detail: {
step: this.step,
}
});
this.dispatchEvent(event);
}
render() {
const label = this.step < 0
? `- ${-1 * this.step}` // "- 1"
: `+ ${this.step}`; // "+ 1"
return html`
<button @click=${this.onClick}>${label}</button>
`;
}
}
ในตัวอย่างด้านบน คอมโพเนนต์ Lit จะทําสิ่งต่อไปนี้
- สร้างพร็อพเพอร์ตี้แบบรีแอกทีฟ
step - ส่งเหตุการณ์ที่กําหนดเองชื่อ
update-counterซึ่งมีค่าstepขององค์ประกอบเมื่อคลิก
เหตุการณ์ของเบราว์เซอร์จะส่งต่อจากองค์ประกอบย่อยไปยังองค์ประกอบหลัก เหตุการณ์ช่วยให้บุตรหลานสามารถออกอากาศเหตุการณ์การโต้ตอบและการเปลี่ยนแปลงสถานะได้ โดยพื้นฐานแล้ว React จะส่งต่อสถานะในทิศทางตรงกันข้าม จึงไม่ค่อยพบเห็นคอมโพเนนต์ React ที่ส่งและรับฟังเหตุการณ์ในลักษณะเดียวกับคอมโพเนนต์ Lit
คอมโพเนนต์ที่มีสถานะ
ใน React การใช้ Hook เพื่อจัดการสถานะเป็นเรื่องปกติ MyCounter คอมโพเนนต์สามารถสร้างได้โดยการนำCounterButton คอมโพเนนต์มาใช้ซ้ำ สังเกตว่ามีการส่ง addToCounter ไปยังอินสแตนซ์ทั้ง 2 ของ CounterButton อย่างไร
const MyCounter = (props) => {
const [counterSum, setCounterSum] = React.useState(0);
const addToCounter = useCallback(
(step) => {
setCounterSum(counterSum + step);
},
[counterSum, setCounterSum]
);
return (
<div>
<h3>Σ: {counterSum}</h3>
<CounterButton
step={-1}
addToCounter={addToCounter} />
<CounterButton
step={1}
addToCounter={addToCounter} />
</div>
);
};
ตัวอย่างข้างต้นจะทำสิ่งต่อไปนี้
- สร้างสถานะ
count - สร้างการเรียกกลับที่เพิ่มหมายเลขไปยัง
countรัฐ CounterButtonใช้addToCounterเพื่ออัปเดตcountโดยstepทุกครั้งที่คลิก
คุณสามารถใช้งาน MyCounter ที่คล้ายกันใน Lit ได้ โปรดสังเกตว่าระบบจะไม่ส่ง addToCounter ไปยัง counter-button แต่จะผูกการเรียกกลับเป็น Listener เหตุการณ์กับเหตุการณ์ @update-counter ในองค์ประกอบระดับบนแทน
@customElement("my-counter")
export class MyCounter extends LitElement {
@property({type: Number}) count = 0;
addToCounter(e: CustomEvent<{step: number}>) {
// Get step from detail of event or via @query
this.count += e.detail.step;
}
render() {
return html`
<div @update-counter="${this.addToCounter}">
<h3>Σ ${this.count}</h3>
<counter-button step="-1"></counter-button>
<counter-button step="1"></counter-button>
</div>
`;
}
}
ตัวอย่างข้างต้นจะทำสิ่งต่อไปนี้
- สร้างพร็อพเพอร์ตี้แบบรีแอกทีฟชื่อ
countซึ่งจะอัปเดตคอมโพเนนต์เมื่อมีการเปลี่ยนค่า - เชื่อมโยง Callback
addToCounterกับ Listener เหตุการณ์@update-counter - อัปเดต
countโดยการเพิ่มค่าที่พบในdetail.stepของเหตุการณ์update-counter - ตั้งค่า
stepของcounter-buttonผ่านแอตทริบิวต์step
การใช้พร็อพเพอร์ตี้แบบรีแอกทีฟใน Lit เพื่อออกอากาศการเปลี่ยนแปลงจากองค์ประกอบหลักไปยังองค์ประกอบย่อยเป็นวิธีที่นิยมใช้กันมากกว่า ในทำนองเดียวกัน แนวทางปฏิบัติที่ดีคือการใช้ระบบเหตุการณ์ของเบราว์เซอร์เพื่อส่งต่อรายละเอียดจากล่างขึ้นบน
แนวทางนี้เป็นไปตามแนวทางปฏิบัติแนะนำและสอดคล้องกับเป้าหมายของ Lit ในการให้การสนับสนุนคอมโพเนนต์เว็บแบบข้ามแพลตฟอร์ม
11. การจัดรูปแบบ
ในส่วนนี้ คุณจะได้เรียนรู้เกี่ยวกับการจัดรูปแบบใน Lit
การจัดรูปแบบ
Lit มีหลายวิธีในการจัดรูปแบบองค์ประกอบ รวมถึงโซลูชันในตัว
รูปแบบในบรรทัด
Lit รองรับสไตล์อินไลน์และการเชื่อมโยงกับสไตล์เหล่านั้น
import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';
@customElement('my-element')
class MyElement extends LitElement {
render() {
return html`
<div>
<h1 style="color:orange;">This text is orange</h1>
<h1 style="color:rebeccapurple;">This text is rebeccapurple</h1>
</div>
`;
}
}
ในตัวอย่างข้างต้นมีส่วนหัว 2 รายการซึ่งแต่ละรายการมีสไตล์อินไลน์
ตอนนี้ให้นำเข้าและเชื่อมโยงเส้นขอบจาก border-color.js ไปยังข้อความสีส้ม
...
import borderColor from './border-color.js';
...
html`
...
<h1 style="color:orange;${borderColor}">This text is orange</h1>
...`
การคำนวณสตริงรูปแบบทุกครั้งอาจเป็นเรื่องที่น่ารำคาญเล็กน้อย ดังนั้น Lit จึงมีคำสั่งเพื่อช่วยในเรื่องนี้
styleMap
styleMap Directive ช่วยให้ใช้ JavaScript เพื่อตั้งค่าสไตล์อินไลน์ได้ง่ายขึ้น เช่น
import {LitElement, html, css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {styleMap} from 'lit/directives/style-map.js';
@customElement('my-element')
class MyElement extends LitElement {
@property({type: String})
color = '#000'
render() {
// Define the styleMap
const headerStyle = styleMap({
'border-color': this.color,
});
return html`
<div>
<h1
style="border-style:solid;
<!-- Use the styleMap -->
border-width:2px;${headerStyle}">
This div has a border color of ${this.color}
</h1>
<input
type="color"
@input=${e => (this.color = e.target.value)}
value="#000">
</div>
`;
}
}
ตัวอย่างข้างต้นจะทำสิ่งต่อไปนี้
- แสดง
h1ที่มีเส้นขอบและเครื่องมือเลือกสี - เปลี่ยน
border-colorเป็นค่าจากเครื่องมือเลือกสี
นอกจากนี้ ยังมี styleMap ซึ่งใช้เพื่อกำหนดรูปแบบของ h1 styleMap ใช้ไวยากรณ์ที่คล้ายกับไวยากรณ์การเชื่อมโยงแอตทริบิวต์ style ของ React
CSSResult
วิธีที่แนะนำในการจัดรูปแบบคอมโพเนนต์คือการใช้cssแท็กเทมเพลตสตริง
import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';
const ORANGE = css`orange`;
@customElement('my-element')
class MyElement extends LitElement {
static styles = [
css`
#orange {
color: ${ORANGE};
}
#purple {
color: rebeccapurple;
}
`
];
render() {
return html`
<div>
<h1 id="orange">This text is orange</h1>
<h1 id="purple">This text is rebeccapurple</h1>
</div>
`;
}
}
ตัวอย่างข้างต้นจะทำสิ่งต่อไปนี้
- ประกาศแท็กเทมเพลตลิเทอรัล CSS พร้อมการเชื่อมโยง
- ตั้งค่าสีของ
h12 รายการที่มีรหัส
ประโยชน์ของการใช้แท็กเทมเพลต css มีดังนี้
- แยกวิเคราะห์ 1 ครั้งต่อคลาสเทียบกับต่ออินสแตนซ์
- สร้างขึ้นโดยคำนึงถึงการนำโมดูลกลับมาใช้ซ้ำ
- แยกสไตล์ออกเป็นไฟล์ของตัวเองได้อย่างง่ายดาย
- ใช้ได้กับ Polyfill ของพร็อพเพอร์ตี้ CSS ที่กำหนดเอง
นอกจากนี้ โปรดสังเกตแท็ก <style> ใน index.html
<!-- index.html -->
<style>
h1 {
color: red !important;
}
</style>
Lit จะกำหนดขอบเขตของสไตล์คอมโพเนนต์ให้เป็นรูท ซึ่งหมายความว่าสไตล์จะไม่รั่วไหลเข้าและออก หากต้องการส่งสไตล์ไปยังคอมโพเนนต์ ทีม Lit ขอแนะนำให้ใช้พร็อพเพอร์ตี้ที่กำหนดเองของ CSS เนื่องจากพร็อพเพอร์ตี้เหล่านี้สามารถแทรกซึมการกำหนดขอบเขตสไตล์ของ Lit ได้
แท็กสไตล์
นอกจากนี้ คุณยังสามารถแทรกแท็ก <style> ในเทมเพลตได้ด้วย เบราว์เซอร์จะขจัดแท็กสไตล์ที่ซ้ำกัน แต่การวางแท็กเหล่านี้ในเทมเพลตจะทำให้ระบบแยกวิเคราะห์แท็กต่ออินสแตนซ์ของคอมโพเนนต์ แทนที่จะแยกวิเคราะห์ต่อคลาสเหมือนกับเทมเพลตที่ติดแท็ก css นอกจากนี้ การขจัดข้อมูลที่ซ้ำกันในเบราว์เซอร์ของ CSSResult ยังเร็วขึ้นมากด้วย
แท็กลิงก์
การใช้ <link rel="stylesheet"> ในเทมเพลตก็เป็นอีกทางเลือกหนึ่งสำหรับสไตล์ แต่เราไม่แนะนำให้ทำเช่นนี้เนื่องจากอาจทำให้เกิดการกะพริบของเนื้อหาที่ไม่มีสไตล์ (FOUC) ในตอนแรก
12. หัวข้อขั้นสูง (ไม่บังคับ)
JSX และการสร้างเทมเพลต
Lit และ Virtual DOM
Lit-html ไม่มี Virtual DOM แบบเดิมที่เปรียบเทียบแต่ละโหนด แต่จะใช้ฟีเจอร์ด้านประสิทธิภาพที่อยู่ในข้อกำหนด Tagged Template Literal ของ ES2015 แทน Tagged Template Literal คือสตริง Template Literal ที่มีฟังก์ชันแท็กแนบอยู่
ตัวอย่างของเทมเพลตลิเทอรัลมีดังนี้
const str = 'string';
console.log(`This is a template literal ${str}`);
ตัวอย่างแท็กเทมเพลตลิเทอรัลมีดังนี้
const tag = (strings, ...values) => ({strings, values});
const f = (x) => tag`hello ${x} how are you`;
console.log(f('world')); // {strings: ["hello ", " how are you"], values: ["world"]}
console.log(f('world').strings === f(1 + 2).strings); // true
ในตัวอย่างข้างต้น แท็กคือฟังก์ชัน tag และฟังก์ชัน f จะแสดงผลการเรียกใช้เทมเพลตสตริงที่ติดแท็ก
ประสิทธิภาพที่ยอดเยี่ยมของ Lit ส่วนใหญ่มาจากข้อเท็จจริงที่ว่าอาร์เรย์สตริงที่ส่งไปยังฟังก์ชันแท็กมีตัวชี้เดียวกัน (ดังที่แสดงใน console.log ที่ 2) เบราว์เซอร์จะไม่สร้างอาร์เรย์ strings ใหม่ทุกครั้งที่เรียกใช้ฟังก์ชันแท็ก เนื่องจากใช้เทมเพลตสตริงเดียวกัน (กล่าวคือ ในตำแหน่งเดียวกันใน AST) ดังนั้นการเชื่อมโยง การแยกวิเคราะห์ และการแคชเทมเพลตของ Lit จึงใช้ประโยชน์จากฟีเจอร์เหล่านี้ได้โดยไม่มีค่าใช้จ่ายในการเปรียบเทียบรันไทม์มากนัก
ลักษณะการทำงานของเบราว์เซอร์ในตัวของเทมเพลตสตริงที่ติดแท็กนี้ทำให้ Lit มีข้อได้เปรียบด้านประสิทธิภาพอย่างมาก DOM เสมือนแบบเดิมส่วนใหญ่จะทำงานใน JavaScript อย่างไรก็ตาม Tagged Template Literal จะทำการเปรียบเทียบส่วนใหญ่ใน C++ ของเบราว์เซอร์
หากต้องการเริ่มต้นใช้งานเทมเพลตสตริงที่ติดแท็ก HTML กับ React หรือ Preact ทีม Lit ขอแนะนำให้ใช้ไลบรารี htm
อย่างไรก็ตาม คุณจะเห็นว่าการไฮไลต์ไวยากรณ์ของเทมเพลตลิเทอรัลที่ติดแท็กนั้นไม่ค่อยพบเห็นได้บ่อยนัก เช่นเดียวกับเว็บไซต์ Google Codelabs และโปรแกรมแก้ไขโค้ดออนไลน์หลายโปรแกรม IDE และโปรแกรมแก้ไขข้อความบางรายการรองรับโดยค่าเริ่มต้น เช่น Atom และเครื่องมือไฮไลต์โค้ดบล็อกของ GitHub นอกจากนี้ ทีม Lit ยังทำงานร่วมกับชุมชนอย่างใกล้ชิดเพื่อดูแลโปรเจ็กต์ต่างๆ เช่น lit-plugin ซึ่งเป็นปลั๊กอิน VS Code ที่จะเพิ่มการไฮไลต์ไวยากรณ์ การตรวจสอบประเภท และการเติมข้อความอัตโนมัติลงในโปรเจ็กต์ Lit
Lit และ JSX + React DOM
JSX ไม่ได้ทำงานในเบราว์เซอร์ แต่จะใช้ตัวประมวลผลล่วงหน้าเพื่อแปลง JSX เป็นการเรียกฟังก์ชัน JavaScript (โดยปกติผ่าน Babel)
เช่น Babel จะแปลงโค้ดนี้
const element = <div className="title">Hello World!</div>;
ReactDOM.render(element, mountNode);
เป็น
const element = React.createElement('div', {className: 'title'}, 'Hello World!');
ReactDOM.render(element, mountNode);
จากนั้น React DOM จะนำเอาเอาต์พุตของ React ไปแปลเป็น DOM จริง ซึ่งรวมถึงพร็อพเพอร์ตี้ แอตทริบิวต์ ตัวแฮนเดิลเหตุการณ์ และอื่นๆ
Lit-html ใช้แท็กเทมเพลตสตริง ซึ่งสามารถเรียกใช้ในเบราว์เซอร์ได้โดยไม่ต้องมีการแปลงหรือตัวประมวลผลล่วงหน้า ซึ่งหมายความว่าหากต้องการเริ่มต้นใช้งาน Lit คุณจะต้องมีไฟล์ HTML, สคริปต์โมดูล ES และเซิร์ฟเวอร์ นี่คือสคริปต์ที่เรียกใช้ในเบราว์เซอร์ได้โดยสมบูรณ์
<!DOCTYPE html>
<html>
<head>
<script type="module">
import {html, render} from 'https://cdn.skypack.dev/lit';
render(
html`<div>Hello World!</div>`,
document.querySelector('.root')
)
</script>
</head>
<body>
<div class="root"></div>
</body>
</html>
นอกจากนี้ เนื่องจากระบบการสร้างเทมเพลตของ Lit ซึ่งก็คือ lit-html ไม่ได้ใช้ Virtual DOM แบบเดิม แต่ใช้ DOM API โดยตรง ขนาดของ Lit 2 จึงมีขนาดต่ำกว่า 5kb เมื่อย่อและบีบอัดด้วย gzip เมื่อเทียบกับ React (2.8kb) + react-dom (39.4kb) ซึ่งมีขนาด 40kb เมื่อย่อและบีบอัดด้วย gzip
กิจกรรม
React ใช้ระบบเหตุการณ์สังเคราะห์ ซึ่งหมายความว่า react-dom ต้องกําหนดทุกเหตุการณ์ที่จะใช้ในทุกคอมโพเนนต์ และจัดเตรียมเครื่องมือฟังเหตุการณ์ในรูปแบบ CamelCase ที่เทียบเท่าสําหรับโหนดแต่ละประเภท ด้วยเหตุนี้ JSX จึงไม่มีวิธีในการกําหนด Listener เหตุการณ์สําหรับเหตุการณ์ที่กําหนดเอง และนักพัฒนาซอฟต์แวร์ต้องใช้ ref จากนั้นจึงใช้ Listener โดยตรง ซึ่งจะสร้างประสบการณ์การใช้งานของนักพัฒนาแอปที่ไม่ดีเมื่อผสานรวมไลบรารีที่ไม่ได้ออกแบบมาสำหรับ React จึงต้องเขียน Wrapper เฉพาะของ React
Lit-html เข้าถึง DOM โดยตรงและใช้เหตุการณ์ดั้งเดิม ดังนั้นการเพิ่ม Listener เหตุการณ์จึงง่ายดายเพียงแค่ @event-name=${eventNameListener} ซึ่งหมายความว่าจะมีการแยกวิเคราะห์รันไทม์น้อยลงสำหรับการเพิ่มเครื่องมือฟังเหตุการณ์และการทริกเกอร์เหตุการณ์
คอมโพเนนต์และพร็อพ
คอมโพเนนต์ React และองค์ประกอบที่กำหนดเอง
LitElement ใช้ Custom Elements เพื่อจัดแพ็กเกจคอมโพเนนต์ของตัวเอง Custom Elements ทำให้เกิดการแลกเปลี่ยนบางอย่างระหว่างคอมโพเนนต์ React เมื่อพูดถึงการแยกคอมโพเนนต์ (มีการอธิบายสถานะและวงจรเพิ่มเติมในส่วนสถานะและวงจร)
ข้อดีบางประการขององค์ประกอบที่กำหนดเองในฐานะระบบคอมโพเนนต์มีดังนี้
- ทำงานในเบราว์เซอร์โดยไม่ต้องใช้เครื่องมือใดๆ
- ทำงานร่วมกับ API ของเบราว์เซอร์ทุกรายการตั้งแต่
innerHTMLและdocument.createElementไปจนถึงquerySelector - โดยปกติแล้วจะใช้ได้กับทุกเฟรมเวิร์ก
- ลงทะเบียนแบบเลซีโหลดด้วย
customElements.defineและ "ไฮเดรต" DOM ได้
ข้อเสียบางประการของ Custom Elements เมื่อเทียบกับคอมโพเนนต์ React
- สร้างองค์ประกอบที่กำหนดเองไม่ได้หากไม่ได้กำหนดคลาส (จึงไม่มีคอมโพเนนต์ฟังก์ชันที่คล้าย JSX)
- ต้องมีแท็กปิด
- หมายเหตุ: แม้ว่าผู้ให้บริการเบราว์เซอร์จะอำนวยความสะดวกให้แก่นักพัฒนาแอป แต่ก็มักจะเสียใจกับข้อกำหนดของแท็กที่ปิดตัวเอง ซึ่งเป็นเหตุผลที่ข้อกำหนดใหม่ๆ มักจะไม่มีแท็กที่ปิดตัวเอง
- เพิ่มโหนดพิเศษลงในแผนผัง DOM ซึ่งอาจทำให้เกิดปัญหาเกี่ยวกับเลย์เอาต์
- ต้องลงทะเบียนผ่าน JavaScript
Lit เลือกใช้ Custom Elements แทนระบบองค์ประกอบที่กำหนดเองเนื่องจาก Custom Elements มีอยู่ในเบราว์เซอร์ และทีม Lit เชื่อว่าประโยชน์ข้ามเฟรมเวิร์กมีมากกว่าประโยชน์ที่เลเยอร์การแยกส่วนประกอบมอบให้ ในความเป็นจริง ความพยายามของทีม Lit ในพื้นที่ lit-ssr ได้แก้ไขปัญหาหลักเกี่ยวกับการลงทะเบียน JavaScript แล้ว นอกจากนี้ บางบริษัท เช่น GitHub ยังใช้ประโยชน์จากการลงทะเบียนแบบ Lazy Loading ของ Custom Element เพื่อเพิ่มประสิทธิภาพหน้าเว็บอย่างต่อเนื่องด้วยลูกเล่นเสริม
การส่งข้อมูลไปยังองค์ประกอบที่กำหนดเอง
ความเข้าใจผิดที่พบบ่อยเกี่ยวกับองค์ประกอบที่กำหนดเองคือเชื่อว่าส่งข้อมูลได้เฉพาะในรูปแบบสตริงเท่านั้น ความเข้าใจผิดนี้อาจเกิดจากข้อเท็จจริงที่ว่าแอตทริบิวต์ขององค์ประกอบเขียนเป็นสตริงได้เท่านั้น แม้ว่า Lit จะแปลงแอตทริบิวต์สตริงเป็นประเภทที่กำหนดไว้ แต่ Custom Elements ก็ยอมรับข้อมูลที่ซับซ้อนเป็นพร็อพเพอร์ตี้ได้เช่นกัน
ตัวอย่างเช่น หากกำหนด LitElement ดังนี้
// data-test.ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@customElement('data-test')
class DataTest extends LitElement {
@property({type: Number})
num = 0;
@property({attribute: false})
data = {a: 0, b: null, c: [html`<div>hello</div>`, html`<div>world</div>`]}
render() {
return html`
<div>num + 1 = ${this.num + 1}</div>
<div>data.a = ${this.data.a}</div>
<div>data.b = ${this.data.b}</div>
<div>data.c = ${this.data.c}</div>`;
}
}
มีการกำหนดพร็อพเพอร์ตี้รีแอกทีฟดั้งเดิม num ซึ่งจะแปลงค่าสตริงของแอตทริบิวต์เป็น number จากนั้นจึงมีการนำโครงสร้างข้อมูลที่ซับซ้อนมาใช้กับ attribute:false ซึ่งจะปิดใช้งานการจัดการแอตทริบิวต์ของ Lit
วิธีส่งข้อมูลไปยังองค์ประกอบที่กำหนดเองนี้มีดังนี้
<head>
<script type="module">
import './data-test.js'; // loads element definition
import {html} from './data-test.js';
const el = document.querySelector('data-test');
el.data = {
a: 5,
b: null,
c: [html`<div>foo</div>`,html`<div>bar</div>`]
};
</script>
</head>
<body>
<data-test num="5"></data-test>
</body>
สถานะและวงจร
การเรียกกลับวงจรอื่นๆ ของ React
static getDerivedStateFromProps
ไม่มีเทียบเท่าใน Lit เนื่องจากทั้งพร็อพและสถานะเป็นพร็อพเพอร์ตี้ของคลาสเดียวกัน
shouldComponentUpdate
- เทียบเท่ากับ
shouldUpdate - เรียกใช้เมื่อแสดงผลครั้งแรก ซึ่งต่างจาก React
- มีฟังก์ชันคล้ายกับ
shouldComponentUpdateของ React
getSnapshotBeforeUpdate
ใน Lit getSnapshotBeforeUpdate จะคล้ายกับทั้ง update และ willUpdate
willUpdate
- โทรเมื่อก่อนวันที่
update willUpdateจะเรียกใช้ก่อนrenderซึ่งต่างจากgetSnapshotBeforeUpdate- การเปลี่ยนแปลงคุณสมบัติแบบรีแอกทีฟใน
willUpdateจะไม่ทริกเกอร์รอบการอัปเดตอีกครั้ง - เป็นตำแหน่งที่ดีในการคำนวณค่าพร็อพเพอร์ตี้ที่ขึ้นอยู่กับพร็อพเพอร์ตี้อื่นๆ และใช้ในกระบวนการอัปเดตที่เหลือ
- ระบบจะเรียกใช้เมธอดนี้ในเซิร์ฟเวอร์ใน SSR ดังนั้นจึงไม่แนะนำให้เข้าถึง DOM ที่นี่
update
- โทรหลังวันที่
willUpdate updateจะเรียกใช้ก่อนrenderซึ่งต่างจากgetSnapshotBeforeUpdate- การเปลี่ยนแปลงคุณสมบัติแบบรีแอกทีฟใน
updateจะไม่ทริกเกอร์รอบการอัปเดตอีกครั้งหากมีการเปลี่ยนแปลงก่อนเรียกใช้super.update - เป็นจุดที่ดีในการบันทึกข้อมูลจาก DOM ที่อยู่รอบๆ คอมโพเนนต์ก่อนที่จะส่งเอาต์พุตที่แสดงผลไปยัง DOM
- ระบบจะไม่เรียกใช้เมธอดนี้ในเซิร์ฟเวอร์ใน SSR
การเรียกกลับอื่นๆ ของวงจร Lit
มี Callback ของวงจรหลายรายการที่ไม่ได้กล่าวถึงในส่วนก่อนหน้าเนื่องจากไม่มี Callback ที่คล้ายกันใน React ดังนี้
attributeChangedCallback
โดยจะเรียกใช้เมื่อobservedAttributesขององค์ประกอบใดองค์ประกอบหนึ่งมีการเปลี่ยนแปลง ทั้ง observedAttributes และ attributeChangedCallback เป็นส่วนหนึ่งของข้อกำหนดองค์ประกอบที่กำหนดเองและ Lit ได้นำไปใช้เบื้องหลังเพื่อจัดหา API แอตทริบิวต์สำหรับองค์ประกอบ Lit
adoptedCallback
เรียกใช้เมื่อย้ายคอมโพเนนต์ไปยังเอกสารใหม่ เช่น จากHTMLTemplateElementของdocumentFragmentไปยังdocumentหลัก การเรียกกลับนี้ยังเป็นส่วนหนึ่งของข้อกำหนดขององค์ประกอบที่กำหนดเอง และควรใช้เฉพาะในกรณีการใช้งานขั้นสูงเมื่อคอมโพเนนต์เปลี่ยนแปลงเอกสาร
เมธอดและพร็อพเพอร์ตี้อื่นๆ ของวงจร
เมธอดและพร็อพเพอร์ตี้เหล่านี้เป็นสมาชิกของคลาสที่คุณเรียก ใช้แทน หรือรอเพื่อช่วยจัดการกระบวนการวงจรของออบเจ็กต์
updateComplete
นี่คือ Promise ที่จะได้รับการแก้ไขเมื่อองค์ประกอบอัปเดตเสร็จแล้ว เนื่องจากวงจรการอัปเดตและการแสดงผลเป็นแบบอะซิงโครนัส ตัวอย่าง
async nextButtonClicked() {
this.step++;
// Wait for the next "step" state to render
await this.updateComplete;
this.dispatchEvent(new Event('step-rendered'));
}
getUpdateComplete
นี่คือเมธอดที่ควรลบล้างเพื่อปรับแต่งเวลาที่ updateComplete แก้ไข ซึ่งมักเกิดขึ้นเมื่อคอมโพเนนต์แสดงผลคอมโพเนนต์ย่อยและรอบการแสดงผลของคอมโพเนนต์ทั้ง 2 ต้องซิงค์กัน เช่น
class MyElement extends LitElement {
...
async getUpdateComplete() {
await super.getUpdateComplete();
await this.myChild.updateComplete;
}
}
performUpdate
เมธอดนี้คือสิ่งที่เรียกใช้การเรียกกลับของวงจรการอัปเดต โดยทั่วไปแล้ว คุณไม่จำเป็นต้องดำเนินการนี้ ยกเว้นในกรณีที่หายากซึ่งต้องอัปเดตแบบพร้อมกันหรือสำหรับการกำหนดเวลาที่กำหนดเอง
hasUpdated
พร็อพเพอร์ตี้นี้จะเป็น true หากคอมโพเนนต์ได้รับการอัปเดตอย่างน้อย 1 ครั้ง
isConnected
พร็อพเพอร์ตี้นี้เป็นส่วนหนึ่งของข้อกำหนดขององค์ประกอบที่กำหนดเอง และจะมีค่าเป็น true หากองค์ประกอบเชื่อมต่อกับโครงสร้างเอกสารหลักในปัจจุบัน
การแสดงภาพวงจรการอัปเดต Lit
วงจรการอัปเดตมี 3 ส่วน ได้แก่
- ก่อนการอัปเดต
- อัปเดต
- หลังการอัปเดต
ก่อนการอัปเดต

หลังจากวันที่ requestUpdate คุณจะได้รับการอัปเดตตามกำหนดเวลา
อัปเดต

หลังการอัปเดต

ฮุก
เหตุผลที่ควรใช้ฮุก
มีการนำ Hook มาใช้ใน React สำหรับกรณีการใช้งานคอมโพเนนต์ฟังก์ชันอย่างง่ายที่ต้องใช้สถานะ ในหลายๆ กรณีที่เรียบง่าย ส่วนประกอบฟังก์ชันที่มี Hook มักจะเรียบง่ายและอ่านง่ายกว่าส่วนประกอบคลาสที่เทียบเท่ากันมาก อย่างไรก็ตาม เมื่อมีการอัปเดตสถานะแบบไม่พร้อมกัน รวมถึงการส่งข้อมูลระหว่างฮุกหรือเอฟเฟกต์ รูปแบบฮุกมักจะไม่เพียงพอ และโซลูชันแบบคลาส เช่น คอนโทรลเลอร์แบบรีแอกทีฟ มักจะทำงานได้ดี
Hook และตัวควบคุมคำขอ API
การเขียน Hook ที่ขอข้อมูลจาก API เป็นเรื่องปกติ เช่น ลองดูคอมโพเนนต์ฟังก์ชัน React นี้ที่ทำสิ่งต่อไปนี้
index.tsx- แสดงข้อความ
- แสดงคำตอบของ
useAPI- รหัสผู้ใช้ + ชื่อผู้ใช้
- ข้อความแสดงข้อผิดพลาด
- 404 เมื่อถึงผู้ใช้ 11 (ตามการออกแบบ)
- ยกเลิกข้อผิดพลาดหากยกเลิกการดึงข้อมูล API
- ข้อความการโหลด
- แสดงปุ่มดำเนินการ
- ผู้ใช้ถัดไป: ซึ่งดึงข้อมูล API สำหรับผู้ใช้ถัดไป
- ยกเลิก: ซึ่งจะยกเลิกการดึงข้อมูล API และแสดงข้อผิดพลาด
useApi.tsx- กำหนด
useApiฮุกที่กำหนดเอง - จะดึงข้อมูลออบเจ็กต์ผู้ใช้จาก API แบบไม่พร้อมกัน
- ปล่อย:
- ชื่อผู้ใช้
- กำลังโหลดการดึงข้อมูลหรือไม่
- ข้อความแสดงข้อผิดพลาด
- ฟังก์ชันเรียกกลับเพื่อยกเลิกการดึงข้อมูล
- ยกเลิกการดึงข้อมูลที่กำลังดำเนินการอยู่หากมีการยกเลิกการเชื่อมต่อ
- กำหนด
ดูการใช้งาน Lit + Reactive Controller ได้ที่นี่
สรุปประเด็นสำคัญ:
- Reactive Controllers มีลักษณะคล้ายกับ Custom Hooks มากที่สุด
- การส่งข้อมูลที่แสดงผลไม่ได้ระหว่างฟังก์ชันเรียกกลับและเอฟเฟกต์
- React ใช้
useRefเพื่อส่งข้อมูลระหว่างuseEffectกับuseCallback - Lit ใช้พร็อพเพอร์ตี้คลาสส่วนตัว
- โดยพื้นฐานแล้ว React จะเลียนแบบลักษณะการทำงานของพร็อพเพอร์ตี้คลาสส่วนตัว
- React ใช้
นอกจากนี้ หากคุณชอบไวยากรณ์ของคอมโพเนนต์ฟังก์ชัน React ที่มี Hook แต่ต้องการใช้สภาพแวดล้อมแบบไม่ต้องบิลด์เดียวกันกับ Lit ทีม Lit ขอแนะนำให้ใช้ไลบรารี Haunted
เด็ก
ตําแหน่งเริ่มต้น
เมื่อไม่ได้กำหนดแอตทริบิวต์ slot ให้กับองค์ประกอบ HTML ระบบจะกำหนดองค์ประกอบเหล่านั้นให้กับช่องเริ่มต้นที่ไม่มีชื่อ ในตัวอย่างด้านล่าง MyApp จะใส่ย่อหน้าหนึ่งลงในช่องที่มีชื่อ ส่วนย่อหน้าอื่นๆ จะเป็นช่องที่ไม่มีชื่อโดยค่าเริ่มต้น
@customElement("my-element")
export class MyElement extends LitElement {
render() {
return html`
<section>
<div>
<slot></slot>
</div>
<div>
<slot name="custom-slot"></slot>
</div>
</section>
`;
}
}
@customElement("my-app")
export class MyApp extends LitElement {
render() {
return html`
<my-element>
<p slot="custom-slot">
This paragraph will be placed in the custom-slot!
</p>
<p>
This paragraph will be placed in the unnamed default slot!
</p>
</my-element>
`;
}
}
ข้อมูลอัปเดตเกี่ยวกับสล็อต
เมื่อโครงสร้างขององค์ประกอบย่อยของช่องมีการเปลี่ยนแปลง ระบบจะทริกเกอร์slotchange คอมโพเนนต์ Lit สามารถเชื่อมโยงเครื่องมือฟังเหตุการณ์กับslotchange event ได้ ในตัวอย่างด้านล่าง ช่องแรกที่พบใน shadowRoot จะมี assignedNodes ที่บันทึกลงในคอนโซลใน slotchange
@customElement("my-element")
export class MyElement extends LitElement {
onSlotChange(e: Event) {
const slot = this.shadowRoot.querySelector('slot');
console.log(slot.assignedNodes({flatten: true}));
}
render() {
return html`
<section>
<div>
<slot @slotchange="{this.onSlotChange}"></slot>
</div>
</section>
`;
}
}
Refs
การสร้างการอ้างอิง
ทั้ง Lit และ React ต่างก็แสดงการอ้างอิงไปยัง HTMLElement หลังจากเรียกใช้ฟังก์ชัน render แต่ก็ควรตรวจสอบว่า React และ Lit สร้าง DOM ที่จะแสดงผลในภายหลังผ่านตัวตกแต่ง @query ของ Lit หรือการอ้างอิงของ React อย่างไร
React เป็นไปป์ไลน์การทำงานที่สร้างคอมโพเนนต์ React ไม่ใช่ HTMLElements เนื่องจากมีการประกาศ Ref ก่อนที่จะแสดงผล HTMLElement จึงมีการจัดสรรพื้นที่ในหน่วยความจำ ด้วยเหตุนี้ คุณจึงเห็น null เป็นค่าเริ่มต้นของ Ref เนื่องจากยังไม่ได้สร้าง (หรือแสดง) องค์ประกอบ DOM จริง นั่นคือ useRef(null)
หลังจากที่ ReactDOM แปลง React Component เป็น HTMLElement แล้ว ก็จะมองหาแอตทริบิวต์ที่ชื่อ ref ใน ReactComponent หากมี ReactDOM จะวางการอ้างอิง HTMLElement ไว้ใน ref.current
LitElement ใช้htmlฟังก์ชันแท็กเทมเพลตจาก lit-html เพื่อสร้างองค์ประกอบเทมเพลตในส่วนประกอบพื้นฐานในการทำงาน LitElement จะประทับเนื้อหาของเทมเพลตลงใน Shadow DOM ขององค์ประกอบที่กำหนดเองหลังการแสดงผล Shadow DOM คือแผนผัง DOM ที่กำหนดขอบเขตซึ่งห่อหุ้มโดย Shadow Root จากนั้น @query Decorator จะสร้าง Getter สำหรับพร็อพเพอร์ตี้ ซึ่งจะทําหน้าที่ this.shadowRoot.querySelector ในรูทที่กำหนดขอบเขต
ค้นหาหลายองค์ประกอบ
ในตัวอย่างด้านล่าง @queryAll Decorator จะแสดงย่อหน้า 2 ย่อหน้าใน Shadow Root เป็น NodeList
@customElement("my-element")
export class MyElement extends LitElement {
@queryAll('p')
paragraphs!: NodeList;
render() {
return html`
<p>Hello, world!</p>
<p>How are you?</p>
`;
}
}
โดยพื้นฐานแล้ว @queryAll จะสร้างตัวรับสำหรับ paragraphs ที่แสดงผลลัพธ์ของ this.shadowRoot.querySelectorAll() ใน JavaScript คุณสามารถประกาศ Getter เพื่อให้มีวัตถุประสงค์เดียวกันได้
get paragraphs() {
return this.renderRoot.querySelectorAll('p');
}
องค์ประกอบการเปลี่ยนแปลงการค้นหา
เครื่องมือตกแต่ง @queryAsync เหมาะกว่าในการจัดการโหนดที่เปลี่ยนแปลงได้ตามสถานะของพร็อพเพอร์ตี้องค์ประกอบอื่น
ในตัวอย่างด้านล่าง @queryAsync จะค้นหาองค์ประกอบย่อหน้าแรก อย่างไรก็ตาม ระบบจะแสดงผลองค์ประกอบย่อหน้าเมื่อ renderParagraph สร้างเลขคี่แบบสุ่มเท่านั้น คำสั่ง @queryAsync จะแสดงผล Promise ที่จะได้รับการแก้ไขเมื่อย่อหน้าแรกพร้อมใช้งาน
@customElement("my-dissappearing-paragraph")
export class MyDisapppearingParagraph extends LitElement {
@queryAsync('p')
paragraph!: Promise<HTMLElement>;
renderParagraph() {
const randomNumber = Math.floor(Math.random() * 10)
if (randomNumber % 2 === 0) {
return "";
}
return html`<p>This checkbox is checked!`
}
render() {
return html`
${this.renderParagraph()}
`;
}
}
สถานะการไกล่เกลี่ย
ใน React ธรรมเนียมคือการใช้ฟังก์ชันเรียกกลับเนื่องจาก React เป็นตัวกลางจัดการสถานะเอง React พยายามอย่างเต็มที่ที่จะไม่พึ่งพาสถานะที่องค์ประกอบระบุ DOM เป็นเพียงผลลัพธ์ของกระบวนการแสดงผล
สถานะภายนอก
คุณใช้ Redux, MobX หรือไลบรารีการจัดการสถานะอื่นๆ ควบคู่ไปกับ Lit ได้
คอมโพเนนต์ Lit จะสร้างขึ้นในขอบเขตของเบราว์เซอร์ ดังนั้นไลบรารีใดๆ ที่มีอยู่ในขอบเขตของเบราว์เซอร์จะพร้อมใช้งานกับ Lit มีการสร้างไลบรารีที่ยอดเยี่ยมมากมายเพื่อใช้ประโยชน์จากระบบการจัดการสถานะที่มีอยู่ใน Lit
ต่อไปนี้คือซีรีส์จาก Vaadin ที่อธิบายวิธีใช้ประโยชน์จาก Redux ในคอมโพเนนต์ Lit
ดู lit-mobx จาก Adobe เพื่อดูว่าเว็บไซต์ขนาดใหญ่จะใช้ประโยชน์จาก MobX ใน Lit ได้อย่างไร
นอกจากนี้ โปรดดู Apollo Elements เพื่อดูว่านักพัฒนาแอปใช้ GraphQL ในคอมโพเนนต์ของเว็บอย่างไร
Lit ทำงานร่วมกับฟีเจอร์เบราว์เซอร์ดั้งเดิม และโซลูชันการจัดการสถานะส่วนใหญ่ในขอบเขตเบราว์เซอร์สามารถใช้ในคอมโพเนนต์ Lit ได้
การจัดรูปแบบ
Shadow DOM
Lit ใช้ Shadow DOM เพื่อห่อหุ้มสไตล์และ DOM ภายใน Custom Element โดยเนทีฟ Shadow Root จะสร้าง Shadow Tree แยกจากต้นไม้เอกสารหลัก ซึ่งหมายความว่าสไตล์ส่วนใหญ่จะกำหนดขอบเขตไว้ในเอกสารนี้ แต่สไตล์บางอย่างจะยังคงแสดงอยู่ เช่น สี และสไตล์อื่นๆ ที่เกี่ยวข้องกับแบบอักษร
นอกจากนี้ Shadow DOM ยังนำเสนอแนวคิดและตัวเลือกใหม่ๆ ในข้อกำหนด CSS ด้วย ดังนี้
:host, :host(:hover), :host([hover]) {
/* Styles the element in which the shadow root is attached to */
}
slot[name="title"]::slotted(*), slot::slotted(:hover), slot::slotted([hover]) {
/*
* Styles the elements projected into a slot element. NOTE: the spec only allows
* styling the direcly slotted elements. Children of those elements are not stylable.
*/
}
รูปแบบการแชร์
Lit ช่วยให้แชร์สไตล์ระหว่างคอมโพเนนต์ในรูปแบบของ CSSTemplateResults ผ่านแท็กเทมเพลต css ได้ง่ายๆ เช่น
// typography.ts
export const body1 = css`
.body1 {
...
}
`;
// my-el.ts
import {body1} from './typography.ts';
@customElement('my-el')
class MyEl Extends {
static get styles = [
body1,
css`/* local styles come after so they will override bod1 */`
]
render() {
return html`<div class="body1">...</div>`
}
}
ธีม
Shadow Root เป็นอุปสรรคเล็กๆ ต่อการกำหนดธีมแบบเดิมๆ ซึ่งมักเป็นการใช้แท็กสไตล์จากบนลงล่าง วิธีทั่วไปในการจัดการการกำหนดธีมด้วย Web Components ที่ใช้ Shadow DOM คือการเปิดเผย API สไตล์ผ่านพร็อพเพอร์ตี้ที่กำหนดเองของ CSS ตัวอย่างเช่น รูปแบบที่ Material Design ใช้มีดังนี้
.mdc-textfield-outline {
border-color: var(--mdc-theme-primary, /* default value */ #...);
}
.mdc-textfield--input {
caret-color: var(--mdc-theme-primary, #...);
}
จากนั้นผู้ใช้จะเปลี่ยนธีมของเว็บไซต์โดยใช้ค่าพร็อพเพอร์ตี้ที่กำหนดเองได้ดังนี้
html {
--mdc-theme-primary: #F00;
}
html[dark] {
--mdc-theme-primary: #F88;
}
หากต้องใช้การจัดธีมจากบนลงล่างและคุณไม่สามารถแสดงรูปแบบได้ คุณปิดใช้ Shadow DOM ได้เสมอโดยการลบล้าง createRenderRoot เพื่อให้แสดงผล this ซึ่งจะแสดงผลเทมเพลตของคอมโพเนนต์ไปยังองค์ประกอบที่กำหนดเองเอง แทนที่จะแสดงผลไปยังรูทของ Shadow ที่แนบมากับองค์ประกอบที่กำหนดเอง การดำเนินการนี้จะทำให้คุณเสียการห่อหุ้มสไตล์ การห่อหุ้ม DOM และช่อง
เวอร์ชันที่ใช้งานจริง
IE 11
หากต้องการรองรับเบราว์เซอร์รุ่นเก่า เช่น IE 11 คุณจะต้องโหลด Polyfill บางรายการซึ่งมีขนาดประมาณ 33 KB ดูข้อมูลเพิ่มเติมได้ที่นี่
แพ็กเกจแบบมีเงื่อนไข
ทีม Lit ขอแนะนำให้แสดงผล 2 บันเดิลที่แตกต่างกัน โดยบันเดิลหนึ่งสำหรับ IE 11 และอีกบันเดิลหนึ่งสำหรับเบราว์เซอร์สมัยใหม่ ซึ่งมีประโยชน์หลายประการ ดังนี้
- การแสดง ES 6 จะเร็วกว่าและจะแสดงต่อไคลเอ็นต์ส่วนใหญ่
- การแปลง ES 5 จะเพิ่มขนาด Bundle อย่างมาก
- แพ็กเกจแบบมีเงื่อนไขจะช่วยให้คุณได้รับประโยชน์ทั้ง 2 อย่าง
- การรองรับ IE 11
- ไม่มีการชะลอความเร็วในเบราว์เซอร์ที่ทันสมัย
ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีสร้าง Bundle ที่แสดงตามเงื่อนไขได้ในเว็บไซต์เอกสารประกอบที่นี่