1. บทนำ
Lit คืออะไร
Lit เป็นไลบรารีที่เรียบง่ายสำหรับการสร้างคอมโพเนนต์เว็บที่เบาและรวดเร็วซึ่งทำงานได้กับเฟรมเวิร์กใดก็ได้ หรือจะทำงานโดยไม่ใช้เฟรมเวิร์กเลยก็ได้ Lit ช่วยให้คุณสามารถสร้างคอมโพเนนต์ แอปพลิเคชัน ระบบการออกแบบ และอื่นๆ ที่แชร์ได้
สิ่งที่คุณจะได้เรียนรู้
วิธีแปลแนวคิดต่างๆ ของ React เป็น Lit เช่น
- JSX และ Templating
- คอมโพเนนต์และพร็อพ
- สถานะและวงจร
- ฮุก
- เด็ก
- การอ้างอิง
- รัฐสื่อกลาง
สิ่งที่คุณจะสร้าง
เมื่อจบ Codelab นี้ คุณจะแปลงแนวคิดคอมโพเนนต์ React เป็นคอมโพเนนต์ Lit ได้
สิ่งที่คุณต้องมี
- Chrome, Safari, Firefox หรือ Edge เวอร์ชันล่าสุด
- ความรู้เกี่ยวกับ HTML, CSS, JavaScript และ เครื่องมือสำหรับนักพัฒนาเว็บใน Chrome
- ความรู้เรื่องปฏิกิริยา
- (ขั้นสูง) หากต้องการประสบการณ์การพัฒนาที่ดีที่สุด ให้ดาวน์โหลด 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 ทำงานได้เร็วกว่า 8-10% กว่า React ในกรณีที่แย่ที่สุด และเร็วกว่า 50% ขึ้นไปใน Use Case ที่พบบ่อย
LitElement (คลาสฐานคอมโพเนนต์ของ Lit) จะเพิ่มค่าใช้จ่ายเพิ่มเติมเพียงเล็กน้อยใน lit-html แต่มีประสิทธิภาพมากกว่า React 16-30% เมื่อเปรียบเทียบฟีเจอร์คอมโพเนนต์ เช่น การใช้หน่วยความจำ การโต้ตอบ และเวลาเริ่มต้น
ไม่ต้องมีบิลด์
เมื่อใช้ฟีเจอร์ใหม่ของเบราว์เซอร์ เช่น โมดูล ES และนิพจน์เทมเพลตที่ติดแท็ก Lit ไม่จําเป็นต้องคอมไพล์เพื่อเรียกใช้ ซึ่งหมายความว่าคุณตั้งค่าสภาพแวดล้อมของนักพัฒนาซอฟต์แวร์ได้ด้วยแท็กสคริปต์ + เบราว์เซอร์ + เซิร์ฟเวอร์ แล้วใช้งานได้เลย
เมื่อใช้โมดูล ES และ CDN สมัยใหม่ เช่น Skypack หรือ UNPKG คุณอาจไม่จําเป็นต้องใช้ NPM ในการเริ่มต้นใช้งานเลย
อย่างไรก็ตาม คุณยังคงสร้างและเพิ่มประสิทธิภาพโค้ด Lit ได้หากต้องการ การรวมนักพัฒนาซอฟต์แวร์ล่าสุดเกี่ยวกับโมดูล ES เนทีฟเป็นผลดีสำหรับ Lit Lit เป็นเพียง JavaScript ปกติและไม่จำเป็นต้องมี CLI เฉพาะเฟรมเวิร์กหรือการจัดการบิลด์
ไม่ยึดติดกับเฟรมเวิร์ก
คอมโพเนนต์ของ Lit สร้างขึ้นจากชุดมาตรฐานเว็บที่เรียกว่าคอมโพเนนต์เว็บ ซึ่งหมายความว่าการสร้างคอมโพเนนต์ใน Lit จะใช้งานได้ในเฟรมเวิร์กในปัจจุบันและอนาคต หากรองรับองค์ประกอบ HTML ก็จะรองรับคอมโพเนนต์เว็บด้วย
ปัญหาเดียวเกี่ยวกับการทำงานร่วมกันของเฟรมเวิร์กคือเมื่อเฟรมเวิร์กมีการรองรับ DOM แบบจำกัด React เป็นหนึ่งในเฟรมเวิร์กเหล่านี้ แต่อนุญาตให้ใช้ทางออกผ่าน Refs และ Refs ใน React ไม่ใช่ประสบการณ์การใช้งานที่ดีสำหรับนักพัฒนาซอฟต์แวร์
ทีม Lit กำลังทําโปรเจ็กต์เวอร์ชันทดลองชื่อ @lit-labs/react
ซึ่งจะแยกวิเคราะห์คอมโพเนนต์ Lit โดยอัตโนมัติและสร้าง React Wrapper เพื่อให้คุณไม่ต้องใช้ refs
นอกจากนี้ องค์ประกอบที่กำหนดเองทุกที่จะแสดงให้คุณเห็นว่าเฟรมเวิร์กและไลบรารีใดทำงานได้ดีกับองค์ประกอบที่กำหนดเอง
การสนับสนุน TypeScript เฟิร์สคลาส
แม้ว่าจะเขียนโค้ด Lit ทั้งหมดได้ใน JavaScript แต่ Lit เขียนด้วย TypeScript และทีม Lit แนะนำให้นักพัฒนาซอฟต์แวร์ใช้ TypeScript ด้วย
ทีม Lit ทำงานร่วมกับชุมชน Lit เพื่อช่วยดูแลรักษาโปรเจ็กต์ที่นำการตรวจสอบประเภทและ IntelliSense ของ 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
การเข้าถึงรหัส
ทั่วทั้ง Codelab จะมีลิงก์ไปยัง Lit Play แบบนี้
พื้นที่ทํางานเป็นแซนด์บ็อกซ์โค้ดที่ทํางานในเบราว์เซอร์ของคุณโดยสมบูรณ์ เครื่องมือนี้สามารถคอมไพล์และเรียกใช้ไฟล์ 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 สนามเด็กเล่นที่มีไฟสว่าง
ภาพหน้าจอ UI ของ Lit Playground จะไฮไลต์ส่วนที่คุณจะใช้ในโค้ดแล็บนี้
- ตัวเลือกไฟล์ สังเกตปุ่มบวก...
- เครื่องมือแก้ไขไฟล์
- แสดงตัวอย่างโค้ด
- ปุ่มโหลดซ้ำ
- ปุ่มดาวน์โหลด
การตั้งค่า VS Code (ขั้นสูง)
ข้อดีในการใช้การตั้งค่า VS Code นี้มีดังนี้
- การตรวจสอบประเภทเทมเพลต
- ฟีเจอร์อัจฉริยะของเทมเพลตและการเติมข้อความอัตโนมัติ
หากติดตั้ง 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 และ Templating
ในส่วนนี้ คุณจะได้เรียนรู้พื้นฐานของการสร้างเทมเพลตใน Lit
เทมเพลต JSX และ Lit
JSX คือส่วนขยายไวยากรณ์ของ JavaScript ที่ช่วยให้ผู้ใช้ React เขียนเทมเพลตในโค้ด JavaScript ได้อย่างง่ายดาย เทมเพลต Lit มีวัตถุประสงค์คล้ายกัน ซึ่งก็คือการแสดง UI ของคอมโพเนนต์เป็นฟังก์ชันของสถานะ
ไวยากรณ์พื้นฐาน
ใน React คุณจะแสดงผล Hello World ของ JSX ดังนี้
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 ไม่จำเป็นต้องใช้เศษส่วนความรู้สึกเพื่อจัดกลุ่มองค์ประกอบหลายรายการในเทมเพลต
ใน Lit เทมเพลตจะห่อด้วยเทมเพลตที่ติดแท็ก html
ชื่อ LIT ซึ่งจะเป็นชื่อที่ Lit ได้รับ
ค่าเทมเพลต
เทมเพลต Lit ยอมรับเทมเพลต Lit อื่นๆ ซึ่งเรียกว่า TemplateResult
ได้ เช่น ตัด name
ไว้ในแท็กตัวเอียง (<i>
) แล้วตัดด้วยเทมเพลตลิเทอรัลที่ติดแท็ก หมายเหตุ โปรดใช้อักขระแบ็กทิก (`
) ไม่ใช่อักขระเครื่องหมายคำพูดเดี่ยว ('
)
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 TemplateResult
ยอมรับอาร์เรย์, สตริง, TemplateResult
อื่นๆ และคำสั่งได้
ลองแปลงโค้ด 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
ไวยากรณ์การเชื่อมโยง Lit Pro
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
);
ในตัวอย่างนี้ มีการเพิ่ม Listener ลงในเหตุการณ์ click
ด้วย @click
ถัดไป แทนที่จะใช้ onChange
จะมีการเชื่อมโยงกับเหตุการณ์ input
แบบเนทีฟของ <input>
เนื่องจากเหตุการณ์ change
แบบเนทีฟเริ่มทำงานเฉพาะใน blur
(แสดงบทคัดย่อจากเหตุการณ์เหล่านี้)
ไวยากรณ์เครื่องจัดการเหตุการณ์ Lit
html`<my-element @event-name=${() => {...}}></my-element>`;
- คำนำหน้า
@
คือไวยากรณ์การเชื่อมโยงสำหรับ Listener เหตุการณ์ - เทียบเท่ากับ
inputRef.addEventListener('event-name', ...)
- ใช้ชื่อเหตุการณ์ DOM ดั้งเดิม
5. คอมโพเนนต์และพร็อพ
ในส่วนนี้ คุณจะได้เรียนรู้เกี่ยวกับคอมโพเนนต์และฟังก์ชันของคลาส Lit เราจะกล่าวถึงสถานะและ 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}
}
}
- แท็กนี้ทำหน้าที่เหมือนกับตัวตกแต่ง TS ของ
@property
แต่จะทำงานแบบเนทีฟใน JavaScript
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
- โดยจะเรียกเมื่อมีการเปลี่ยนแปลงคุณสมบัติเชิงรับ
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
...
}
- การดำเนินการนี้จะเชื่อมโยงชื่อแท็กองค์ประกอบ HTML กับการกำหนดคลาส
- ตามมาตรฐานองค์ประกอบที่กำหนดเอง ชื่อแท็กต้องมีขีดกลางสั้น (-)
this
ใน LitElement หมายถึงอินสแตนซ์ขององค์ประกอบที่กำหนดเอง (ในกรณีนี้คือ<welcome-banner>
)
customElements.define('welcome-banner', WelcomeBanner);
- นี่คือ JavaScript ที่เทียบเท่ากับการตกแต่งด้าน TS ของ
@customElement
<head>
<script type="module" src="./index.js"></script>
</head>
- นำเข้าคำจำกัดความองค์ประกอบที่กำหนดเอง
<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 เกี่ยวกับ "คุณสมบัติเชิงรับ" เป็นการผสมผสานสถานะของปฏิกิริยาและอุปกรณ์ประกอบฉาก พร็อพเพอร์ตี้แบบรีแอ็กทีฟจะทริกเกอร์วงจรชีวิตของคอมโพเนนต์ได้เมื่อมีการเปลี่ยนแปลง พร็อพเพอร์ตี้เชิงรับมีด้วยกัน 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';
}
}
- ค่าเทียบเท่าของ Lit คือ
constructor
ด้วย - ไม่จำเป็นต้องส่งต่อสิ่งใดไปยัง Super Call
- เรียกใช้โดย (ไม่ครอบคลุมทั้งหมด):
document.createElement
document.innerHTML
new ComponentClass()
- หากมีชื่อแท็กที่ยังไม่ได้อัปเกรดอยู่ในหน้าเว็บ และโหลดคําจํากัดความและลงทะเบียนด้วย
@customElement
หรือcustomElements.define
- ฟังก์ชันการทำงานคล้ายกับ
constructor
ของ React
render
// React
render() {
return <div>Hello World</div>
}
// Lit
render() {
return html`<div>Hello World</div>`;
}
- ค่าเทียบเท่าของ Lit คือ
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 แสดงผลโดยคอมโพเนนต์
- ซึ่งแตกต่างจาก
componentDidMount
ของ React เนื่องจากการเปลี่ยนแปลงพร็อพเพอร์ตี้แบบรีแอ็กทีฟในfirstUpdated
จะทำให้เกิดการแสดงผลอีกครั้ง แม้ว่าโดยทั่วไปเบราว์เซอร์จะรวมการเปลี่ยนแปลงไว้ในเฟรมเดียวกัน หากการเปลี่ยนแปลงเหล่านั้นไม่จำเป็นต้องเข้าถึง DOM ของรูท การเปลี่ยนแปลงก็ควรอยู่ในwillUpdate
connectedCallback
// React
componentDidMount() {
this.window.addEventListener('resize', this.boundOnResize);
}
// Lit
connectedCallback() {
super.connectedCallback();
this.window.addEventListener('resize', this.boundOnResize);
}
- เรียกใช้เมื่อใดก็ตามที่องค์ประกอบที่กำหนดเองถูกแทรกลงในแผนผัง DOM
- ซึ่งแตกต่างจากคอมโพเนนต์ React เมื่อนำองค์ประกอบที่กําหนดเองออกจาก DOM องค์ประกอบดังกล่าวจะไม่ถูกทำลาย จึงสามารถ "เชื่อมต่อ" ได้หลายครั้ง
- ระบบจะไม่เรียกใช้
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
(ใช้คำภาษาอังกฤษ "update" ที่เป็นอดีตกาล) - ซึ่งแตกต่างจาก React ที่เรียก
updated
ในการเรนเดอร์ครั้งแรกด้วย - ฟังก์ชันการทำงานคล้ายกับ
componentDidUpdate
ของ React
componentWillUnmount
// React
componentWillUnmount() {
this.window.removeEventListener('resize', this.boundOnResize);
}
// Lit
disconnectedCallback() {
super.disconnectedCallback();
this.window.removeEventListener('resize', this.boundOnResize);
}
- Lit สิ่งที่เท่ากันคล้ายกับ
disconnectedCallback
- ซึ่งแตกต่างจากคอมโพเนนต์ React ที่ระบบจะไม่ทำลายคอมโพเนนต์เมื่อนำองค์ประกอบที่กําหนดเองออกจาก DOM
- สิ่งที่ต่างจาก
componentWillUnmount
คือจะเรียกdisconnectedCallback
ว่าหลังการนำองค์ประกอบออกจากแผนผังต้นไม้ - 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 คือการผสมผสานระหว่าง 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
);
}
สุดท้าย ให้ล้างช่วงเวลาเพื่อไม่ให้เรียกใช้เครื่องหมายถูกหลังจากยกเลิกการเชื่อมต่อองค์ประกอบจากแผนผังเอกสาร
// 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
React Hook เป็นวิธีที่คอมโพเนนต์ฟังก์ชันจะ "ฮุก" เข้ากับสถานะ ซึ่งมีประโยชน์หลายประการ
- ช่วยให้ทำให้การนำตรรกะใหม่กลับมาใช้ใหม่ได้ง่ายขึ้น
- ช่วยแยกคอมโพเนนต์ออกเป็นฟังก์ชันย่อยๆ
นอกจากนี้ การมุ่งเน้นที่คอมโพเนนต์แบบฟังก์ชันยังช่วยแก้ปัญหาบางอย่างเกี่ยวกับไวยากรณ์แบบคลาสของ React เช่น
- ต้องผ่าน
props
จากconstructor
ไปsuper
- การเริ่มต้นพร็อพเพอร์ตี้ที่ไม่เป็นระเบียบใน
constructor
- ซึ่งเป็นเหตุผลที่ทีม React ระบุไว้ในขณะนั้นแต่แก้ไขได้ภายใน ES2019
- ปัญหาที่เกิดจาก
this
ไม่ได้อ้างอิงถึงคอมโพเนนต์อีกต่อไป
แสดงความรู้สึกแนวคิดฮุกใน Lit
ดังที่ได้กล่าวไว้ในส่วนคอมโพเนนต์และพร็อพ Lit ไม่ได้เสนอวิธีสร้างองค์ประกอบที่กําหนดเองจากฟังก์ชัน แต่ 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
ในกรณีส่วนใหญ่หมายถึงการอ้างอิงองค์ประกอบที่กําหนดเอง- ตอนนี้คุณสร้างอินสแตนซ์พร็อพเพอร์ตี้ของคลาสเป็นสมาชิกของคลาสได้แล้ว การดำเนินการนี้จะช่วยจัดระเบียบการใช้งานที่อิงตามตัวสร้าง
ตัวควบคุมการโต้ตอบ
แนวคิดหลักเบื้องหลัง Hooks อยู่ใน 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
จะทำให้ระบบเรียกใช้วงจรของตัวควบคุมควบคู่ไปกับตัวควบคุมของโฮสต์ ตัวอย่างเช่น ดูตัวอย่างนาฬิกาจากส่วน State &Lifecycle ดังนี้
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 ต้องการใช้ในเคสพื้นฐานส่วนใหญ่
ตอนนี้คุณต้องแปลการเรียกกลับวงจรชีวิตของ React เป็นการเรียกกลับของคอนโทรลเลอร์ กล่าวโดยสรุป:
componentDidMount
- ถึง
connectedCallback
ของ LitElement - ไปยัง
hostConnected
ของตัวควบคุม
- ถึง
ComponentWillUnmount
- ไปยัง
disconnectedCallback
ของ LitElement - ไปยัง
hostDisconnected
ของตัวควบคุม
- ไปยัง
ดูข้อมูลเพิ่มเติมเกี่ยวกับการแปลงวงจรชีวิตของ React เป็นวงจรชีวิตของ Lit ได้ที่ส่วนสถานะและวงจร
จากนั้นใช้เมธอด hostConnected
callback และ 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();
}
ตอนนี้นาฬิกาน่าจะเริ่มเดินแล้ว
สำหรับการเปรียบเทียบในเชิงลึกของ Use Case ที่พบบ่อยเกี่ยวกับ Hook โปรดดูส่วน หัวข้อขั้นสูง - Hook
8. เด็ก
ในส่วนนี้ คุณจะได้เรียนรู้วิธีใช้ช่องเพื่อจัดการรายการย่อยใน Lit
สล็อตและเด็ก
สล็อตช่วยให้คุณคอมโพสชันได้โดยอนุญาตให้ฝังคอมโพเนนต์
ใน React องค์ประกอบย่อยจะรับค่าผ่านพร็อพ ช่องเริ่มต้นคือ props.children
และฟังก์ชัน render
จะกำหนดตำแหน่งของช่องโฆษณาเริ่มต้น เช่น
const MyArticle = (props) => {
return <article>{props.children}</article>;
};
โปรดทราบว่า props.children
คือคอมโพเนนต์ React ไม่ใช่องค์ประกอบ HTML
ใน Lit องค์ประกอบย่อยจะประกอบอยู่ในฟังก์ชันการแสดงผลด้วยองค์ประกอบสล็อต โปรดทราบว่าองค์ประกอบย่อยจะไม่รับค่าในลักษณะเดียวกับ React ใน Lit องค์ประกอบย่อยคือ HTMLElement ที่แนบมากับช่อง ไฟล์แนบนี้เรียกว่าการฉายภาพ
@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>
`;
}
}
เนื้อหาเริ่มต้นของสล็อต
สล็อตจะแสดงแผนผังย่อยเมื่อไม่มีโหนดที่ฉายไปยังช่องโฆษณานั้น เมื่อฉายโหนดไปยังสล็อต สล็อตนั้นจะไม่แสดงแผนผังย่อยและแสดงโหนดที่ฉายแทน
@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. การอ้างอิง
บางครั้งนักพัฒนาซอฟต์แวร์อาจต้องเข้าถึง API ของ HTMLElement
ในส่วนนี้ คุณจะได้เรียนรู้วิธีรับการอ้างอิงองค์ประกอบใน Lit
ข้อมูลอ้างอิงของรีแอ็กชัน
คอมโพเนนต์รีแอ็กชันจะเปลี่ยนเป็นชุดการเรียกใช้ฟังก์ชันที่สร้าง DOM เสมือนเมื่อเรียกใช้ ReactDOM จะตีความ DOM เสมือนนี้และแสดงผล HTMLElement
ใน 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
ใน Lit คือ HTMLElement ที่แสดงผลโดยตัวตกแต่ง @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
(สร้างตัวรับสำหรับHTMLInputElement
) - ประกาศและแนบการเรียกกลับเหตุการณ์คลิกที่ชื่อ
onButtonClick
- เน้นการป้อนข้อมูลเมื่อคลิกปุ่ม
ใน JavaScript ตัวตกแต่ง @query
และ @queryAll
จะทำงาน querySelector
และ querySelectorAll
ตามลำดับ รายการนี้เทียบเท่ากับ @query('input') inputEl!: HTMLInputElement;
ใน JavaScript
get inputEl() {
return this.renderRoot.querySelector('input');
}
หลังจากคอมโพเนนต์ Lit คอมมิตเทมเพลตของเมธอด render
ไปยังรูทของ my-element
แล้ว ตอนนี้ตัวตกแต่ง @query
จะอนุญาตให้ inputEl
แสดงผลองค์ประกอบ input
รายการแรกที่พบในรูทการแสดงผล จะแสดงผล null
หาก @query
ไม่พบองค์ประกอบที่ระบุ
หากมีองค์ประกอบ input
หลายรายการในรูทการแสดงผล @queryAll
จะแสดงรายการโหนด
10. รัฐสื่อกลาง
ในส่วนนี้ คุณจะได้เรียนรู้วิธีสื่อกลางสถานะระหว่างคอมโพเนนต์ใน Lit
คอมโพเนนต์ที่นำมาใช้ซ้ำได้
แสดงความรู้สึกเลียนแบบไปป์ไลน์การแสดงผลที่ทำงานด้วยโฟลว์ข้อมูลจากด้านบนลงล่าง องค์ประกอบหลักจะส่งสถานะไปยังองค์ประกอบย่อยผ่านพร็อพ เด็กๆ สื่อสารกับผู้ปกครองผ่านการเรียกกลับที่พบในอุปกรณ์ประกอบฉาก
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 ได้ แต่รูปแบบทั่วไปจะแตกต่างออกไป คอมโพเนนต์รีแอ็กชันในตัวอย่างด้านบนอาจเขียนเป็นองค์ประกอบ 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 นั้นเป็นเรื่องปกติที่จะใช้ฮุกเพื่อจัดการสถานะ คุณสามารถสร้างคอมโพเนนต์ MyCounter
ได้โดยนําคอมโพเนนต์ CounterButton
มาใช้ซ้ำ โปรดสังเกตว่ามีการส่ง addToCounter
ไปยังอินสแตนซ์ CounterButton
ทั้ง 2 รายการอย่างไร
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
- สร้าง Callback ที่เพิ่มหมายเลขไปยังสถานะ
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
directive styleMap
ช่วยให้ใช้ 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 ที่มีการเชื่อมโยง
- กำหนดสีของ
h1
2 รายการที่มีรหัส
ประโยชน์ของการใช้แท็กเทมเพลต 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 แบบดั้งเดิมที่เปรียบเทียบความแตกต่างของโหนดแต่ละโหนด แต่จะใช้ฟีเจอร์ด้านประสิทธิภาพที่มีอยู่ในข้อกำหนดนิพจน์เทมเพลตที่ติดแท็กของ ES2015 แทน นิพจน์เทมเพลตที่ติดแท็กคือสตริงนิพจน์เทมเพลตที่มีฟังก์ชันแท็กแนบอยู่
ตัวอย่างลิเทอรัลของเทมเพลตมีดังนี้
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 มีประสิทธิภาพเหนือกว่า Virtual DOM แบบดั้งเดิมส่วนใหญ่ทํางานส่วนใหญ่ใน JavaScript แต่จริงๆ แล้วเทมเพลตที่ติดแท็กมีลักษณะการทำงานส่วนใหญ่ที่แตกต่างกันใน C++ ของเบราว์เซอร์
หากต้องการเริ่มต้นใช้งานนิพจน์เทมเพลตที่ติดแท็ก HTML กับ React หรือ Preact ทีม Lit ขอแนะนําให้ใช้ไลบรารี htm
แม้ว่าเว็บไซต์ Google Codelabs และตัวแก้ไขโค้ดออนไลน์หลายตัวจะมีกรณีเช่นเดียวกับเว็บไซต์ Google Codelabs และตัวแก้ไขโค้ดออนไลน์หลายรายการ คุณจะเห็นว่าการไฮไลต์ไวยากรณ์ตามตัวอักษรของเทมเพลตที่ติดแท็กนั้นไม่ใช่รูปแบบที่พบได้บ่อยนัก IDE และเครื่องมือแก้ไขข้อความบางรายการรองรับอุปกรณ์โดยค่าเริ่มต้น เช่น Atom และเครื่องมือไฮไลต์ Codeblock ของ GitHub นอกจากนี้ ทีม Lit ยังทำงานร่วมกับชุมชนอย่างใกล้ชิดเพื่อดูแลรักษาโปรเจ็กต์ต่างๆ เช่น lit-plugin
ซึ่งเป็นปลั๊กอิน VS Code ที่จะช่วยเพิ่มการไฮไลต์ไวยากรณ์ การตรวจสอบประเภท และ IntelliSense ให้กับโปรเจ็กต์ 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 จริง ซึ่งได้แก่ พร็อพเพอร์ตี้, แอตทริบิวต์, Listener เหตุการณ์ และทั้งหมด
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 ไม่ได้ใช้ DOM เสมือนแบบทั่วไป แต่ใช้ DOM API โดยตรง ขนาดของ Lit 2 จึงลดขนาดลงและใช้ gzip น้อยกว่า 5 KB เมื่อเทียบกับ React (2.8 KB) + record-dom's (39.4 KB) 40 KB ที่ถูกลดขนาดและใช้ gizp
กิจกรรม
React ใช้ระบบเหตุการณ์สังเคราะห์ ซึ่งหมายความว่า react-dom ต้องกําหนดเหตุการณ์ทั้งหมดที่จะใช้ในคอมโพเนนต์ทุกรายการ และให้ตัวรับเหตุการณ์ camelCase ที่เทียบเท่าสําหรับโหนดแต่ละประเภท ด้วยเหตุนี้ JSX จึงไม่มีเมธอดในการกำหนด Listener เหตุการณ์สำหรับเหตุการณ์ที่กำหนดเองและนักพัฒนาซอฟต์แวร์ต้องใช้ ref
จากนั้นจึงใช้ Listener อย่างจริงจัง ซึ่งทำให้นักพัฒนาซอฟต์แวร์ได้รับประสบการณ์การใช้งานที่ต่ำกว่ามาตรฐานเมื่อผสานรวมไลบรารีที่ไม่ได้ออกแบบมาเพื่อ React จึงส่งผลให้ต้องเขียน Wrapper สำหรับ React โดยเฉพาะ
Lit-html เข้าถึง DOM โดยตรงและใช้เหตุการณ์แบบเนทีฟ ดังนั้นการเพิ่ม Listener เหตุการณ์จึงง่ายเหมือน @event-name=${eventNameListener}
ซึ่งหมายความว่าการแยกวิเคราะห์รันไทม์จะน้อยลงสําหรับการเพิ่ม Listener เหตุการณ์และการเรียกเหตุการณ์
คอมโพเนนต์และพร็อพ
คอมโพเนนต์ React และองค์ประกอบที่กําหนดเอง
เบื้องหลัง LitElement ใช้องค์ประกอบที่กำหนดเองเพื่อแพ็กเกจคอมโพเนนต์ องค์ประกอบที่กําหนดเองจะทําให้ต้องเสียบางอย่างไปเมื่อใช้กับคอมโพเนนต์ React ในเรื่องของการแยกคอมโพเนนต์ (ดูรายละเอียดเกี่ยวกับสถานะและวงจรชีวิตของคอมโพเนนต์ได้ในส่วนสถานะและวงจร)
ข้อดีบางอย่างขององค์ประกอบที่กำหนดเองในฐานะระบบคอมโพเนนต์มีดังนี้
- มาพร้อมเบราว์เซอร์และไม่ต้องใช้เครื่องมือใดๆ
- ปรับให้พอดีกับทุก API ของเบราว์เซอร์ตั้งแต่
innerHTML
และdocument.createElement
ไปจนถึงquerySelector
- โดยทั่วไปสามารถใช้ได้กับเฟรมเวิร์กต่างๆ
- ลงทะเบียนแบบเลื่อนเวลาได้โดยใช้
customElements.define
และ "hydrate" DOM
ข้อเสียบางอย่างขององค์ประกอบที่กําหนดเองเมื่อเทียบกับคอมโพเนนต์ React
- ไม่สามารถสร้างองค์ประกอบที่กำหนดเองโดยไม่กำหนดคลาส (จึงไม่มีคอมโพเนนต์การทำงานแบบ JSX)
- ต้องมีแท็กปิด
- หมายเหตุ: แม้ว่าผู้ให้บริการเบราว์เซอร์อำนวยความสะดวกของนักพัฒนาซอฟต์แวร์มักจะเสียใจกับข้อกำหนดของแท็กที่ปิดตัวเองอยู่เสมอ ด้วยเหตุนี้ข้อกำหนดใหม่ๆ จึงไม่มีแท็กที่ปิดเอง
- เพิ่มโหนดพิเศษลงในต้นไม้ DOM ซึ่งอาจทำให้เกิดปัญหาการจัดวาง
- ต้องลงทะเบียนผ่าน JavaScript
Lit ใช้องค์ประกอบที่กําหนดเองแทนระบบองค์ประกอบที่กําหนดเอง เนื่องจากองค์ประกอบที่กําหนดเองสร้างขึ้นในเบราว์เซอร์ และทีม Lit เชื่อว่าประโยชน์แบบข้ามเฟรมเวิร์กมีมากกว่าประโยชน์ที่ได้จากเลเยอร์การแยกแยะคอมโพเนนต์ อันที่จริงแล้ว ความพยายามของทีม Lit ในเรื่อง AI ได้แก้ปัญหาหลักๆ เกี่ยวกับการลงทะเบียน JavaScript ไปแล้ว นอกจากนี้ บริษัทบางแห่ง เช่น GitHub ใช้ประโยชน์จากการลงทะเบียนแบบ Lazy Loading ขององค์ประกอบที่กำหนดเอง เพื่อเพิ่มประสิทธิภาพให้หน้าเว็บอย่างต่อเนื่องโดยให้มีลูกเล่นเพิ่มเติม
การส่งข้อมูลไปยังองค์ประกอบที่กําหนดเอง
ความเข้าใจผิดที่พบได้ทั่วไปเกี่ยวกับองค์ประกอบที่กําหนดเองคือสามารถส่งข้อมูลเป็นสตริงได้เท่านั้น ความเข้าใจผิดนี้อาจมาจากข้อเท็จจริงที่ว่าแอตทริบิวต์ขององค์ประกอบจะเขียนเป็นสตริงได้เท่านั้น แม้จะเป็นจริงว่า Lit จะแคสต์แอตทริบิวต์สตริงไปยังประเภทที่กำหนดไว้ แต่องค์ประกอบที่กำหนดเองยังยอมรับข้อมูลที่ซับซ้อนเป็นพร็อพเพอร์ตี้ได้ด้วย
ตัวอย่างเช่น จากคําจํากัดความ 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>
สถานะและวงจร
Callback วงจรชีวิตของ React อื่นๆ
static getDerivedStateFromProps
ไม่มีสิ่งที่เทียบเท่าใน Lit เนื่องจากพร็อพและสถานะเป็นพร็อพเพอร์ตี้คลาสเดียวกัน
shouldComponentUpdate
- ค่าเทียบเท่าในภาษา C คือ
shouldUpdate
- เรียกใช้เมื่อแสดงผลครั้งแรก ซึ่งแตกต่างจาก React
- ฟังก์ชันการทำงานคล้ายกับ
shouldComponentUpdate
ของ React
getSnapshotBeforeUpdate
ในภาษาไทย getSnapshotBeforeUpdate
คล้ายกับทั้ง update
และ willUpdate
willUpdate
- โทรก่อนวันที่
update
willUpdate
จะเรียกก่อนrender
ต่างจากgetSnapshotBeforeUpdate
- การเปลี่ยนแปลงพร็อพเพอร์ตี้แบบเรียลไทม์ใน
willUpdate
จะไม่ทริกเกอร์รอบการอัปเดตอีกครั้ง - เป็นตำแหน่งที่ดีในการคำนวณค่าพร็อพเพอร์ตี้ที่ขึ้นอยู่กับพร็อพเพอร์ตี้อื่นๆ และใช้ในขั้นตอนการอัปเดตที่เหลือ
- ระบบจะเรียกใช้เมธอดนี้ในเซิร์ฟเวอร์ใน SSR ดังนั้นจึงไม่แนะนําให้เข้าถึง DOM ที่นี่
update
- โทรหาหลังวันที่
willUpdate
update
จะเรียกก่อนrender
ต่างจากgetSnapshotBeforeUpdate
- การเปลี่ยนแปลงพร็อพเพอร์ตี้แบบเรียลไทม์ใน
update
จะไม่ทริกเกอร์รอบการอัปเดตอีกครั้งหากมีการเปลี่ยนแปลงก่อนการเรียกsuper.update
- เป็นที่ที่ดีในการบันทึกข้อมูลจาก DOM รอบๆ คอมโพเนนต์ก่อนที่เอาต์พุตที่แสดงผลจะผูกเข้ากับ DOM
- ระบบจะไม่เรียกใช้เมธอดนี้ในเซิร์ฟเวอร์ใน SSR
Lifecycle Callback อื่นๆ ของ Lit
Callbacks เกี่ยวกับวงจรมีหลายรายการที่ไม่ได้กล่าวถึงในส่วนก่อนหน้านี้เนื่องจากไม่มีรายการที่คล้ายกันใน React ดังนี้
attributeChangedCallback
จะมีการเรียกใช้เมื่อ observedAttributes
ขององค์ประกอบรายการใดรายการหนึ่งมีการเปลี่ยนแปลง ทั้ง observedAttributes
และ attributeChangedCallback
เป็นส่วนหนึ่งของข้อกำหนดเฉพาะขององค์ประกอบที่กำหนดเอง และ Lit นำไปใช้งานเบื้องหลังเพื่อให้บริการ API แอตทริบิวต์สำหรับองค์ประกอบ Lit
adoptedCallback
เรียกใช้เมื่อย้ายคอมโพเนนต์ไปยังเอกสารใหม่ เช่น จาก documentFragment
ของ HTMLTemplateElement
ไปยัง document
หลัก การคอลแบ็กนี้เป็นส่วนหนึ่งของข้อกำหนดขององค์ประกอบที่กำหนดเองด้วย และควรใช้กับ Use Case ขั้นสูงเท่านั้นเมื่อคอมโพเนนต์เปลี่ยนเอกสาร
พร็อพเพอร์ตี้และวิธีการใช้วงจรอื่นๆ
เมธอดและพร็อพเพอร์ตี้เหล่านี้จะเป็นสมาชิกชั้นเรียนที่คุณสามารถเรียกใช้ ลบล้าง หรือรอเพื่อช่วยจัดการกระบวนการในวงจรได้
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
แปลงค่า ปัญหานี้เกิดขึ้นได้ทั่วไปเมื่อคอมโพเนนต์แสดงผลคอมโพเนนต์ย่อยและรอบการแสดงผลต้องสอดคล้องกัน เช่น
class MyElement extends LitElement {
...
async getUpdateComplete() {
await super.getUpdateComplete();
await this.myChild.updateComplete;
}
}
performUpdate
วิธีนี้คือสิ่งที่เรียก Callback ของวงจรการอัปเดต โดยทั่วไปแล้ว คุณไม่จำเป็นต้องใช้ตัวเลือกนี้ ยกเว้นในกรณีที่ต้องอัปเดตแบบซิงค์กันหรือกำหนดเวลาเอง ซึ่งเกิดขึ้นไม่บ่อยนัก
hasUpdated
พร็อพเพอร์ตี้นี้จะเป็น true
หากคอมโพเนนต์มีการอัปเดตอย่างน้อย 1 ครั้ง
isConnected
ส่วนหนึ่งของข้อมูลจำเพาะขององค์ประกอบที่กำหนดเอง คุณสมบัตินี้จะเป็น true
หากองค์ประกอบแนบอยู่กับแผนผังเอกสารหลักในขณะนี้
การแสดงภาพวงจรการอัปเดต Lit อัปเดต
วงจรการอัปเดตมี 3 ส่วน ได้แก่
- ก่อนการอัปเดต
- อัปเดต
- หลังการอัปเดต
ก่อนอัปเดต
หลังจากวันที่ requestUpdate
ระบบจะรอการอัปเดตตามกำหนดการ
อัปเดต
หลังการอัปเดต
ฮุก
ทำไมต้องฮุก
มีการนำฮุกเข้าสู่ React สำหรับ Use Case ของคอมโพเนนต์ฟังก์ชันอย่างง่ายที่ต้องระบุสถานะ ในหลายกรณีง่ายๆ คอมโพเนนต์ฟังก์ชันที่มีฮุกมักจะเรียบง่ายและอ่านง่ายกว่าคอมโพเนนต์คลาส อย่างไรก็ตาม เมื่อมีการอัปเดตสถานะแบบไม่พร้อมกันและส่งข้อมูลระหว่างฮุกหรือเอฟเฟกต์ รูปแบบ Hook มักจะไม่เพียงพอ และโซลูชันที่อิงตามคลาส เช่น เครื่องมือควบคุมเชิงรับก็มีแนวโน้มที่จะโดดเด่นออกมา
ฮุกและตัวควบคุมคําขอ API
การเขียนฮุกที่ขอข้อมูลจาก API เป็นเรื่องปกติ ตัวอย่างเช่น มาดูคอมโพเนนต์ฟังก์ชัน React นี้ที่ทําสิ่งต่อไปนี้
index.tsx
- แสดงผลข้อความ
- แสดงผลคำตอบของ
useAPI
- รหัสผู้ใช้ + ชื่อผู้ใช้
- ข้อความแสดงข้อผิดพลาด
- 404 เมื่อถึงผู้ใช้ที่ 11 (ตามการออกแบบ)
- ข้อผิดพลาดในการยกเลิกหากการดึงข้อมูล API ถูกยกเลิก
- ข้อความการโหลด
- แสดงผลปุ่มดำเนินการ
- ผู้ใช้ถัดไป: ซึ่งดึงข้อมูล API สําหรับผู้ใช้คนถัดไป
- ยกเลิก: ซึ่งจะยกเลิกการดึงข้อมูล API และแสดงข้อผิดพลาด
useApi.tsx
- กำหนด
useApi
ฮุกที่กำหนดเอง - จะไม่ซิงค์ออบเจ็กต์ผู้ใช้จาก API หรือไม่
- การปล่อยก๊าซ:
- ชื่อผู้ใช้
- กำลังโหลดการดึงข้อมูลหรือไม่
- ข้อความแสดงข้อผิดพลาดที่ปรากฏ
- ฟังก์ชันการเรียกกลับเพื่อยกเลิกการดึงข้อมูล
- ยกเลิกการดึงข้อมูลที่กำลังดำเนินการอยู่หากถอดออก
- กำหนด
ดูการติดตั้งใช้งาน Lit + ตัวควบคุมการโต้ตอบ
สรุปประเด็นสำคัญ:
- ตัวควบคุมแบบรีแอกทีฟคล้ายกับฮุกที่กำหนดเองมากที่สุด
- การส่งข้อมูลที่แสดงผลไม่ได้ระหว่าง Callback และเอฟเฟกต์
- React ใช้
useRef
เพื่อส่งข้อมูลระหว่างuseEffect
ถึงuseCallback
- Lit ใช้พร็อพเพอร์ตี้คลาสส่วนตัว
- React เลียนแบบลักษณะการทำงานของพร็อพเพอร์ตี้ส่วนตัวของชั้นเรียน
- React ใช้
นอกจากนี้ หากคุณชอบไวยากรณ์คอมโพเนนต์ฟังก์ชัน React ที่มีฮุก แต่ต้องการใช้สภาพแวดล้อมแบบไม่สร้างของ Lit ทีม Lit ขอแนะนำไลบรารี Haunted
เด็ก
ช่องเริ่มต้น
เมื่อไม่ได้รับแอตทริบิวต์ slot
องค์ประกอบ HTML ระบบจะกำหนดให้กับช่องที่ไม่มีชื่อซึ่งเป็นค่าเริ่มต้น ในตัวอย่างด้านล่าง MyApp
จะรวม 1 ย่อหน้าไว้ในช่องที่ตั้งชื่อ ส่วนย่อหน้าอื่นๆ จะเป็นช่องที่ไม่มีชื่อโดยค่าเริ่มต้น"
@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
ได้ ในตัวอย่างด้านล่าง ช่องโฆษณาแรกที่พบใน 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>
`;
}
}
อ้างอิง
การสร้างข้อมูลอ้างอิง
ทั้ง Lit และ React จะแสดงการอ้างอิงไปยัง HTMLElement หลังจากที่เรียกฟังก์ชัน render
แล้ว แต่คุณควรตรวจสอบวิธีที่ React และ Lit เขียน DOM ซึ่งส่งคืนภายหลังผ่านมัณฑนากรของ Lit @query
หรือ React Reference
React เป็นไปป์ไลน์แบบฟังก์ชันที่สร้างคอมโพเนนต์ React ไม่ใช่ HTMLElement เนื่องจากมีการประกาศ Ref ก่อนที่จะแสดงผล HTMLElement ระบบจึงจัดสรรพื้นที่ในหน่วยความจำ นี่จึงเป็นเหตุผลที่ทำให้คุณเห็น null
เป็นค่าเริ่มต้นของการอ้างอิง เนื่องจากองค์ประกอบ DOM จริงยังไม่ได้สร้าง (หรือแสดงผล) เช่น useRef(null)
หลังจากที่ ReactDOM แปลงคอมโพเนนต์ React เป็น HTMLElement แล้ว ระบบจะมองหาแอตทริบิวต์ที่ชื่อว่า ref
ใน ReactComponent ReactDOM จะวางการอ้างอิง HTMLElement ไปยัง ref.current
หากมี
LitElement ใช้html
ฟังก์ชันแท็กเทมเพลตจาก lit-html เพื่อคอมไพล์องค์ประกอบเทมเพลต LitElement จะประทับเนื้อหาของเทมเพลตลงใน Shadow DOM ขององค์ประกอบที่กําหนดเองหลังจากแสดงผล Shadow DOM คือแผนผัง DOM ที่มีขอบเขตซึ่งห่อหุ้มโดยรูทเงา จากนั้นตัวตกแต่ง @query
จะสร้างตัวรับสำหรับพร็อพเพอร์ตี้ ซึ่งโดยพื้นฐานแล้วจะดำเนินการ this.shadowRoot.querySelector
ในรูทที่มีขอบเขต
ค้นหาองค์ประกอบหลายรายการ
ในตัวอย่างที่ด้านล่าง ดีคอร์เลเตอร์ @queryAll
จะแสดงผลย่อหน้า 2 ย่อหน้าในรากเงาเป็น 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
จะสร้าง Getter สำหรับ paragraphs
ซึ่งแสดงผลผลลัพธ์ของ this.shadowRoot.querySelectorAll()
ใน JavaScript สามารถประกาศ Getter ให้ดำเนินการตามจุดประสงค์เดียวกันได้ ดังนี้
get paragraphs() {
return this.renderRoot.querySelectorAll('p');
}
องค์ประกอบที่เปลี่ยนแปลงในข้อความค้นหา
เครื่องมือตกแต่ง @queryAsync
เหมาะกับการจัดการโหนดที่เปลี่ยนแปลงได้ตามสถานะของพร็อพเพอร์ตี้ขององค์ประกอบอื่น
ในตัวอย่างด้านล่าง @queryAsync
จะค้นหาองค์ประกอบย่อหน้าแรก แต่องค์ประกอบย่อหน้าจะแสดงผลเมื่อ renderParagraph
สุ่มสร้างจำนวนคี่เท่านั้น คำสั่ง @queryAsync
จะแสดงสัญญาที่จะได้รับการแก้ไขเมื่อย่อหน้าแรกพร้อมใช้งาน
@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 นั้น กฎที่ใช้คือการใช้ Callback เนื่องจากมีสถานะเป็นสื่อกลางโดย 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 ภายในองค์ประกอบที่กำหนดเอง Shadow Roots จะสร้างแผนผังเงาที่แยกจากแผนผังเอกสารหลัก ซึ่งหมายความว่ารูปแบบส่วนใหญ่จะอยู่ในเอกสารนี้ บางรูปแบบอาจหลุดผ่านไปได้ เช่น สีและสไตล์อื่นๆ ที่เกี่ยวข้องกับแบบอักษร
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 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
ซึ่งจะแสดงผลเทมเพลตของคอมโพเนนต์ไปยังองค์ประกอบที่กำหนดเองแทนที่จะเป็นรูทเงาที่แนบอยู่กับองค์ประกอบที่กำหนดเอง ซึ่งจะทำให้คุณสูญเสียการห่อหุ้มสไตล์ การห่อหุ้ม DOM และสLOTS
เวอร์ชันที่ใช้งานจริง
IE 11
หากคุณต้องการรองรับเบราว์เซอร์รุ่นเก่าอย่าง IE 11 คุณจะต้องโหลดโพลีฟิลบางส่วนซึ่งมีขนาดประมาณ 33 KB ดูข้อมูลเพิ่มเติมได้ที่นี่
กลุ่มแบบมีเงื่อนไข
ทีมงาน Lit แนะนำให้ใช้ 2 ชุด คือชุดหนึ่งสำหรับ IE 11 และอีกชุดสำหรับเบราว์เซอร์สมัยใหม่ ซึ่งมีประโยชน์หลายประการ ดังนี้
- การให้บริการ ES 6 จะรวดเร็วขึ้นและพร้อมให้บริการแก่ลูกค้าส่วนใหญ่ของคุณ
- ES 5 ที่แปลงแล้วทำให้ขนาดของกลุ่มเพิ่มขึ้นอย่างมาก
- กลุ่มแบบมีเงื่อนไขช่วยให้คุณได้รับสิ่งที่ดีที่สุดจากทั้ง 2 โลก
- รองรับ IE 11
- ไม่ทำงานช้าลงในเบราว์เซอร์สมัยใหม่
ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีสร้างแพ็กเกจที่ให้บริการอย่างมีเงื่อนไขได้ในเว็บไซต์เอกสารประกอบของเราที่นี่