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

כדי ליצור תוכן לפי תבנית, מגדירים רכיב Markup ומעטפת אותו ברכיב <template>:

<template id="mytemplate">
    <img src="" alt="great image">
    <div class="comment"></div>
</template>

העמודים

כשעוטפים תוכן ב-<template>, אנחנו מקבלים כמה מאפיינים חשובים.

  1. התוכן שלו לא פעיל עד שמפעילים אותו. בעיקרון, ה-Markup הוא 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);

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

הדגמות

דוגמה: סקריפט לא פעיל

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

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

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