Lit for React Developers

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

แผนภูมิแท่งของขนาดแพ็กเกจที่ย่อและบีบอัดในหน่วย KB Lit bar มีขนาด 5 KB และ React + React DOM มีขนาด 42.2 KB

รวดเร็ว

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

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

แผนภูมิแท่งแบบกลุ่มของประสิทธิภาพที่เปรียบเทียบ Lit กับ React ในหน่วยมิลลิวินาที (ยิ่งต่ำยิ่งดี)

ไม่จำเป็นต้องสร้าง

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

ภาพหน้าจอของ IDE ที่แสดงการตรวจสอบประเภทที่ไม่เหมาะสมสำหรับการตั้งค่าบูลีนที่ระบุเป็นตัวเลข

ภาพหน้าจอของ IDE ที่แสดงคำแนะนำของ Intellisense

เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์จะติดตั้งมาในเบราว์เซอร์

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

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

รูปภาพของเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ของ Chrome ที่แสดง $0 returns <mwc-textfield>, $0.value returns hello world, $0.outlined returns true และ {$0} แสดงการขยายพร็อพเพอร์ตี้

สร้างขึ้นโดยคำนึงถึงการแสดงผลฝั่งเซิร์ฟเวอร์ (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

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

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

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

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

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

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

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

  • กดปุ่มดาวน์โหลด
  • แตกเนื้อหาของไฟล์ tar ลงในไดเรกทอรี
  • (หากเป็น TS) ตั้งค่า quick tsconfig ที่เอาต์พุตเป็นโมดูล ES และ es2015+
  • ติดตั้งเซิร์ฟเวอร์สำหรับนักพัฒนาซอฟต์แวร์ที่สามารถระบุโมดูลเปล่า (ทีม Lit ขอแนะนำ @web/dev-server)
  • เรียกใช้เซิร์ฟเวอร์สำหรับนักพัฒนาซอฟต์แวร์และเปิดเบราว์เซอร์ (หากใช้ @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}
  }
}
  • ซึ่งมีฟังก์ชันการทำงานเหมือนกับ @property TS 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 ที่เทียบเท่ากับตัวตกแต่ง @customElement TS
<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.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>`;
}
  • โดยมีค่าเท่ากับ 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>&Sigma;: {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>&Sigma; ${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 พร้อมการเชื่อมโยง
  • ตั้งค่าสีของ 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 แบบเดิมที่เปรียบเทียบแต่ละโหนด แต่จะใช้ฟีเจอร์ด้านประสิทธิภาพที่อยู่ในข้อกำหนด 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 ส่วน ได้แก่

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

ก่อนการอัปเดต

กราฟแบบมีทิศทางแบบไม่มีวงจรของโหนดที่มีชื่อเรียกกลับ ตัวสร้างเพื่อขออัปเดต @property ไปยัง Property Setter, attributeChangedCallback ไปยัง Property Setter Property Setter ไปยัง hasChanged hasChanged ไปยัง requestUpdate requestUpdate ชี้ไปยังกราฟวงจรการอัปเดตถัดไป

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

อัปเดต

กราฟแบบมีทิศทางแบบไม่มีวงจรของโหนดที่มีชื่อการเรียกกลับ ลูกศรจากรูปภาพก่อนหน้าของจุดวงจรการอัปเดตก่อนหน้าชี้ไปที่ performUpdate, performUpdate ชี้ไปที่ shouldUpdate, shouldUpdate ชี้ไปที่ทั้ง &quot;อัปเดตให้เสร็จสมบูรณ์หากเป็นเท็จ&quot; และ willUpdate, willUpdate ชี้ไปที่ update, update ชี้ไปที่ทั้ง render และกราฟวงจรหลังการอัปเดตถัดไป, render ชี้ไปที่กราฟวงจรหลังการอัปเดตถัดไปด้วย

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

กราฟแบบมีทิศทางแบบไม่มีวงจรของโหนดที่มีชื่อการเรียกกลับ ลูกศรจากรูปภาพก่อนหน้าของจุดวงจรการอัปเดตไปยัง firstUpdated จาก firstUpdated ไปยัง updated จาก updated ไปยัง updateComplete

ฮุก

เหตุผลที่ควรใช้ฮุก

มีการนำ 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 ที่มี 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 ที่แสดงตามเงื่อนไขได้ในเว็บไซต์เอกสารประกอบที่นี่