תבניות סטנדרטיות בצד הלקוח
מבוא
הקונספט של תבניות הוא לא חדש בפיתוח אתרים. למעשה, מנועים ושפות של תבניות בצד השרת, כמו Django (Python), ERB/Haml (Ruby) ו-Smarty (PHP), קיימים כבר הרבה זמן. אבל בשנתיים האחרונות, ראינו פריחה של מסגרות MVC. כולן קצת שונות, אבל רובן חולקות מכניקה משותפת לעיבוד שכבת ההצגה (התצוגה): תבניות.
בואו נודה בזה. התבניות הן כלי נהדר. קדימה, תשאלו חופשי. אפילו ההגדרה שלו גורמת לתחושה חמימה ונעימה:
"…לא צריך ליצור מחדש בכל פעם…" לא יודע מה איתכם, אבל אני אוהב להימנע מעבודה מיותרת. אז למה בפלטפורמת האינטרנט אין תמיכה מובנית במשהו שכל כך חשוב למפתחים?
התשובה היא מפרט התבניות של WhatWG HTML. הוא מגדיר רכיב <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>, אנחנו מקבלים כמה מאפיינים חשובים.
התוכן שלו לא פעיל עד שמפעילים אותו. בעצם, תגי העיצוב הם 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>
הפעלת תבנית
כדי להשתמש בתבנית, צריך להפעיל אותה. אחרת, התוכן שלו לא יוצג אף פעם.
הדרך הכי פשוטה לעשות את זה היא ליצור עותק עמוק של .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>מוטבע, עשויות לעבור ל-head באמצעות כללי השכתוב של 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> ולשנות את התוכן שלו כמחרוזת. ג'ון רסיג היה כנראה הראשון שהראה את זה ב-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.
לדעתי, כל מה שהופך את תהליך יצירת האתרים ליותר הגיוני, קל לתחזוקה וכולל יותר תכונות הוא דבר טוב.
מקורות מידע נוספים
- מפרט WhatWG
- מבוא לרכיבי אינטרנט
- <web>components</web> (סרטון) – מצגת מקיפה ונהדרת שיצרתי בעצמי.