การกำหนดมาตรฐานเทมเพลตฝั่งไคลเอ็นต์
บทนำ
แนวคิดเรื่องการใช้เทมเพลตไม่ใช่เรื่องใหม่สำหรับการพัฒนาเว็บ อันที่จริงแล้ว ภาษา/กลไกเทมเพลตฝั่งเซิร์ฟเวอร์ เช่น Django (Python), ERB/Haml (Ruby) และ Smarty (PHP) มีมานานแล้ว อย่างไรก็ตาม ในช่วง 2-3 ปีที่ผ่านมา เราได้เห็นเฟรมเวิร์ก MVC เกิดขึ้นมากมาย ซึ่งทั้งหมดนี้มีความแตกต่างกันเล็กน้อย แต่ส่วนใหญ่จะใช้กลไกทั่วไปในการแสดงผลเลเยอร์การนำเสนอ (หรือที่เรียกว่ามุมมอง) ซึ่งก็คือเทมเพลต
ว่ากันตามจริง เทมเพลตเป็นเครื่องมือที่ยอดเยี่ยม ถามมาได้เลย ไม่ต้องเกรงใจ แม้แต่คำจำกัดความของคำนี้ก็ทำให้คุณรู้สึกอบอุ่นและสบายใจ
"…ไม่จำเป็นต้องสร้างใหม่ทุกครั้ง…" ไม่รู้ว่าคุณคิดยังไง แต่ฉันชอบหลีกเลี่ยงงานที่ต้องทำเพิ่ม แล้วทำไมแพลตฟอร์มเว็บจึงไม่มีการรองรับแบบเนทีฟสำหรับสิ่งที่นักพัฒนาแอปให้ความสำคัญอย่างชัดเจน
ข้อกำหนดเทมเพลต HTML ของ WhatWG คือคำตอบ โดยจะกำหนดองค์ประกอบ <template> ใหม่ซึ่งอธิบายแนวทางตาม DOM มาตรฐานสำหรับการสร้างเทมเพลตฝั่งไคลเอ็นต์ เทมเพลตช่วยให้คุณประกาศส่วนย่อยของมาร์กอัปที่แยกวิเคราะห์เป็น HTML ได้ โดยจะไม่มีการใช้งานเมื่อการโหลดหน้าเว็บ แต่สามารถสร้างอินสแตนซ์ได้ในภายหลังที่รันไทม์ ขอยกคำพูดของ Rafael Weinstein มาดังนี้
ซึ่งเป็นที่สำหรับใส่ HTML จำนวนมากที่คุณไม่ต้องการให้เบราว์เซอร์เข้าไปยุ่งเลย…ไม่ว่าด้วยเหตุผลใดก็ตาม
Rafael Weinstein (ผู้เขียนข้อกำหนด)
การตรวจจับฟีเจอร์
หากต้องการตรวจหาฟีเจอร์ <template> ให้สร้างองค์ประกอบ DOM และตรวจสอบว่ามีพร็อพเพอร์ตี้ .content อยู่หรือไม่
function supportsTemplate() {
return 'content' in document.createElement('template');
}
if (supportsTemplate()) {
// Good to go!
} else {
// Use old templating techniques or libraries.
}
การประกาศเนื้อหาเทมเพลต
องค์ประกอบ <template> ของ HTML แสดงถึงเทมเพลตในมาร์กอัป ซึ่งมี "เนื้อหาเทมเพลต" โดยพื้นฐานแล้วคือกลุ่ม DOM ที่โคลนได้ซึ่งไม่มีการทำงาน
ให้คิดว่าเทมเพลตเป็นโครงสร้างที่คุณสามารถใช้ (และนำกลับมาใช้ซ้ำ) ได้ตลอดอายุการใช้งานของแอป
หากต้องการสร้างเนื้อหาที่มีเทมเพลต ให้ประกาศมาร์กอัปบางส่วนและรวมไว้ในองค์ประกอบ <template> ดังนี้
<template id="mytemplate">
<img src="" alt="great image">
<div class="comment"></div>
</template>
หลักการ
การรวมเนื้อหาไว้ใน <template> จะทำให้เราได้พร็อพเพอร์ตี้ที่สำคัญบางอย่าง
เนื้อหาจะไม่มีผลจนกว่าจะเปิดใช้งาน โดยพื้นฐานแล้ว มาร์กอัปของคุณคือ DOM ที่ซ่อนอยู่และไม่แสดงผล
เนื้อหาภายในเทมเพลตจะไม่มีผลข้างเคียง สคริปต์ไม่ทำงาน รูปภาพไม่โหลด เสียงไม่เล่น…จนกว่าจะมีการใช้เทมเพลต
ระบบจะถือว่าเนื้อหาไม่ได้อยู่ในเอกสาร การใช้
document.getElementById()หรือquerySelector()ในหน้าหลักจะไม่แสดงโหนดระดับย่อยของเทมเพลตคุณสามารถวางเทมเพลตไว้ที่ใดก็ได้ภายใน
<head>,<body>หรือ<frameset>และเทมเพลตสามารถมีเนื้อหาประเภทใดก็ได้ที่อนุญาตในองค์ประกอบเหล่านั้น โปรดทราบว่า "ทุกที่" หมายความว่าคุณสามารถใช้<template>ได้อย่างปลอดภัยในตำแหน่งที่โปรแกรมแยกวิเคราะห์ HTML ไม่อนุญาต…ทั้งหมดยกเว้นองค์ประกอบย่อยของโมเดลเนื้อหา นอกจากนี้ยังวางเป็นแท็กย่อยของ<table>หรือ<select>ได้ด้วย ดังนี้
<table>
<tr>
<template id="cells-to-repeat">
<td>some content</td>
</template>
</tr>
</table>
การเปิดใช้งานเทมเพลต
หากต้องการใช้เทมเพลต คุณต้องเปิดใช้งานเทมเพลตนั้น มิเช่นนั้น เนื้อหาจะไม่แสดงผล
วิธีที่ง่ายที่สุดในการดำเนินการนี้คือการสร้างสำเนาแบบ Deep Copy ของ .content โดยใช้ document.importNode() พร็อพเพอร์ตี้ .content เป็น DocumentFragment แบบอ่านอย่างเดียวที่มีเนื้อหาของเทมเพลต
var t = document.querySelector('#mytemplate');
// Populate the src at runtime.
t.content.querySelector('img').src = 'logo.png';
var clone = document.importNode(t.content, true);
document.body.appendChild(clone);
หลังจากประทับเทมเพลตแล้ว เนื้อหาจะ "เผยแพร่" ในตัวอย่างนี้ ระบบจะโคลนเนื้อหา ส่งคำขอรูปภาพ และแสดงผลมาร์กอัปสุดท้าย
การสาธิต
ตัวอย่าง: สคริปต์ที่ไม่มีการทำงาน
ตัวอย่างนี้แสดงให้เห็นถึงความเฉื่อยของเนื้อหาเทมเพลต <script> จะทำงานเมื่อกดปุ่มเท่านั้น โดยจะปั๊มเทมเพลตออกมา
<button onclick="useIt()">Use me</button>
<div id="container"></div>
<script>
function useIt() {
var content = document.querySelector('template').content;
// Update something in the template DOM.
var span = content.querySelector('span');
span.textContent = parseInt(span.textContent) + 1;
document.querySelector('#container').appendChild(
document.importNode(content, true)
);
}
</script>
<template>
<div>Template used: <span>0</span></div>
<script>alert('Thanks!')</script>
</template>
ตัวอย่าง: การสร้าง Shadow DOM จากเทมเพลต
คนส่วนใหญ่จะแนบ Shadow DOM เข้ากับโฮสต์โดยตั้งค่าสตริงของมาร์กอัปเป็น .innerHTML ดังนี้
<div id="host"></div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.innerHTML = '<span>Host node</span>';
</script>
ปัญหาของวิธีนี้คือยิ่ง Shadow DOM ซับซ้อนมากขึ้นเท่าใด
คุณก็ยิ่งต้องเชื่อมโยงสตริงมากขึ้นเท่านั้น เพราะมันไม่ยืดหยุ่น ทุกอย่างจะยุ่งเหยิง
อย่างรวดเร็ว และเด็กๆ ก็จะเริ่มร้องไห้ ซึ่งแนวทางนี้เป็นจุดเริ่มต้นของ XSS ด้วย <template> มาช่วยแล้ว
สิ่งที่สมเหตุสมผลกว่าคือการทำงานกับ DOM โดยตรงด้วยการเพิ่มเนื้อหาเทมเพลตลงใน Shadow Root
<template>
<style>
:host {
background: #f8f8f8;
padding: 10px;
transition: all 400ms ease-in-out;
box-sizing: border-box;
border-radius: 5px;
width: 450px;
max-width: 100%;
}
:host(:hover) {
background: #ccc;
}
div {
position: relative;
}
header {
padding: 5px;
border-bottom: 1px solid #aaa;
}
h3 {
margin: 0 !important;
}
textarea {
font-family: inherit;
width: 100%;
height: 100px;
box-sizing: border-box;
border: 1px solid #aaa;
}
footer {
position: absolute;
bottom: 10px;
right: 5px;
}
</style>
<div>
<header>
<h3>Add a Comment
</header>
<content select="p"></content>
<textarea></textarea>
<footer>
<button>Post</button>
</footer>
</div>
</template>
<div id="host">
<p>Instructions go here</p>
</div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.appendChild(document.querySelector('template').content);
</script>
Gotchas
ต่อไปนี้เป็นข้อควรระวังบางประการที่ฉันพบเมื่อใช้ <template> ในสภาพแวดล้อมจริง
- หากคุณใช้ modpagespeed โปรดระวังข้อบกพร่องนี้ เทมเพลตที่กำหนด
<style scoped>ในบรรทัดอาจย้ายไปที่ส่วนหัวด้วยกฎการเขียน CSS ใหม่ของ PageSpeed - คุณไม่สามารถ "แสดงผลล่วงหน้า" เทมเพลตได้ ซึ่งหมายความว่าคุณไม่สามารถโหลดเนื้อหาล่วงหน้า ประมวลผล JS ดาวน์โหลด CSS เริ่มต้น ฯลฯ ทั้งในฝั่งเซิร์ฟเวอร์และฝั่งไคลเอ็นต์ เทมเพลตจะแสดงผลเมื่อเผยแพร่เท่านั้น
โปรดระวังเทมเพลตที่ซ้อนกัน ซึ่งอาจทำงานไม่เหมือนกับที่คุณคาดหวัง เช่น
<template> <ul> <template> <li>Stuff</li> </template> </ul> </template>การเปิดใช้งานเทมเพลตด้านนอกจะไม่เปิดใช้งานเทมเพลตด้านใน กล่าวคือ เทมเพลตที่ฝังอยู่กำหนดให้ต้องเปิดใช้งานเทมเพลตย่อยด้วยตนเองด้วย
เส้นทางสู่มาตรฐาน
เราต้องไม่ลืมว่าเรามาจากไหน เส้นทางสู่เทมเพลต HTML ที่อิงตามมาตรฐาน เป็นเส้นทางที่ยาวไกล ตลอดหลายปีที่ผ่านมา เราได้คิดค้นเคล็ดลับที่ชาญฉลาดในการสร้างเทมเพลตที่นำกลับมาใช้ใหม่ได้ ด้านล่างนี้คือ 2 ปัญหาที่พบบ่อยซึ่งเราพบเจอ เราจึงรวมไว้ในบทความนี้เพื่อเปรียบเทียบ
วิธีที่ 1: DOM นอกหน้าจอ
วิธีหนึ่งที่ผู้คนใช้กันมานานคือการสร้าง DOM "นอกหน้าจอ" และซ่อนไม่ให้เห็นโดยใช้แอตทริบิวต์ hidden หรือ display:none
<div id="mytemplate" hidden>
<img src="logo.png">
<div class="comment"></div>
</div>
แม้ว่าเทคนิคนี้จะได้ผล แต่ก็มีข้อเสียหลายประการ สรุปเทคนิคนี้
- การใช้ DOM - เบราว์เซอร์รู้จัก DOM ซึ่งทำได้ดีมาก เราโคลนได้ง่ายๆ
- ไม่มีการแสดงผล - การเพิ่ม
hiddenจะป้องกันไม่ให้บล็อกแสดง - ไม่เฉื่อย - แม้ว่าเนื้อหาจะซ่อนอยู่ แต่ระบบก็ยังคงส่งคำขอเครือข่ายสำหรับรูปภาพ
- การจัดรูปแบบและการกำหนดธีมที่ยุ่งยาก - หน้าที่ฝังต้องใส่คำนำหน้ากฎ CSS ทั้งหมดด้วย
#mytemplateเพื่อกำหนดขอบเขตของรูปแบบให้เป็นเทมเพลต วิธีนี้ไม่เสถียรและไม่มีการรับประกันว่าเราจะไม่พบการชนกันของชื่อในอนาคต เช่น เราจะประสบปัญหาหากหน้าเว็บที่ฝังมีองค์ประกอบที่มีรหัสนั้นอยู่แล้ว
วิธีที่ 2: สคริปต์ Overloading
อีกเทคนิคหนึ่งคือการโอเวอร์โหลด <script> และจัดการเนื้อหาเป็นสตริง John Resig น่าจะเป็นคนแรกที่แสดงให้เห็นเรื่องนี้ในปี 2008 ด้วยยูทิลิตี Micro Templating ของเขา
ปัจจุบันมีเฟรมเวิร์กอื่นๆ อีกมากมาย รวมถึงเฟรมเวิร์กใหม่ๆ อย่าง handlebars.js
เช่น
<script id="mytemplate" type="text/x-handlebars-template">
<img src="logo.png">
<div class="comment"></div>
</script>
สรุปเทคนิคนี้
- ไม่มีการแสดงผล - เบราว์เซอร์จะไม่แสดงผลบล็อกนี้เนื่องจาก
<script>มีค่าเป็นdisplay:noneโดยค่าเริ่มต้น - Inert - เบราว์เซอร์จะไม่แยกวิเคราะห์เนื้อหาสคริปต์เป็น JS เนื่องจากมีการตั้งค่าประเภทเป็นอย่างอื่นที่ไม่ใช่ "text/javascript"
- ปัญหาด้านความปลอดภัย - ส่งเสริมให้ใช้
.innerHTMLการแยกวิเคราะห์สตริงรันไทม์ของข้อมูลที่ผู้ใช้ระบุอาจทำให้เกิดช่องโหว่ XSS ได้ง่าย
บทสรุป
ยังจำได้ไหมว่า jQuery ทำให้การทำงานกับ DOM เป็นเรื่องง่ายมาก ผลลัพธ์ที่ได้คือมีการเพิ่ม querySelector()/querySelectorAll() ลงในแพลตฟอร์ม เห็นได้ชัดว่าได้ประโยชน์กันทั้งคู่ใช่ไหม ไลบรารีหนึ่งทำให้การดึงข้อมูล DOM
ด้วยตัวเลือก CSS เป็นที่นิยม และมาตรฐานต่างๆ ก็นำไปใช้ในภายหลัง มันไม่ได้เป็นแบบนั้นเสมอไป แต่ฉันชอบเวลาที่มันเป็นแบบนั้น
เราคิดว่า <template> เป็นเคสที่คล้ายกัน ซึ่งทำให้วิธีการสร้างเทมเพลตฝั่งไคลเอ็นต์เป็นมาตรฐาน แต่ที่สำคัญกว่านั้นคือช่วยลดความจำเป็นในการแฮ็กในปี 2008
การทำให้กระบวนการเขียนเว็บทั้งหมดมีความสมเหตุสมผลมากขึ้น ดูแลรักษาได้ง่ายขึ้น และมีฟีเจอร์ครบครันมากขึ้นเป็นสิ่งที่ดีเสมอในมุมมองของฉัน
แหล่งข้อมูลเพิ่มเติม
- ข้อกำหนด WhatWG
- ข้อมูลเบื้องต้นเกี่ยวกับคอมโพเนนต์เว็บ
- <web>components</web> (วิดีโอ) - การนำเสนอที่ครอบคลุมอย่างน่าอัศจรรย์โดยตัวคุณเอง