Shadow DOM 101

เกริ่นนำ

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

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

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

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

คอมโพเนนต์ของเว็บประกอบด้วย 3 ส่วนดังนี้

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

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

สวัสดี Shadow World

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

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

<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 ของปุ่มคืออะไร ปุ่มนั้นจะไม่แสดงขึ้นว่า "แยกแยะ んすちは、影の世界!" แต่จะมีคำว่า "สวัสดี ทุกคน" เนื่องจากต้นไม้ย่อย 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: ซ่อนรายละเอียดงานนำเสนอ

ในเชิงความหมาย เราน่าจะสนใจเฉพาะเรื่องต่อไปนี้

  • เป็นป้ายชื่อ
  • ชื่อ "บอย"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

และ Shadowรูท ซึ่งใช้ตัวเลือก 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"> อีเมลของปวิชจะปรากฏกี่ครั้ง และเป็นสีอะไร

คำตอบคืออีเมลของบัญชาจะปรากฏขึ้นครั้งเดียวและเป็นสีเหลือง

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

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

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

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

<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 101 แล้ว! คุณสามารถทำสิ่งต่างๆ ได้มากขึ้นด้วย Shadow DOM เช่น คุณสามารถใช้เงาหลายตัวในโฮสต์เงาค่าเดียว หรือเงาที่ซ้อนกันสำหรับการห่อหุ้มหรือกำหนดสถาปนิกหน้าเว็บโดยใช้มุมมองที่ขับเคลื่อนด้วยโมเดล (MDV) และ Shadow DOM และคอมโพเนนต์เว็บเป็นมากกว่า Shadow DOM

โดยเราจะอธิบายไว้ในโพสต์ถัดไป