แท็กเทมเพลตใหม่ของ HTML

การกำหนดมาตรฐานเทมเพลตฝั่งไคลเอ็นต์

บทนำ

แนวคิดเทมเพลตไม่ใช่แนวคิดใหม่สำหรับการพัฒนาเว็บ อันที่จริงแล้ว ภาษา/เครื่องมือเทมเพลตฝั่งเซิร์ฟเวอร์อย่าง Django (Python), ERB/Haml (Ruby) และ Smarty (PHP) นั้นมีมานานแล้ว อย่างไรก็ตาม ในช่วง 2-3 ปีที่ผ่านมา เราได้เห็นเฟรมเวิร์ก MVC เกิดขึ้นมากมาย เครื่องมือเหล่านี้ทั้งหมดจะแตกต่างกันเล็กน้อย แต่ส่วนใหญ่จะใช้กลไกทั่วไปในการแสดงผลเลเยอร์การนำเสนอ (หรือที่เรียกว่ามุมมอง DA) ซึ่งก็คือเทมเพลต

ว่ากันตามจริง เทมเพลตยอดเยี่ยมมาก ถามไปรอบๆ ได้เลย แม้แต่คำจำกัดความก็ทำให้รู้สึกอบอุ่นและสบายใจ

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

ข้อกำหนดของเทมเพลต 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> จะช่วยให้เราทราบพร็อพเพอร์ตี้ที่สำคัญบางอย่าง

  1. เนื้อหาจะไม่มีการทํางานจนกว่าจะเปิดใช้งาน โดยพื้นฐานแล้ว มาร์กอัปของคุณคือ DOM ที่ซ่อนอยู่และจะไม่แสดงผล

  2. เนื้อหาภายในเทมเพลตจะไม่มีผลข้างเคียง สคริปต์ไม่ทํางาน รูปภาพไม่โหลด เสียงไม่เล่น…จนกว่าจะใช้เทมเพลต

  3. ระบบจะถือว่าเนื้อหาไม่ได้อยู่ในเอกสาร การใช้ document.getElementById() หรือ querySelector() ในหน้าหลักจะไม่แสดงโหนดย่อยของเทมเพลต

  4. เทมเพลตสามารถวางไว้ที่ใดก็ได้ภายใน <head>, <body> หรือ <frameset> และสามารถมีเนื้อหาประเภทใดก็ได้ที่อนุญาตในองค์ประกอบเหล่านั้น โปรดทราบว่า "ทุกที่" หมายความว่า <template> สามารถใช้ได้อย่างปลอดภัยในที่ที่โปรแกรมแยกวิเคราะห์ HTML ไม่อนุญาต ยกเว้นองค์ประกอบ content model นอกจากนี้ยังวางเป็นแท็กย่อยของ <table> หรือ <select> ได้ด้วย

<table>
  <tr>
    <template id="cells-to-repeat">
      <td>some content</td>
    </template>
  </tr>
</table>

การเปิดใช้งานเทมเพลต

หากต้องการใช้เทมเพลต คุณต้องเปิดใช้งานเทมเพลตนั้น มิเช่นนั้น เนื้อหาของวิดีโอจะแสดงผลไม่ได้ วิธีที่ง่ายที่สุดคือสร้างสำเนา .content ของ .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 โดยตรงโดยการต่อท้ายเนื้อหาเทมเพลตไปยังรูทเงา

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

ข้อควรระวัง

ต่อไปนี้คือข้อควรระวังบางประการที่เราพบเมื่อใช้ <template> จริง

  • หากคุณใช้ modpagespeed โปรดระวังข้อบกพร่องนี้ เทมเพลตที่กําหนด <style scoped> ในบรรทัดควรย้ายไปไว้ที่ส่วนหัวด้วยกฎการเขียน CSS ใหม่ของ PageSpeed
  • คุณไม่สามารถ "แสดงผลเทมเพลตล่วงหน้า" ได้ ซึ่งหมายความว่าคุณจะโหลดเนื้อหาล่วงหน้า ประมวลผล JS ดาวน์โหลด CSS เริ่มต้น และอื่นๆ ไม่ได้ ทั้งสำหรับเซิร์ฟเวอร์และไคลเอ็นต์ เทมเพลตจะแสดงผลก็ต่อเมื่อเผยแพร่เท่านั้น
  • โปรดระมัดระวังเทมเพลตที่ฝังอยู่ การดำเนินการดังกล่าวอาจไม่เป็นไปตามที่คุณคาดหวัง เช่น

    <template>
      <ul>
        <template>
          <li>Stuff</li>
        </template>
      </ul>
    </template>
    

    การเปิดใช้งานเทมเพลตด้านนอกจะไม่เปิดใช้งานเทมเพลตด้านใน กล่าวคือเทมเพลตที่ฝังอยู่ต้องเปิดใช้งานเทมเพลตย่อยด้วยตนเองด้วย

เส้นทางสู่มาตรฐาน

อย่าลืมจุดเริ่มต้นของเรา เส้นทางสู่เทมเพลต HTML ตามมาตรฐานนั้นยาวนาน ตลอดหลายปีที่ผ่านมา เราได้คิดค้นเทคนิคที่ค่อนข้างฉลาดในการสร้างเทมเพลตที่นํามาใช้ซ้ำได้ ด้านล่างนี้คือ 2 ตัวอย่างที่พบบ่อยซึ่งเราพบ เราจึงรวมข้อมูลดังกล่าวไว้ในบทความนี้เพื่อเปรียบเทียบ

วิธีที่ 1: DOM ที่อยู่นอกหน้าจอ

แนวทางหนึ่งที่ผู้คนใช้มาอย่างยาวนานคือการสร้าง DOM "นอกหน้าจอ" และซ่อน DOM นั้นไม่ให้แสดงโดยใช้แอตทริบิวต์ hidden หรือ display:none

<div id="mytemplate" hidden>
  <img src="logo.png">
  <div class="comment"></div>
</div>

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

  • การใช้ DOM - เบราว์เซอร์รู้จัก DOM อุปกรณ์นี้ทำได้ดี เราโคลนได้ง่ายๆ
  • ไม่มีการแสดงผล - การเพิ่ม hidden จะทำให้บล็อกไม่แสดง
  • ไม่ได้อยู่ในสถานะคงที่ - แม้ว่าเนื้อหาจะซ่อนอยู่ แต่ระบบจะยังคงส่งคำขอเครือข่ายสำหรับรูปภาพ
  • การจัดรูปแบบและธีมที่ยุ่งยาก - หน้าการฝังต้องใส่ #mytemplate ไว้หน้ากฎ CSS ทั้งหมดเพื่อกำหนดขอบเขตรูปแบบลงไปจนถึงเทมเพลต วิธีนี้ไม่เสถียรและไม่มีการรับประกันว่าเราจะไม่มีการทับซ้อนกันของชื่อในอนาคต เช่น เราจะแก้ปัญหาไม่ได้หากหน้าเว็บที่ฝังมีองค์ประกอบที่มีรหัสนั้นอยู่แล้ว

วิธีที่ 2: สคริปต์การโอเวอร์โหลด

อีกเทคนิคหนึ่งคือการโอเวอร์โหลด <script> และจัดการเนื้อหาของ <script> ในรูปแบบสตริง John Resig น่าจะเป็นคนแรกที่แสดงสิ่งนี้ในปี 2008 ด้วยยูทิลิตีเทมเพลตขนาดเล็ก ปัจจุบันมีเครื่องมืออื่นๆ อีกมากมาย รวมถึงเครื่องมือใหม่ๆ เช่น 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 อีกต่อไป การทําให้กระบวนการเขียนเว็บทั้งหมดมีความสมเหตุสมผลมากขึ้น บํารุงรักษาได้ง่ายขึ้น และมีคุณสมบัติครบถ้วนมากขึ้นเป็นสิ่งที่ดีเสมอ

แหล่งข้อมูลเพิ่มเติม