HTML5引入很多新特性,但是多数都十分简单易学,有基础的话很快就会可以掌握了。由于工作繁忙,很多东西就没细究了。
但有天我发现了HTML5原来已经引入了<template />
标签了,这个是以前HTML没有的元素,所以就必须恶补了。
网页模板系统简史
刚开始Web编程的那些年,网页模板系统对于我来说就是<jsp:include />
——它很好,只要有JavaEE服务器,它几乎可以用在任何场景:HTML、CSS、JS……当然它也很不好,应该说,所有基于JSP的Web系统都不够好,它们都很容易造成前端与后端高度耦合。耦合对于代码质量的影响有多大这里就不讨论了,但是这种耦合直接带来了一个开发过程的可用性问题——不启动JavaEE服务器的话、我就连简单的页面测试都无法进行……后来学会了PHP,它的include
同样有这样优缺点。
从那个时候起,我便尽量减少这种耦合,尽可能让前端代码都是静态的。
多了不用多长时间,随着我的CSS业务水平精进,知道它有自己也有@import
指令,那至少有了一个方法来组合CSS文件了。但是HTML、JS文件还是散落一地。
然后进入了后Web2.0时代、HTML5前夜,jQuery大行其道,纯静态前端代码已经可以做大量的工作,基于CSS选择器语法的API后来也被W3C纳入标准化。这时候想将HTML组件化,我的第一个答案是,在JS里面自己拼字符串:
<ul id="my-list"></ul>
<script>
$(function () {
$.get('...', function (resp) {
var list = resp.list,
buf = []; // 这里还使用了数组的push与join
// 来规避当年的JS字符串拼接性能低下的问题
for (var i = 0; i < list.length; ++i) {
var item = list[i];
buf.push('<li>', item.name, '</li>');
// 即便使用新语法也还是有XSS的问题
// buf.push(`<li>${item.name}</li>`);
}
$('#my-list').html(buf.join(''));
});
});
</script>
这种代码简单直接,但是可能会引入XSS漏洞。而且,如果我想单独考察一个<li />
组件的样式时,我都必须执行一遍上述代码。更麻烦的是,如果<li />
组件的内容很复杂的话,字符串拼接不单很容易出错,而且如果中途我想获取组件内某个子组件的数据时,实际上是没有办法的,因为一切都还没渲染。如果可以使用类似jQuery的选择器来操作子组件就好了。
然后开始发现有人利用隐藏的DOM元素来当作HTML模板,这未尝不是一个很好的实践:
<ul id="my-list"></ul>
<div id="my-list-item-template" hidden>
<li></li>
</div>
<script>
$(function () {
$.get('...', function (resp) {
var list = resp.list;
for (var i = 0; i < list.length; ++i) {
var item = list[i];
$('#my-list-item-template li').clone()
.html(item.name)
.appendTo('#my-list');
}
});
});
</script>
这样很好,因为可以使用选择器API来操作了,XSS出现的可能性就变低了。但也不是完全没有问题,因为就算模板不会影响界面,但是模板内的资源还是会被预先加载,想想如果模板里面有视频、图片之类的资源。这种别扭的HTML模板,实际上会违反HTML标签父子内容的约束,不少代码编辑器都会视其为语法错误。而且我就算可以通过除去hidden
属性来预览模板的样式,但是样式也是错的,因为<li />
的样式本就是要在<ul />
当中才可以正常展示的,在<div />
中的<li />
的样式是无法预料的。
而为防止资源加载,竟然有人通过“考古”找到<xmp />
标签,它在早期的HTML3中代表例子(example)标签,它里面的资源都不会加载,甚至连<style />
和<script />
都不会被执行:
<ul id="my-list"></ul>
<xmp id="my-list-item-template" style="display: hidden;">
<li>${name}</li>
</xmp>
<script>
$(function () {
$.get('...', function (resp) {
var list = resp.list,
buf = [],
itemTpl = $('#my-list-item-template').html();
for (var i = 0; i < list.length; ++i) {
var item = list[i];
buf.push(itemTpl.replace(/\$\{name\}/g, item.name));
}
$('#my-list').html(buf.join(''));
});
});
</script>
它的特性很好,但无奈后来HTML4时代已经废弃这个标签了,它被<pre />
标签取代了。而且它更糟糕的问题是它的子元素不能够通过选择器API来操作,只能继续拼接字符串、埋下XSS的隐患。也就是说,我们如果使用<pre />
或者<textarea />
来代替它都依然会有这个问题。
同时期,还有使用<script type="text/x-html-template">...</script>
的方案:
<ul id="my-list"></ul>
<script id="my-list-item-template" typle="text/x-html-template">
<li>${name}</li>
</script>
<script>
$(function () {
$.get('...', function (resp) {
var list = resp.list,
buf = [],
itemTpl = $('#my-list-item-template').text();
for (var i = 0; i < list.length; ++i) {
var item = list[i];
buf.push(itemTpl.replace(/\$\{name\}/g, item.name));
}
$('#my-list').html(buf.join(''));
});
});
</script>
但它的缺点与<xmp />
、<pre />
或者<textarea />
都是一样的,都是一个XSS温床。
设计清奇的BackBone.js与UnderScore.js为前端带来了更完善的框架化、组件化的MVC,虽然它们的模板工具也是基于字符串的,但是使用它们的模板工具能够避免XSS问题:
<ul id="my-list"></ul>
<script>
$(() => {
const itemTpl = _.template('<li><%- name %></li>'); // <%= name %>为不转义
$.get('...', (resp) => {
for (const item of resp.list)
$('#my-list').append(itemTpl({ name: item.name }));
});
});
</script>
它们也成功倒逼W3C带来大量API的改进。
而其中一项就是<template />
标签。现在虽然还很简陋,但起码能用了。
W3C的回应:<template />标签
<template />
标签默认就是不能显示的标签、即便强制设置display: block;
都无法显示,什么内容都可以放进去,而且在它的里面的资源不会加载,还可以使用DOM的API来操作它里面的元素,有效避免XSS:
<ul id="my-list"></ul>
<template id="my-list-item-template">
<li>name</li>
</template>
<script defer>
const xhr = Object.assign(new XMLHttpRequest(), {
onload(event) {
if (this.status >= 200 && this.status < 400) {
const resp = JSON.parse(this.response),
itemTpl = document.getElementById('my-list-item-template'),
myList = document.getElementById('my-list'),
li = itemTpl.content.querySelectorAll('li'); // itemTpl.content相当于另外一个document
// 所以document能用的API它都可以用
for (const item of resp.list) {
li[0].textContent = item.name; // 这样赋值可以不用担心XSS
// 实例化模板,导入这一刻会同时导入模板内的资源
const liClone = document.importNode(itemTpl.content, true); // true是默认值,即深度复制
myList.appendChild(liClone);
}
}
}
});
xhr.open('GET', '...', true); // true是默认值,即异步执行
xhr.send();
</script>
而由于<template />
标签允许任何内容在里面,原则上里面再嵌套<template />
标签也是可以的,但是它的行为不会如你所愿。因为实际上嵌套子模板会在父模板每次实例化的时候才出现在页面的DOM树中,也就是要到那个时机才可以自己手动实例化。所以这里就不建议嵌套使用模板了。
结合槽一起使用
但如果模板实例化时结构化的需求十分强烈,W3C借鉴了Vue.js带来了槽<slot />
标签和slot
属性:
<ul id="my-list"></ul>
<template id="my-list-item-template">
<li>
<img slot="icon" /> <!-- 每种标签都可以使用slot属性 -->
<slot name="name" /> <!-- 如果只是纯文本替换则可以使用slot标签 -->
</li>
</template>
<script defer>
const xhr = Object.assign(new XMLHttpRequest(), {
onload(event) {
if (this.status >= 200 && this.status < 400) {
const resp = JSON.parse(this.response),
itemTpl = document.getElementById('my-list-item-template'),
myList = document.getElementById('my-list'),
icon = itemTpl.content.querySelector('li img[slot="icon"]'),
name = itemTpl.content.querySelector('li slot[name="name"]');
for (const item of resp.list) {
icon.src = item.icon;
name.textContent = item.name;
const liClone = document.importNode(itemTpl.content);
myList.appendChild(liClone);
}
}
}
});
xhr.open('GET', '...');
xhr.send();
</script>
HTML模板与槽是W3C后来新定义的Web组件规范的成员,而其中还有一个影子DOM的规范(与Vue.js和React.js的虚拟DOM不是同一个概念),后续再继续学习。
打赏作者