תג תבנית חדש של HTML'

סטנדרטיזציה ליצירת תבניות בצד הלקוח

מבוא

המושג 'יצירת תבניות' אינו חדש בפיתוח אתרים. למעשה, שפות ומנועים בצד השרת, כמו Django (Python), ERB/Haml (Ruby) ו-Smarty (PHP), נמצאים כבר הרבה זמן. עם זאת, בשנים האחרונות ראינו עלייה אדירה של מסגרות MVC. כל אחת מהן שונה במקצת, אך רובן עושות שימוש בתבניות שונות.

בואו נודה בזה. התבניות נהדרות. קדימה, כדאי לשאול. גם ההגדרה שלו גורמת לך להרגיש חמים ונעים:

"...לא צריך ליצור מחדש כל פעם..." לא יודעים עליך, אבל אני אוהב להימנע מעבודה מיותרת. למה, אם כן, אין בפלטפורמת האינטרנט תמיכה מובנית למשהו שברור למפתחים?

התשובה לכך היא מפרט תבניות ה-HTML של WhatWG. הוא מגדיר רכיב <template> חדש שמתאר גישה סטנדרטית מבוססת DOM ליצירת תבניות בצד הלקוח. התבניות מאפשרות להצהיר על קטעי סימון שנותחו כ-HTML, כשהם לא בשימוש בזמן טעינת הדף, אבל ניתן ליצור אותם מאוחר יותר בזמן הריצה. לצטט את רפאל ויינשטיין:

דפים אלה מיועדים להציב כמות גדולה של HTML כך שאינך רוצה שהדפדפן יתעסק איתו כלל... מכל סיבה שהיא.

רפאל ויינשטיין (מחבר המפרט)

זיהוי תכונה

כדי לזהות תכונות של <template>, צריך ליצור את רכיב ה-DOM ולבדוק שהמאפיין .content קיים:

function supportsTemplate() {
    return 'content' in document.createElement('template');
}

if (supportsTemplate()) {
    // Good to go!
} else {
    // Use old templating techniques or libraries.
}

הצהרה על תוכן התבנית

רכיב ה-HTML <template> מייצג תבנית בתגי העיצוב. היא מכילה 'תוכן התבנית'. למעשה היא מקטע מקטעים של 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 אוסר... כל הצאצאים, מלבד מודל התוכן. אפשר גם להגדיר אותו כצאצא של <table> או של <select>:

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

הפעלת תבנית

כדי להשתמש בתבנית, צריך להפעיל אותה. אחרת, התוכן שלו אף פעם לא יוצג. הדרך הפשוטה ביותר לעשות זאת היא ליצור עותק עמוק של .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);

אחרי שחותמים על חותמת של תבנית, התוכן שלה עולה לאוויר. בדוגמה הספציפית הזו, התוכן משוכפל, הבקשה לתמונה נשלחת ותגי העיצוב הסופיים עוברים עיבוד.

הדגמות

דוגמה: סקריפט Inert

הדוגמה הזו ממחישה את עדות התוכן של התבנית. השדה <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>

דוגמה: יצירת DOM של צל מתבנית

רוב האנשים מצרפים את Shadow DOM למארח על ידי הגדרת מחרוזת תגי עיצוב ל-.innerHTML:

<div id="host"></div>
<script>
  var shadow = document.querySelector('#host').createShadowRoot();
  shadow.innerHTML = '<span>Host node</span>';
</script>

הבעיה בגישה הזו היא שככל שה-DOM של Shadow מורכב יותר, כך שרשורים רבים יותר במחרוזת. הוא לא גדל, הדברים מבולגנים מהר והתינוקות מתחילים לבכות. גישה זו היא גם האופן שבו 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 מבוססות-סטנדרטים הייתה ארוכה מאוד. במהלך השנים חשבנו על כמה טריקים חכמים ליצירת תבניות לשימוש חוזר. בהמשך נתקלתי בשתי בעיות נפוצות. אני כולל אותם במאמר זה לצורך השוואה.

שיטה 1: DOM מחוץ למסך

אחת הגישה שאנשים משתמשים בהן כבר הרבה זמן היא ליצור DOM 'מחוץ למסך' ולהסתיר אותו מהתצוגה באמצעות המאפיין hidden או display:none.

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

השיטה הזו פועלת, אבל יש לה כמה חסרונות. פירוט השיטה הזו:

  • באמצעות DOM - הדפדפן יודע DOM. הוא טוב בזה. אפשר לשכפל אותו בקלות.
  • שום דבר לא עבר עיבוד – ההוספה של hidden מונעת את הצגת החסימה.
  • Not inert – על אף שהתוכן שלנו מוסתר, עדיין נשלחת בקשה לרשת לגבי התמונה.
  • עיצוב ועיצוב לפי סגנון – דף הטמעה צריך להוסיף לכל כללי ה-CSS שלו את הקידומת #mytemplate כדי להחיל את הסגנונות עד לתבנית. הוא שבור, ואנחנו לא מבטיחים שלא ניתקל בהתנגשויות בין שמות בעתיד. לדוגמה, אנחנו משבשים אם דף ההטמעה כבר כולל רכיב עם המזהה הזה.

שיטה 2: טעינת יתר של הסקריפט

שיטה אחרת היא עומס יתר על <script> ומניפולציה של התוכן שלו כמחרוזת. ג'ון רזיג היה ככל הנראה הראשון שהציג את זה ב-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 dead לפשוטה? התוצאה הייתה הוספה של querySelector() מתוך querySelectorAll() לפלטפורמה. ניצחון ברור, נכון? ספרייה פופולרית לאחזור DOM. בשלב מאוחר יותר היא משתמשת בסלקטורים ב-CSS ובסטנדרטים. זה לא תמיד עובד ככה, אבל אני אוהבת את זה.

לדעתי, <template> הוא מקרה דומה. הוא סטנדרט לאופן שבו אנחנו מבצעים יצירת תבניות בצד הלקוח, אבל חשוב מכך, הוא מבטל את הצורך בפריצות משנת 2008. אני תמיד רוצה לשפר את הספר שלי בהפיכת התהליך כולו של כתיבת תוכן לשפוי, יותר לתחזוקה ומקיף יותר.

מקורות מידע נוספים