บทนำ
คอมโพเนนต์ของเว็บคือชุดมาตรฐานล้ำสมัยที่มีลักษณะดังนี้
- สร้างวิดเจ็ตได้
- …ซึ่งสามารถนำมาใช้ซ้ำได้อย่างน่าเชื่อถือ
- …และจะไม่ทำให้หน้าเว็บเสียหายหากคอมโพเนนต์เวอร์ชันถัดไปเปลี่ยนแปลงรายละเอียดการใช้งานภายใน
หมายความว่าคุณต้องตัดสินใจว่าควรใช้ HTML/JavaScript เมื่อใด และควรใช้ Web Components เมื่อใด ไม่เอาด้วยหรอก HTML และ JavaScript สร้างชิ้นงานภาพแบบอินเทอร์แอกทีฟได้ วิดเจ็ตคือองค์ประกอบภาพแบบอินเทอร์แอกทีฟ คุณควรใช้ประโยชน์จากทักษะ HTML และ JavaScript เมื่อพัฒนาวิดเจ็ต มาตรฐานคอมโพเนนต์เว็บออกแบบมาเพื่อช่วยคุณในเรื่องนั้น
แต่มีปัญหาพื้นฐานที่ทำให้วิดเจ็ตที่สร้างจาก HTML และ JavaScript ใช้งานยาก นั่นคือ ต้นไม้ DOM ภายในวิดเจ็ตไม่ได้แยกออกจากส่วนที่เหลือของหน้า การไม่มีการห่อหุ้มนี้หมายความว่าสไตลชีตเอกสารอาจใช้กับชิ้นส่วนภายในวิดเจ็ตโดยไม่ได้ตั้งใจ, JavaScript อาจแก้ไขชิ้นส่วนภายในวิดเจ็ตโดยไม่ได้ตั้งใจ, รหัสของคุณอาจทับซ้อนกับรหัสภายในวิดเจ็ต และอื่นๆ
Web Components ประกอบด้วย 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
เราจะอธิบายในโพสต์ต่อๆ ไป