Lit for React Developers

1. บทนำ

Lit คืออะไร

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

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

วิธีแปลแนวคิดปฏิกิริยาต่างๆ เป็นภาษา Lit เช่น

  • JSX และ เทมเพลท
  • ส่วนประกอบและ ของตกแต่ง
  • รัฐและ วงจร
  • ฮุก
  • เด็ก
  • การอ้างอิง
  • สถานะสื่อกลาง

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

ในตอนท้ายของ Codelab นี้จะแปลงแนวคิดคอมโพเนนต์ React เป็นแอนะล็อก Lit ของตัวเองได้

สิ่งที่ต้องมี

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 ทำงานได้เร็วกว่า 8-10% เมื่อเทียบกับ React ในกรณีที่แย่ที่สุด และเร็วกว่า 50%+ ในกรณีการใช้งานที่พบบ่อยที่สุด

LitElement (คลาสฐานคอมโพเนนต์ของ Lite) เพิ่มค่าใช้จ่ายขั้นต่ำให้กับ 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 ของคุณโดยอัตโนมัติและสร้าง Wrapper ความรู้สึกเพื่อให้คุณไม่ต้องใช้การอ้างอิง

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

การสนับสนุน TypeScript เฟิร์สคลาส

แม้ว่าจะเขียนโค้ด Lit ทั้งหมดได้ใน JavaScript แต่ Lit เขียนด้วย TypeScript และทีม Lit แนะนำให้นักพัฒนาซอฟต์แวร์ใช้ TypeScript ด้วย

ทีม Lit ได้ทำงานร่วมกับชุมชน Lit เพื่อช่วยรักษาโปรเจ็กต์ที่นำการตรวจสอบประเภทสคริปต์และอัจฉริยะมาสู่เทมเพลตของ Lit ทั้งในส่วนของการพัฒนาและช่วงเวลาในการสร้างด้วย lit-analyzer และ lit-plugin

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

ภาพหน้าจอของ IDE แสดงคำแนะนำอย่างชาญฉลาด

เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์มีอยู่ในเบราว์เซอร์

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

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

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

สร้างขึ้นโดยคำนึงถึงการแสดงผลฝั่งเซิร์ฟเวอร์ (SSR)

Lit 2 สร้างขึ้นโดยคำนึงถึงการรองรับ SSR ตอนที่เขียน Codelab นี้ ทีม Lit ยังไม่ได้เปิดตัวเครื่องมือ SSR ในรูปแบบที่เสถียร แต่ทีม Lit ได้ติดตั้งใช้งานคอมโพเนนต์ที่แสดงผลฝั่งเซิร์ฟเวอร์ในผลิตภัณฑ์ต่างๆ ของ Google แล้ว และได้ทดสอบ SSR ภายในแอปพลิเคชัน React ทีม Lit คาดว่าจะมีการเปิดตัวเครื่องมือเหล่านี้สู่ภายนอกใน GitHub ในเร็วๆ นี้

ในระหว่างนี้ คุณสามารถติดตามความคืบหน้าของทีม Lit ได้ที่นี่

ได้ข้อเสนอต่ำ

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

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

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

3. การตั้งค่า สำรวจสนามเด็กเล่น

มี 2 วิธีในการทำ Codelab นี้ ได้แก่

  • ซึ่งทำได้ทั้งแบบออนไลน์และในเบราว์เซอร์
  • (ขั้นสูง) คุณสามารถดำเนินการดังกล่าวได้ในเครื่องของคุณเองโดยใช้ VS Code

การเข้าถึงโค้ด

ตลอดทั้ง Codelab จะมีลิงก์ไปยัง Lit Play แบบ "จำลอง" ดังนี้

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

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

// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';

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

สำรวจ UI สนามเด็กเล่นที่มีไฟสว่าง

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

ภาพหน้าจอ UI ของ Litground ที่ไฮไลต์ส่วนต่างๆ ที่คุณจะใช้ใน Codelab นี้

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

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

ประโยชน์ที่ได้รับจากการตั้งค่า VS Code มีดังนี้

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

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

  • กดปุ่มดาวน์โหลด
  • แตกเนื้อหาของไฟล์ tar ลงในไดเรกทอรี
  • (หาก TS) ตั้งค่า tsconfig อย่างรวดเร็ว ซึ่งแสดงผลโมดูล es และ es2015+
  • ติดตั้งเซิร์ฟเวอร์ dev ที่สามารถแก้ปัญหาตัวระบุโมดูลเปล่า (ทีม 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 ของตนเองได้อย่างง่ายดาย เทมเพลตของ Lite มีวัตถุประสงค์ที่คล้ายกัน กล่าวคือ แสดง 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>) และห่อด้วยเทมเพลตที่ติดแท็กตามตัวอักษร 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 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 คือไวยากรณ์การเชื่อมโยงข้อมูล ตัวอย่างเช่น นำอินพุตของรีแอ็กชันนี้ไปใช้กับการเชื่อมโยง

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 เนื่องจาก disabled="false" ยังคงอ่านค่าว่าเป็นจริงโดย DOM เนื่องจาก 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 (แสดงบทคัดย่อจากเหตุการณ์เหล่านี้)

ไวยากรณ์ของเครื่องจัดการเหตุการณ์ Lit

html`<my-element @event-name=${() => {...}}></my-element>`;
  • คำนำหน้า @ คือไวยากรณ์การเชื่อมโยงสำหรับ Listener เหตุการณ์
  • เทียบเท่ากับ inputRef.addEventListener('event-name', ...)
  • ใช้ชื่อเหตุการณ์ DOM ดั้งเดิม

5. ส่วนประกอบและ ของตกแต่ง

ในส่วนนี้ คุณจะได้เรียนรู้เกี่ยวกับคอมโพเนนต์และฟังก์ชันของคลาส Lit เราจะกล่าวถึงสถานะและ Hook อย่างละเอียดในหัวข้อถัดๆ ไป

ส่วนประกอบของชั้นเรียนและ LitElement

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 เกี่ยวกับ "คุณสมบัติเชิงรับ" เป็นการผสมผสานสถานะของ 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
  • คล้ายกับสถานะของรีแอ็กชันแต่เปลี่ยนแปลงได้
  • สถานะภายในส่วนตัวที่โดยทั่วไปจะมีการเข้าถึงจากภายในคอมโพเนนต์หรือคลาสย่อย

อายุการใช้งาน

วงจรของ 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 เช่นกัน
  • ไม่จำเป็นต้องส่งต่อสิ่งใดไปยัง 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>`;
}
  • จำนวนที่เทียบเท่าคือ render เช่นกัน
  • แสดงผลลัพธ์ที่แสดงผลได้ เช่น TemplateResult หรือ string เป็นต้น
  • render() ควรเป็นฟังก์ชันที่ไม่ซับซ้อน คล้ายกับ React
  • จะแสดงผลต่อโหนดใดก็ตามที่ createRenderRoot() ส่งคืน (ShadowRoot โดยค่าเริ่มต้น)

componentDidMount

componentDidMount คล้ายกับการรวมกันของ Callback วงจรทั้ง firstUpdated ของ Lit และ connectedCallback

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);
  }
}
  • Litเทียบเท่าคือ updated (ใช้ "update" ในภาษาอังกฤษ
  • มีการเรียก updated ในการแสดงภาพเริ่มต้น ซึ่งต่างจาก React
  • ฟังก์ชันคล้ายกับ componentDidUpdate ของ React

componentWillUnmount

// React
componentWillUnmount() {
  this.window.removeEventListener('resize', this.boundOnResize);
}

// Lit
disconnectedCallback() {
  super.disconnectedCallback();
  this.window.removeEventListener('resize', this.boundOnResize);
}
  • จำนวนบัญชีที่เทียบเท่ากันกับ 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')
);

ในตัวอย่างด้านบน มีนาฬิกาแบบง่ายที่ทำงานต่อไปนี้

  • ผลการค้นหาจะแสดงข้อความ "สวัสดีทุกคน! คือ" จากนั้นจะแสดงเวลา
  • ทุกวินาทีจะอัปเดตนาฬิกา
  • เมื่อถอดออกแล้ว เครื่องจะล้างระยะห่างของการเรียกเห็บ

ก่อนอื่น ให้เริ่มต้นด้วยการประกาศคลาสคอมโพเนนต์

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

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

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

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

  • ต้องผ่าน props จาก constructor ไปยัง super
  • การเริ่มต้นพร็อพเพอร์ตี้ที่ไม่เป็นระเบียบใน constructor
    • ซึ่งเป็นเหตุผลที่ทีม React ระบุไว้ในขณะนั้นแต่แก้ไขได้ภายใน ES2019
  • ปัญหาที่เกิดจาก this ไม่ได้อ้างอิงถึงคอมโพเนนต์อีกต่อไป

แสดงความรู้สึกแนวคิดฮุกใน Lit

ตามที่ระบุไว้ในคอมโพเนนต์และ ส่วน Props 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 & ส่วนวงจร

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

ในตัวอย่างด้านบน มีนาฬิกาแบบง่ายที่ทำงานต่อไปนี้

  • ผลการค้นหาจะแสดงข้อความ "สวัสดีทุกคน! คือ" จากนั้นจะแสดงเวลา
  • ทุกวินาทีจะอัปเดตนาฬิกา
  • เมื่อถอดออกแล้ว เครื่องจะล้างระยะห่างของการเรียกเห็บ

การสร้างนั่งร้านคอมโพเนนต์

ก่อนอื่นให้เริ่มต้นด้วยการประกาศคลาสคอมโพเนนต์แล้วเพิ่มฟังก์ชัน 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 ของตัวควบคุม

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการแปลวงจรของปฏิกิริยาเป็นวงจรชีวิตของสังคม โปรดดูสถานะและ วงจร

ถัดไป ให้ใช้เมธอด hostConnected Callback และ tick และล้างช่วงเวลาใน hostDisconnected ตามที่ทำในตัวอย่าง State & วงจร

// 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 Components ไม่ใช่องค์ประกอบ 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>
   `;
  }
}

เนื้อหาเริ่มต้นของสล็อต

สล็อตจะแสดงแผนผังย่อยเมื่อไม่มีโหนดที่ฉายไปยังช่องโฆษณานั้น เมื่อฉายโหนดไปยังสล็อต สล็อตนั้นจะไม่แสดงแผนผังย่อยและแสดงโหนดที่ฉายแทน

@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 เสมือนเมื่อเรียกใช้ DOM เสมือนนี้ตีความโดย ReactDOM และแสดงผล 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

Lit "การอ้างอิง" กับ @query

Lit อยู่ใกล้เบราว์เซอร์และสร้างแอบสแตรกต์ที่บางมากเหนือฟีเจอร์ในเบราว์เซอร์

ความรู้สึกที่เทียบเท่ากับ 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 (การสร้าง Getter สำหรับ HTMLInputElement)
  • ประกาศและแนบ Callback ของเหตุการณ์การคลิกที่เรียกว่า 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

ส่วนประกอบที่นำกลับมาใช้ใหม่ได้

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

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 เป็นอาร์กิวเมนต์เมื่อคลิก

แม้ว่าจะสามารถส่ง Callback ใน 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 จะส่งสถานะไปในทิศทางตรงกันข้าม ดังนั้นจึงไม่บ่อยนักที่จะเห็นการจ่ายคอมโพเนนต์ของรีแอ็กชันและฟังเหตุการณ์ในลักษณะเดียวกับคอมโพเนนต์ 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>&Sigma;: {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 ระบบจะเชื่อมโยง Callback เป็น 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 ช่วยให้ใช้ 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 และ เทมเพลท

ลิขสิทธิ์และ DOM เสมือน

Lit-html ไม่มี 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 ที่สอง) เบราว์เซอร์จะไม่สร้างอาร์เรย์ strings ใหม่ต่อการเรียกใช้ฟังก์ชันแท็กแต่ละรายการ เนื่องจากมีการใช้ลิเทอรัลเทมเพลตเดียวกัน (กล่าวคือ อยู่ในตำแหน่งเดียวกันใน AST) ดังนั้น การเชื่อมโยง การแยกวิเคราะห์ และการแคชเทมเพลตของ Lit สามารถใช้ประโยชน์จากฟีเจอร์เหล่านี้โดยไม่ต้องเพิ่มค่าใช้จ่ายในรันไทม์มากนัก

ลักษณะการทำงานในเบราว์เซอร์ในตัวของเทมเพลตที่ติดแท็กนี้ทำให้ Lit ได้เปรียบด้านประสิทธิภาพอย่างมาก DOM เสมือนแบบดั้งเดิมส่วนใหญ่จะทำงานส่วนใหญ่ใน JavaScript แต่จริงๆ แล้วเทมเพลตที่ติดแท็กมีลักษณะการทำงานส่วนใหญ่ที่แตกต่างกันใน C++ ของเบราว์เซอร์

หากต้องการเริ่มใช้เทมเพลตที่ติดแท็ก HTML แบบตรงตัวกับ React หรือ Preact ทีม Lit ขอแนะนำไลบรารี htm

แม้ว่าเว็บไซต์ Google Codelabs และตัวแก้ไขโค้ดออนไลน์หลายรายจะมีกรณีเดียวกัน แต่คุณจะพบว่าการไฮไลต์ไวยากรณ์ตามตัวอักษรของเทมเพลตที่ติดแท็กนั้นไม่ใช่รูปแบบที่พบได้บ่อยนัก IDE และเครื่องมือแก้ไขข้อความบางรายการรองรับอุปกรณ์โดยค่าเริ่มต้น เช่น Atom และเครื่องมือไฮไลต์ Codeblock ของ GitHub นอกจากนี้ ทีม Lit ยังทำงานอย่างใกล้ชิดกับชุมชนเพื่อดูแลรักษาโปรเจ็กต์ต่างๆ เช่น lit-plugin ซึ่งเป็นปลั๊กอิน VS Code ที่จะเพิ่มการไฮไลต์ไวยากรณ์ การตรวจสอบประเภท และหลักสติปัญญาให้กับโปรเจ็กต์ 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.8kb) + record-dom's (39.4kb) 40kb ที่ถูกลดขนาดและใช้ gizp

กิจกรรม

React ใช้ระบบเหตุการณ์สังเคราะห์ ซึ่งหมายความว่า Active-dom ต้องกำหนดทุกเหตุการณ์ที่จะใช้กับคอมโพเนนต์ทั้งหมด และให้ Listener เหตุการณ์แบบ CamlCase สำหรับโหนดแต่ละประเภท ด้วยเหตุนี้ JSX จึงไม่มีเมธอดในการกำหนด Listener เหตุการณ์สำหรับเหตุการณ์ที่กำหนดเองและนักพัฒนาซอฟต์แวร์ต้องใช้ ref จากนั้นจึงใช้ Listener อย่างจริงจัง การทำเช่นนี้ทำให้นักพัฒนาแอปได้รับประสบการณ์การใช้งานในระดับต่ำกว่าเดิมเมื่อผสานรวมไลบรารีที่ไม่มี React อยู่แล้ว ซึ่งทำให้ต้องเขียน Wrapper ที่เจาะจงของ React

Lit-html เข้าถึง DOM โดยตรงและใช้เหตุการณ์แบบเนทีฟ ดังนั้นการเพิ่ม Listener เหตุการณ์จึงง่ายเหมือน @event-name=${eventNameListener} ซึ่งหมายความว่าการแยกวิเคราะห์รันไทม์จะน้อยลงสําหรับการเพิ่ม Listener เหตุการณ์และการเริ่มทํางาน

ส่วนประกอบและ ของตกแต่ง

แสดงความรู้สึกและ องค์ประกอบที่กำหนดเอง

ในเบื้องหลัง LitElement จะใช้องค์ประกอบที่กำหนดเองเพื่อรวมคอมโพเนนต์ต่างๆ องค์ประกอบที่กำหนดเองทำให้เกิดข้อดีข้อเสียบางอย่างระหว่างคอมโพเนนต์ React เมื่อพูดถึงการแยกคอมโพเนนต์ (พูดถึงสถานะและวงจรชีวิตเพิ่มเติมในส่วนสถานะและวงจร)

ระบบคอมโพเนนต์มีข้อดีที่องค์ประกอบที่กำหนดเองบางอย่างมี

  • มาพร้อมเบราว์เซอร์และไม่ต้องใช้เครื่องมือใดๆ
  • ปรับให้พอดีกับทุก API ของเบราว์เซอร์ตั้งแต่ innerHTML และ document.createElement ไปจนถึง querySelector
  • โดยทั่วไปแล้วจะใช้ได้ในกรอบการทำงานต่างๆ
  • ลงทะเบียนแบบ Lazy Loading กับ 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 Lifecycle อื่นๆ

static getDerivedStateFromProps

ไม่มีสิ่งที่เทียบเท่าใน Lit เนื่องจากพร็อพและสถานะเป็นพร็อพเพอร์ตี้คลาสเดียวกัน

shouldComponentUpdate

  • ลิทเทียบเท่า shouldUpdate
  • เรียกใช้เมื่อแสดงผลครั้งแรกซึ่งต่างจาก React
  • ฟังก์ชันคล้ายกับ shouldComponentUpdate ของ React

getSnapshotBeforeUpdate

ใน Lit จะมี getSnapshotBeforeUpdate คล้ายกับทั้ง update และ willUpdate

willUpdate

  • โทรก่อน update
  • สิ่งที่ต่างจาก getSnapshotBeforeUpdate คือจะมีการเรียก willUpdate ก่อน render
  • การเปลี่ยนแปลงพร็อพเพอร์ตี้เชิงรับใน willUpdate จะไม่ทริกเกอร์รอบการอัปเดตอีกครั้ง
  • เป็นตำแหน่งที่ดีในการคำนวณค่าพร็อพเพอร์ตี้ที่ขึ้นอยู่กับพร็อพเพอร์ตี้อื่นๆ และใช้ในขั้นตอนการอัปเดตที่เหลือ
  • วิธีนี้เรียกใช้บนเซิร์ฟเวอร์ใน SSR จึงไม่แนะนำให้เข้าถึง DOM ที่นี่

update

  • โทรหลัง willUpdate
  • สิ่งที่ต่างจาก getSnapshotBeforeUpdate คือจะมีการเรียก update ก่อน render
  • การเปลี่ยนแปลงพร็อพเพอร์ตี้เชิงรับใน update จะไม่ทริกเกอร์รอบการอัปเดตอีกครั้งหากมีการเปลี่ยนแปลงก่อนการเรียกใช้ super.update
  • เป็นตำแหน่งที่ดีในการบันทึกข้อมูลจาก DOM รอบๆ คอมโพเนนต์ก่อนที่เอาต์พุตที่แสดงผลจะผูกเข้ากับ DOM
  • ระบบไม่เรียกใช้เมธอดนี้บนเซิร์ฟเวอร์ใน SSR

Callback ของ Lit Lifecycle อื่นๆ

มี Callback ของวงจรหลายรายการที่ไม่ได้กล่าวถึงในส่วนก่อนหน้าเพราะไม่มีสิ่งที่คล้ายกันใน React ปัจจัยต่างๆ มีดังนี้

attributeChangedCallback

จะมีการเรียกใช้เมื่อ observedAttributes ขององค์ประกอบรายการใดรายการหนึ่งมีการเปลี่ยนแปลง ทั้ง observedAttributes และ attributeChangedCallback เป็นส่วนหนึ่งของข้อกำหนดขององค์ประกอบที่กำหนดเอง และมีการใช้งานโดย Lit ขั้นสูงเพื่อมอบ API แอตทริบิวต์สำหรับองค์ประกอบ Lit

adoptedCallback

เรียกใช้เมื่อย้ายคอมโพเนนต์ไปยังเอกสารใหม่ เช่น จาก documentFragment ของ HTMLTemplateElement ไปยัง document หลัก Callback นี้ยังเป็นส่วนหนึ่งของข้อกำหนดขององค์ประกอบที่กำหนดเอง และควรใช้สำหรับ 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 ส่วน ดังนี้

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

ก่อนอัปเดต

กราฟแบบวนซ้ำที่มีทิศทางของโหนดที่มีชื่อเรียกกลับ เพื่อสร้างคำขออัปเดต @property ไปยังการตั้งค่าพร็อพเพอร์ตี้ ค่าแอตทริบิวต์เปลี่ยน Callback เป็น &quot;ผู้ตั้งค่าพร็อพเพอร์ตี้&quot; เปลี่ยนตัวตั้งค่าพร็อพเพอร์ตี้เป็น hasChangeแล้ว ได้เปลี่ยนเป็น requestUpdate requestUpdate ชี้ไปข้อต่อไปคืออัปเดตกราฟวงจร

หลังจากวันที่ requestUpdate ระบบจะรอการอัปเดตที่กําหนดเวลาไว้

อัปเดต

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

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

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

ฮุก

ทำไมต้องฮุก

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

ฮุกคำขอ API และ คอนโทรลเลอร์

การเขียนฮุกที่ขอข้อมูลจาก API เป็นเรื่องปกติ เช่น ลองดูคอมโพเนนต์ฟังก์ชันรีแอ็กชันนี้ซึ่งทำหน้าที่ต่อไปนี้

  • index.tsx
    • แสดงผลข้อความ
    • แสดงการตอบกลับของ useAPI
      • รหัสผู้ใช้ + ชื่อผู้ใช้
      • ข้อความแสดงข้อผิดพลาด
        • 404 เมื่อเข้าถึงผู้ใช้ 11 (ตามการออกแบบ)
        • ล้มเลิกข้อผิดพลาดหากการดึงข้อมูล API ถูกล้มเลิก
      • ข้อความการโหลด
    • แสดงปุ่มการทำงาน
      • ผู้ใช้รายถัดไป: ซึ่งดึงข้อมูล API สำหรับผู้ใช้รายถัดไป
      • ยกเลิก: ซึ่งจะล้มเลิกการดึงข้อมูล API และแสดงข้อผิดพลาด
  • useApi.tsx
    • กำหนดฮุกที่กำหนดเอง useApi
    • จะไม่ซิงค์ออบเจ็กต์ผู้ใช้จาก API หรือไม่
    • การปล่อยก๊าซ:
      • ชื่อผู้ใช้
      • กำลังโหลดการดึงข้อมูลหรือไม่
      • ข้อความแสดงข้อผิดพลาดที่ปรากฏ
      • มีการเรียกกลับเพื่อล้มเลิกการดึงข้อมูล
    • ล้มเลิกการดึงข้อมูลที่อยู่ระหว่างดำเนินการ

ดูการติดตั้งใช้งาน Lit + ตัวควบคุมการโต้ตอบ

สรุปประเด็นสำคัญ:

  • ตัวควบคุมเชิงรับมีลักษณะคล้ายกับฮุกที่กำหนดเองมากที่สุด
  • การส่งข้อมูลที่แสดงผลไม่ได้ระหว่าง Callback และเอฟเฟกต์
    • React ใช้ useRef เพื่อส่งข้อมูลระหว่าง useEffect ถึง useCallback
    • Lit ใช้พร็อพเพอร์ตี้ของชั้นเรียนส่วนตัว
    • React เลียนแบบลักษณะการทำงานของพร็อพเพอร์ตี้ส่วนตัวของชั้นเรียน

นอกจากนี้ หากคุณชอบไวยากรณ์ของคอมโพเนนต์ฟังก์ชัน React ที่มีฮุก แต่ใช้สภาพแวดล้อมแบบไร้บิลด์แบบเดียวกัน ทีม 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 Components ไม่ใช่ HTMLElements เนื่องจากมีการประกาศการอ้างอิงก่อนที่จะแสดงผล 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 จะสร้าง Getter สำหรับพร็อพเพอร์ตี้ ซึ่งจะดำเนินการ 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

ในการห่อหุ้มรูปแบบและ DOM ภายในองค์ประกอบที่กำหนดเอง Lit จะใช้ Shadow 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 ใช้

.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 และช่องโฆษณา

การผลิต

IE 11

หากคุณต้องการรองรับเบราว์เซอร์รุ่นเก่าอย่าง IE 11 คุณจะต้องโหลดโพลีฟิลบางส่วนซึ่งมีขนาดประมาณ 33 KB ดูข้อมูลเพิ่มเติมได้ที่นี่

กลุ่มแบบมีเงื่อนไข

ทีมงาน Lit แนะนำให้ใช้ 2 ชุด คือชุดหนึ่งสำหรับ IE 11 และอีกชุดสำหรับเบราว์เซอร์สมัยใหม่ ซึ่งมีประโยชน์หลายประการดังนี้

  • การให้บริการ ES 6 จะรวดเร็วขึ้นและพร้อมให้บริการแก่ลูกค้าส่วนใหญ่ของคุณ
  • Transpiled ES 5 ทำให้ขนาดแพ็กเกจเพิ่มขึ้นอย่างมาก
  • แพ็กเกจแบบมีเงื่อนไขช่วยให้คุณได้รับสิ่งที่ดีที่สุดจากทั้ง 2 อย่าง
    • รองรับ IE 11
    • ไม่ทำงานช้าลงในเบราว์เซอร์สมัยใหม่

ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีสร้างแพ็กเกจที่ให้บริการอย่างมีเงื่อนไขได้ในเว็บไซต์เอกสารประกอบของเราที่นี่