วิธีที่ Nordhealth ใช้คุณสมบัติที่กำหนดเองในคอมโพเนนต์เว็บ

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

David Darnes
David Darnes

ผมชื่อ Dave เป็นนักพัฒนาฟรอนท์เอนด์อาวุโสที่ Nordhealth ฉันทำงานด้านการออกแบบและพัฒนาระบบการออกแบบ Nord ซึ่งรวมถึงการสร้างคอมโพเนนต์เว็บสำหรับคลังคอมโพเนนต์ของเรา เราอยากแชร์วิธีแก้ปัญหาเกี่ยวกับการจัดรูปแบบคอมโพเนนต์เว็บโดยใช้คุณสมบัติที่กำหนดเองของ CSS และประโยชน์อื่นๆ ของการใช้คุณสมบัติที่กำหนดเองในระบบการออกแบบและไลบรารีคอมโพเนนต์

วิธีที่เราสร้างคอมโพเนนต์เว็บ

ในการสร้างคอมโพเนนต์เว็บ เราใช้ Lit ซึ่งเป็นไลบรารีที่มีโค้ด Boilerplate จำนวนมาก เช่น สถานะ, สไตล์ที่กำหนดขอบเขต, การสร้างเทมเพลต และอื่นๆ Lit ไม่เพียงแต่มีน้ำหนักเบาเท่านั้น แต่ยังสร้างขึ้นบน JavaScript API แบบเนทีฟด้วย ซึ่งหมายความว่าเราสามารถส่งมอบโค้ดแบบ Lean Bundle ที่ใช้ประโยชน์จากฟีเจอร์ที่เบราว์เซอร์มีอยู่แล้วได้


import {html, css, LitElement} from 'lit';

export class SimpleGreeting extends LitElement {
  static styles = css`:host { color: blue; font-family: sans-serif; }`;

  static properties = {
    name: {type: String},
  };

  constructor() {
    super();
    this.name = 'there';
  }

  render() {
    return html`

Hey ${this.name}, welcome to Web Components!

`
; } } customElements.define('simple-greeting', SimpleGreeting);
คอมโพเนนต์เว็บที่เขียนด้วย Lit

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

การรวมสไตล์ Shadow DOM

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

Shadow DOM ที่ตรวจสอบในเครื่องมือสำหรับนักพัฒนาเว็บ
ตัวอย่าง Shadow DOM ในองค์ประกอบการป้อนข้อความปกติและในคอมโพเนนต์เว็บสำหรับการป้อนข้อมูล Nord

ข้อดี (หรือข้อเสีย ขึ้นอยู่กับมุมมองของคุณ) อย่างหนึ่งของ Shadow DOM คือการรวมสไตล์ หากคุณเขียน CSS ภายในคอมโพเนนต์เว็บ สไตล์เหล่านั้นจะไม่รั่วไหลและส่งผลต่อหน้าหลักหรือองค์ประกอบอื่นๆ โดยจะอยู่ภายในคอมโพเนนต์อย่างสมบูรณ์ นอกจากนี้ CSS ที่เขียนขึ้นสำหรับหน้าหลักหรือคอมโพเนนต์เว็บหลักจะรั่วไหลไปยังคอมโพเนนต์เว็บของคุณไม่ได้

การรวมสไตล์นี้เป็นประโยชน์ในไลบรารีคอมโพเนนต์ของเรา ซึ่งช่วยรับประกันได้มากขึ้นว่าเมื่อมีคนใช้คอมโพเนนต์ของเรา คอมโพเนนต์นั้นจะมีลักษณะตามที่เราต้องการ ไม่ว่าสไตล์ที่ใช้กับหน้าหลักจะเป็นอย่างไรก็ตาม และเพื่อให้แน่ใจยิ่งขึ้น เราจึงเพิ่ม all: unset; ลงในรูทหรือ "โฮสต์" ของคอมโพเนนต์เว็บทั้งหมด


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
โค้ด Boilerplate ของคอมโพเนนต์บางส่วนที่ใช้กับ Shadow Root หรือตัวเลือกโฮสต์

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

ตรงนี้เองที่คุณสมบัติที่กำหนดเองของ CSS จะเข้ามามีบทบาท

คุณสมบัติที่กำหนดเองของ CSS

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


:root {
  --n-color-accent: rgb(53, 89, 199);
  /* ... */
}

.n-color-accent-text {
  color: var(--n-color-accent);
}
ตัวอย่างจากเฟรมเวิร์ก CSS ของโทเค็นการออกแบบเป็นคุณสมบัติที่กำหนดเองและมีการใช้ในคลาสตัวช่วย

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

ความสามารถในการรับค่าคุณสมบัติที่กำหนดเองด้วยการใช้ฟังก์ชัน var() เป็นวิธีที่เราเจาะผ่าน Shadow DOM ของคอมโพเนนต์เว็บและช่วยให้นักพัฒนาซอฟต์แวร์ควบคุมได้ละเอียดยิ่งขึ้นเมื่อจัดสไตล์คอมโพเนนต์

คุณสมบัติที่กำหนดเองในคอมโพเนนต์เว็บ Nord

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


:root {
  --n-space-m: 16px;
  --n-space-l: 24px;
  /* ... */
  --n-color-background: rgb(255, 255, 255);
  --n-color-border: rgb(216, 222, 228);
  /* ... */
}
คุณสมบัติที่กำหนดเองของ CSS ที่กำหนดไว้ในตัวเลือกระดับรูท

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


:host {
  --n-tab-group-padding: 0;
  --n-tab-list-background: var(--n-color-background);
  --n-tab-list-border: inset 0 -1px 0 0 var(--n-color-border);
  /* ... */
}

.n-tab-group-list {
  box-shadow: var(--n-tab-list-border);
  background-color: var(--n-tab-list-background);
  gap: var(--n-space-s);
  /* ... */
}
คุณสมบัติที่กำหนดเองซึ่งกำหนดไว้ใน Shadow Root ของคอมโพเนนต์ แล้วนำไปใช้ในสไตล์คอมโพเนนต์ นอกจากนี้ยังมีการใช้คุณสมบัติที่กำหนดเองจากรายการโทเค็นการออกแบบด้วย

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


.n-tab-group-list::before {
  /* ... */
  padding-inline-start: var(--n-tab-group-padding);
}

.n-tab-group-list::after {
  /* ... */
  padding-inline-end: var(--n-tab-group-padding);
}
คุณสมบัติที่กำหนดเองตามบริบทของระยะห่างจากขอบของกลุ่มแท็บที่ใช้ในหลายตำแหน่งภายในโค้ดคอมโพเนนต์

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


:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
รูปแบบของคอมโพเนนต์แท็บที่มีการเปลี่ยนแปลงระยะห่างจากขอบโดยใช้การอัปเดตคุณสมบัติที่กำหนดเองรายการเดียวแทนการอัปเดตหลายรายการ

แต่ประโยชน์ที่สำคัญที่สุดคือเมื่อเรากำหนดคุณสมบัติที่กำหนดเองตามบริบทเหล่านี้ในคอมโพเนนต์ เราจะสร้าง CSS API ที่กำหนดเองสำหรับคอมโพเนนต์แต่ละรายการ ซึ่งผู้ใช้คอมโพเนนต์นั้นจะเข้าถึงได้


<nord-tab-group label="Title">
  <!-- ... -->
</nord-tab-group>

<style>
  nord-tab-group {
    --n-tab-group-padding: var(--n-space-xl);
  }
</style>
การใช้คอมโพเนนต์กลุ่มแท็บในหน้าเว็บและอัปเดตคุณสมบัติที่กำหนดเองของระยะห่างจากขอบให้มีขนาดใหญ่ขึ้น

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

เราพบว่าแนวทางนี้มีประสิทธิภาพอย่างมาก ไม่เพียงแต่สำหรับเราในฐานะผู้สร้างคอมโพเนนต์ระบบการออกแบบเท่านั้น แต่ยังรวมถึงทีมพัฒนาของเราเมื่อใช้คอมโพเนนต์เหล่านี้ในผลิตภัณฑ์ด้วย

การใช้คุณสมบัติที่กำหนดเองเพิ่มเติม

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

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


<nord-divider></nord-divider>

<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>

<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }

  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
  }
  
  section nord-divider {
    --n-divider-color: var(--n-color-status-success);
  }
</style>
คอมโพเนนต์ตัวแบ่ง 2 รายการที่ต้องใช้สีที่แตกต่างกัน 2 สี รายการหนึ่งซ้อนอยู่ภายในส่วนที่เราสามารถใช้สำหรับตัวเลือกที่เฉพาะเจาะจงมากขึ้น แต่เราต้องกำหนดเป้าหมายตัวแบ่งโดยเฉพาะ

เหตุผลที่คุณต้องตั้งค่าคุณสมบัติที่กำหนดเองในคอมโพเนนต์โดยตรงก็คือเรากำลังกำหนดคุณสมบัติเหล่านั้นในองค์ประกอบเดียวกันโดยใช้ตัวเลือกโฮสต์คอมโพเนนต์ โทเค็นการออกแบบส่วนกลางที่เราใช้โดยตรงในคอมโพเนนต์จะส่งผ่านโดยตรง โดยไม่ได้รับผลกระทบจากปัญหานี้ และยังสามารถสกัดกั้นในองค์ประกอบหลักได้ด้วย เราจะใช้ประโยชน์จากทั้ง 2 บทบาทได้อย่างไร

พร็อพเพอร์ตี้ที่กำหนดเองแบบส่วนตัวและแบบสาธารณะ

พร็อพเพอร์ตี้ที่กำหนดเองแบบส่วนตัวเป็นสิ่งที่ Lea Verou รวบรวมไว้ ซึ่งเป็นพร็อพเพอร์ตี้ที่กำหนดเองแบบ "ส่วนตัว" ตามบริบทในคอมโพเนนต์เอง แต่ตั้งค่าเป็นพร็อพเพอร์ตี้ที่กำหนดเองแบบ "สาธารณะ" ที่มีโหมดสำรอง



:host {
  --_n-divider-color: var(--n-divider-color, var(--n-color-border));
  --_n-divider-size: var(--n-divider-size, 1px);
}

.n-divider {
  border-block-start: solid var(--_n-divider-size) var(--_n-divider-color);
  /* ... */
}
CSS ของคอมโพเนนต์เว็บตัวแบ่งที่มีการปรับคุณสมบัติที่กำหนดเองตามบริบทเพื่อให้ CSS ภายในอาศัยคุณสมบัติที่กำหนดเองแบบส่วนตัว ซึ่งได้รับการตั้งค่าเป็นคุณสมบัติที่กำหนดเองแบบสาธารณะที่มีการสำรอง

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


<nord-divider></nord-divider>

<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>

<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }

  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
    --n-divider-color: var(--n-color-status-success);
  }
</style>
ตัวแบ่ง 2 ตัวอีกครั้ง แต่คราวนี้ตัวแบ่งสามารถเปลี่ยนสีได้โดยการเพิ่มคุณสมบัติที่กำหนดเองตามบริบทของตัวแบ่งลงในตัวเลือกส่วน ตัวแบ่งจะรับค่าดังกล่าวมา ซึ่งจะทำให้โค้ดมีความยืดหยุ่นและเป็นระเบียบมากขึ้น

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

เราหวังว่าคุณจะได้รับประโยชน์จากข้อมูลเชิงลึกเกี่ยวกับวิธีที่เราใช้คอมโพเนนต์เว็บกับคุณสมบัติที่กำหนดเองของ CSS โปรดบอกให้เราทราบว่าคุณคิดอย่างไร และหากคุณตัดสินใจที่จะใช้วิธีใดวิธีหนึ่งเหล่านี้ในงานของคุณเอง คุณสามารถติดต่อฉันได้ทาง Twitter @DavidDarnes คุณยังสามารถติดตาม Nordhealth ได้ที่ @NordhealthHQ บน Twitter รวมถึงทีมที่เหลือของฉันซึ่งทำงานอย่างหนักเพื่อนำระบบการออกแบบนี้มารวมกันและใช้ฟีเจอร์ที่กล่าวถึงในเอกสารนี้ ได้แก่ @Viljamis, @WickyNilliams และ @eric_habich