Shadow DOM 101

บทนำ

คอมโพเนนต์ของเว็บคือชุดมาตรฐานล้ำสมัยที่มีลักษณะดังนี้

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

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

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

Web Components ประกอบด้วย 3 ส่วน ได้แก่

  1. เทมเพลต
  2. Shadow DOM
  3. องค์ประกอบที่กำหนดเอง

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

สวัสดี Shadow World

เมื่อใช้ Shadow DOM องค์ประกอบจะได้รับโหนดประเภทใหม่ที่เชื่อมโยงกับองค์ประกอบ โหนดประเภทใหม่นี้เรียกว่ารูทเงา องค์ประกอบที่มีรูทเงาเชื่อมโยงอยู่เรียกว่าโฮสต์เงา ระบบจะไม่แสดงผลเนื้อหาของโฮสต์เงา แต่ระบบจะแสดงผลเนื้อหาของรูทเงาแทน

ตัวอย่างเช่น หากคุณมีมาร์กอัปเช่นนี้

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

จากนั้นแทนที่จะเป็น

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

ลักษณะของหน้าเว็บ

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

ไม่เพียงเท่านั้น หาก JavaScript ในหน้าเว็บถามว่า textContent ของปุ่มคืออะไร ระบบจะไม่แสดง "こんにちは、影の世界!" แต่แสดงเป็น "Hello, world!" เนื่องจากมีการรวม DOM ย่อยภายใต้รูทเงา

การแยกเนื้อหาออกจากงานนำเสนอ

ตอนนี้เราจะมาดูการใช้ Shadow DOM เพื่อแยกเนื้อหาออกจากการแสดงผล สมมติว่าเรามีป้ายชื่อนี้

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

นี่คือมาร์กอัป นี่คือสิ่งที่คุณเขียนในวันนี้ และไม่ใช้ Shadow DOM ในกรณีต่อไปนี้

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

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

เราจะช่วยคุณหลีกเลี่ยงปัญหาได้

ขั้นตอนที่ 1: ซ่อนรายละเอียดงานนำเสนอ

ในแง่ความหมาย เราอาจสนใจแค่ว่า

  • นั่นคือป้ายชื่อ
  • ชื่อ "Bob"

ก่อนอื่น เราจะเขียนมาร์กอัปที่ใกล้เคียงกับความหมายจริงที่เราต้องการ ดังนี้

<div id="nameTag">Bob</div>

จากนั้นใส่สไตล์และ div ทั้งหมดที่ใช้สำหรับการแสดงผลไว้ในองค์ประกอบ <template> ดังนี้

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

ณ จุดนี้ "Bob" จะเป็นสิ่งเดียวที่แสดงผล เนื่องจากเราได้ย้ายองค์ประกอบ DOM สำหรับการนำเสนอไปไว้ภายในองค์ประกอบ <template> องค์ประกอบดังกล่าวจึงไม่แสดงผล แต่เข้าถึงได้จาก JavaScript เราทําดังนี้เพื่อป้อนข้อมูลรูทเงา

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

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

<div id="nameTag">Bob</div>

ตัวอย่างนี้แสดงให้เห็นว่าเราซ่อนรายละเอียดการแสดงผลของแท็กชื่อจากเอกสารโดยใช้ Shadow DOM รายละเอียดการนำเสนอจะรวมอยู่ใน Shadow DOM

ขั้นตอนที่ 2: แยกเนื้อหาออกจากงานนำเสนอ

ตอนนี้แท็กชื่อจะซ่อนรายละเอียดของงานนำเสนอจากหน้าเว็บ แต่ไม่ได้แยกงานนำเสนอออกจากเนื้อหา เนื่องจากแม้ว่าเนื้อหา (ชื่อ "Bob") จะอยู่ในหน้าเว็บ แต่ชื่อที่แสดงผลคือชื่อที่เราคัดลอกลงในรูทเงา หากต้องการเปลี่ยนชื่อบนป้ายชื่อ เราต้องดำเนินการ 2 ที่และอาจทำให้ข้อมูลไม่ซิงค์กัน

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

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

หากเราเปลี่ยนมาร์กอัปใน Shadow DOM เป็นดังนี้

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

เมื่อแสดงผลแท็กชื่อ ระบบจะโปรเจ็กต์เนื้อหาของโฮสต์เงาไปยังจุดที่องค์ประกอบ <content> ปรากฏขึ้น

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

document.querySelector('#nameTag').textContent = 'Shellie';

เท่านี้ก็เรียบร้อย เบราว์เซอร์จะอัปเดตการแสดงผลของแท็กชื่อโดยอัตโนมัติ เนื่องจากเราแสดงผลเนื้อหาของแท็กชื่อด้วย <content>

<div id="ex2b">

ตอนนี้เราแยกเนื้อหาและงานนำเสนอได้แล้ว เนื้อหาอยู่ในเอกสาร ส่วนงานนำเสนออยู่ใน Shadow DOM เบราว์เซอร์จะซิงค์ข้อมูลเหล่านี้โดยอัตโนมัติเมื่อถึงเวลาแสดงผล

ขั้นตอนที่ 3: กําไร

การแยกเนื้อหาและการแสดงผลช่วยให้เราเขียนโค้ดที่จัดการเนื้อหาได้ง่ายขึ้น ในตัวอย่างนี้ แท็กชื่อจะต้องจัดการกับโครงสร้างง่ายๆ ที่มี <div> เพียงรายการเดียวแทนที่จะเป็นหลายรายการ

ตอนนี้หากเราเปลี่ยนการนำเสนอ ก็ไม่จำเป็นต้องเปลี่ยนโค้ดใดๆ

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

<div id="nameTag">Bob</div>

โค้ดการตั้งค่ารูทเงาจะยังคงเหมือนเดิม สิ่งที่จะเปลี่ยนแปลงในรูทเงามีดังนี้

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

นี่เป็นการพัฒนาที่ยิ่งใหญ่กว่าสถานการณ์บนเว็บในปัจจุบัน เนื่องจากโค้ดอัปเดตชื่ออาจขึ้นอยู่กับโครงสร้างของคอมโพเนนต์ที่เรียบง่ายและสอดคล้องกัน โค้ดอัปเดตชื่อไม่จำเป็นต้องทราบโครงสร้างที่ใช้สำหรับการแสดงผล เมื่อพิจารณาสิ่งที่แสดงผลแล้ว ชื่อจะปรากฏเป็นภาษาอังกฤษเป็นอันดับ 2 (หลัง "สวัสดี ฉันชื่อ”) แต่ขึ้นต้นเป็นภาษาญี่ปุ่น (ก่อน "と申します") ความแตกต่างนี้ไม่มีความหมายทางความหมายจากมุมมองของการอัปเดตชื่อที่แสดง ดังนั้นโค้ดการอัปเดตชื่อจึงไม่จำเป็นต้องทราบรายละเอียดดังกล่าว

เครดิตพิเศษ: การคาดการณ์ขั้นสูง

ในตัวอย่างข้างต้น องค์ประกอบ <content> จะเลือกเนื้อหาทั้งหมดจากโฮสต์เงา การใช้แอตทริบิวต์ select ช่วยให้คุณควบคุมสิ่งที่องค์ประกอบเนื้อหาจะแสดงได้ คุณใช้องค์ประกอบเนื้อหาหลายรายการได้เช่นกัน

ตัวอย่างเช่น หากคุณมีเอกสารที่มีข้อมูลต่อไปนี้

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

และรูทเงาซึ่งใช้ตัวเลือก CSS เพื่อเลือกเนื้อหาที่เฉพาะเจาะจง

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

องค์ประกอบ <div class="email"> ตรงกับทั้งองค์ประกอบ <content select="div"> และ <content select=".email"> อีเมลของ Bob ปรากฏขึ้นกี่ครั้งและในสีใด

คำตอบคือ อีเมลของ Bob ปรากฏขึ้น 1 ครั้งและเป็นสีเหลือง

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

ในตัวอย่างข้างต้น <div class="email"> ตรงกับทั้งตัวเลือก div และตัวเลือก .email แต่เนื่องจากองค์ประกอบเนื้อหาที่มีตัวเลือก div ปรากฏในเอกสารก่อน <div class="email"> จึงไปอยู่ในกลุ่มสีเหลือง และไม่มีองค์ประกอบใดไปอยู่ในกลุ่มสีน้ำเงิน (นี่อาจเป็นเหตุผลที่ท้องฟ้าเป็นสีฟ้า ถึงแม้ว่าคนจะชอบหาคนมาแบ่งความทุกข์ด้วยก็ตาม คุณก็อาจไม่รู้เหมือนกัน)

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

เช่น HTML มีเครื่องมือเลือกวันที่ที่ยอดเยี่ยม หากเขียน <input type="date"> คุณจะเห็นปฏิทินป๊อปอัปที่เรียบร้อย แต่จะเกิดอะไรขึ้นหากคุณต้องการให้ผู้ใช้เลือกช่วงวันที่สำหรับของหวานในการพักผ่อนบนเกาะ (คุณก็รู้… กับเปลญวนที่ทำจากเถาวัลย์แดง) คุณตั้งค่าเอกสารได้ดังนี้

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

แต่สร้าง Shadow DOM ที่ใช้ตารางเพื่อสร้างปฏิทินที่ดูทันสมัยซึ่งไฮไลต์ช่วงวันที่และอื่นๆ เมื่อผู้ใช้คลิกวันในปฏิทิน คอมโพเนนต์จะอัปเดตสถานะในอินพุต startDate และ endDate เมื่อผู้ใช้ส่งแบบฟอร์ม ระบบจะส่งค่าจากองค์ประกอบอินพุตเหล่านั้น

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

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

คุณผ่าน Shadow DOM 101

ข้อมูลเบื้องต้นเกี่ยวกับ Shadow DOM จบแล้ว คุณทําสิ่งต่างๆ ได้มากขึ้นด้วย Shadow DOM เช่น คุณสามารถใช้ Shadow หลายรายการในโฮสต์ Shadow รายการเดียว หรือใช้ Shadow ที่ฝังอยู่เพื่อรวม หรือออกแบบหน้าเว็บโดยใช้มุมมองที่ขับเคลื่อนโดยโมเดล (MDV) และ Shadow DOM และ Web คอมโพเนนต์ไม่ได้มีแค่ Shadow DOM

เราจะอธิบายในโพสต์ต่อๆ ไป