文章目录
  1. 1. 5.将HTML从JavaScript中抽离
    1. 1.1. 常见问题
    2. 1.2. 解决方法
      1. 1.2.1. 方式一:从服务器加载
      2. 1.2.2. 方式二:简单客户端模板
      3. 1.2.3. 方式三:复杂客户端模板

这是一篇读书笔记,对书中的描述进行精简,归纳,总结,
有时也会自己写一些例子,感想和扩展 O(∩_∩)O~


5.将HTML从JavaScript中抽离

正如我们需要将JavaScript从HTML中抽离一样,最好也将HTML从JavaScript中抽离。避免HTML的问题被埋在JavaScript代码中,以节省调试时间。

常见问题

在JavaScript中使用HTML的情形往往是给innerHTML属性赋值时,比如:

1
2
3
// 不好的写法
var div = document.getElementById("my-div");
div.innerHTML = "<h3>Error</h3><p>Invalid e-mail address.</p>"

评价:将HTML嵌入JavaScript代码中是非常不好的实践。
缺点:增加了跟踪文本和结构性问题的复杂度。追踪bug变得困难。
如果你希望修改文本或标签,你只希望去一个地方:可以控制你HTML代码的地方。如果你的标签出现在一处便可以很方便地更新它们。
相比于修改JavaScript代码,修改标签通常不会引发太多错误。当HTML和JavaScript混淆在一起时,问题将变得复杂。JavaScript字符串需要对引号做适当转义,这样则会导致它和模板语言的原生语法略有差异。

解决方法

因为多数Web应用本质上都是动态的,需要通过JavaScript向页面插入或修改标签。有很多方法可以以低耦合方式完成这项工作。

方式一:从服务器加载

将模板放置于远程服务器,使用XMLHttpRequest对象来获取外部标签。
例:点击一个链接,希望弹出一个新对话框,代码可能如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function loadDialog(name, oncomplete){
var xhr = new XMLHttpRequest();
xhr.open("get", "/js/dialog" + name, true);
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 && xhr.status == 200) {
var div = document.getElementById("dlg-holder");
div.innerHTML = xhr.responseText;
oncomplete();
}else{
// 处理错误
}
};
xhr.send(null);
}

评价:低耦合,对单页应用带来更多便捷。
优点:这里没有将HTML字符串嵌入在JavaScript里,而是向服务器发起请求获取字符串,这样可以让HTML代码以最合适的方式注入到页面中。
缺点:这种方法(从服务器获取模板)很容易造成XSS漏洞,需要服务器对模板文件做适当转义处理,比如<和>以及双引号等,当然前端也应当给出与之匹配的渲染规则,总之这种方法需要一揽子前后端的转码和解码策略来尽可能地封堵XSS漏洞。

JavaScript类库将这个操作做了封装,使得直接给DOM元素挂在内容变得非常方便。

1
2
3
4
// YUI
function loadDialog(name, oncomplete){
Y.one("#dlg-holder").load("/js/dialog/" + name, oncomplete);
}
1
2
3
4
// jQuery
function loadDialog(name, oncomplete){
$("#dlg-holder").load("/js/dialog/" + name, oncomplete);
}

适用时机:当你需要注入大段HTML标签到页面中时,使用远程调用的方式来加载标签是非常有帮助的。出于性能的原因,将大量没用的标签存放于内存或DOM中是很糟糕的做法。对于少量的标签段可以考虑采用客户端模板。

方式二:简单客户端模板

客户端模板是一些带“插槽”的标签片段,这些“插槽”会被JavaScript程序替换为数据以保证模板的完整可用。
比如:一段用来添加数据项的模板看起来就像下面这样。

1
<li><a href="%s">%s</a></li>

这段模板中包含%s占位符,这个位置的文本会被程序替换掉(这个格式和C语言中的sprintf()一模一样)。JavaScript程序会将这些占位符替换为真实数据,然后将结果注入DOM。

那么模板放在哪儿呢?
通常我们将模板定义在其他标签之间,直接存放于HTML页面里,这样可以被JavaScript读取,用以下两种方法之一可做到。

模板位置一:在HTML注释中包含模板文本。

注释是和元素及文本一样的DOM节点,因此可以通过JavaScript将其提取出来。
完整实例如下(书上的例子有些小问题,所以整理试了下,以下程序亲测可用):

1
2
3
4
5
6
<!-- 简单客户端模板(模板位置一) - HTML代码 -->
<ul id="mylist"><!--<li><a href="%s">%s</a></li>-->
<li><a href="/item/1">First item</a></li>
<li><a href="/item/2">Second item</a></li>
<li><a href="/item/3">Third item</a></li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 简单客户端模板(模板位置一) - JavaScript代码
// 用参数替换占位符
function sprintf(text){
var i=1,args=arguments;
return text.replace(/%s/g,function(){
return (i < args.length) ? args[i++] : "";
});
}
// 将填充模板之后的结果添加到适当位置
function addItem(url, text){
var mylist = document.getElementById("mylist"),
templateText = mylist.firstChild.nodeValue,
//templateText: <li><a href="%s">%s</a></li>
result = sprintf(templateText, url, text);
mylist.insertAdjacentHTML("beforeend", result);
}
// 用法
addItem("/item/4", "Fourth item");

笔记

  • HTML代码中的注释部分一定要和上一级的开始标签挨着(此处就是注释和<ul>挨着),如果是像这样写
1
2
3
4
5
6
7
<!-- 错误的写法 -->
<ul id="mylist">
<!--<li><a href="%s">%s</a></li>-->
<li><a href="/item/1">First item</a></li>
<li><a href="/item/2">Second item</a></li>
<li><a href="/item/3">Third item</a></li>
</ul>
1
2
var mylist = document.getElementById("mylist"),
templateText = mylist.firstChild.nodeValue;

将获取不到

1
<li><a href="%s">%s</a></li>
  • insertAdjacentHTML方法:在指定的地方插入html标签语句
    原型:insertAdajcentHTML(swhere,stext)
    参数:swhere: 指定插入html标签语句的地方,有四种值可用:
  1. beforeBegin: 插入到标签开始前
  2. afterBegin:插入到标签开始标记之后
  3. beforeEnd:插入到标签结束标记前
  4. afterEnd:插入到标签结束标记后

模板位置二:放在一个带有自定义type属性的<script>元素。

浏览器会默认将<script>元素中的内容识别为JavaScript代码,但你可以通过给type赋值为浏览器不识别的类型,来告诉浏览器这不是一段JavaScript脚本。

完整实例如下

1
2
3
4
5
6
<!-- 简单客户端模板(模板位置二) - HTML代码 -->
<ul id="mylist">
<li><a href="/item/1">First item</a></li>
<li><a href="/item/2">Second item</a></li>
<li><a href="/item/3">Third item</a></li>
</ul>
1
2
3
4
// 简单客户端模板(模板位置二) - 模板写在script元素中
<script type="text/x-my-template" id="list-item">
<li><a href="%s">%s</a></li>
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 简单客户端模板(模板位置二) - JavaScript代码
// 用参数替换占位符
function sprintf(text){
var i=1,args=arguments;
return text.replace(/%s/g,function(){
return (i < args.length) ? args[i++] : "";
});
}
// 将填充模板之后的结果添加到适当位置
function addItem(url, text){
var mylist = document.getElementById("mylist"),
script = document.getElementById("list-item"),
templateText = script.text,
result = sprintf(templateText, url, text),
div = document.createElement("div");
div.innerHTML = result.replace(/^\s*/, "");
mylist.appendChild(div.firstChild);
}
// 用法
addItem("/item/4", "Fourth item");

笔记

  • 函数中去掉了模板文本中的前导空格
1
result.replace(/^\s*/, "");

之所以会出现这个多余的前导空格,是因为模板文本总是在 <script>起始标签的下一行。如果将模板文本原样注入,则会在 <div>里创建一个文本结点,这个文本节点的内容是一个空格,而这个文本节点最终会代替 <li>被添加进列表之中。

方式三:复杂客户端模板

前两种方式模板格式都非常简单,无太多转义,如果想用一些更健壮的模板,可以考虑诸如Handlebars所提供的解决方案。

Handlebars是专为浏览器端JavaScript设计的完整的客户端模板系统。
在Handlebars的模板中,占位符使用双花括号来表示。

上一节中的模板Handlebars版本完整实例如下

1
2
3
4
5
6
<!-- 复杂客户端模板(Handlebars) - HTML代码 -->
<ul id="mylist">
<li><a href="/item/1">First item</a></li>
<li><a href="/item/2">Second item</a></li>
<li><a href="/item/3">Third item</a></li>
</ul>
1
2
3
4
// 复杂客户端模板(Handlebars) - 模板写在script元素中
<script type="text/x-handlebars-template" id="list-item">
<li><a href=""></a></li>
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 复杂客户端模板(Handlebars) - JavaScript代码
function addItem(url, text){
var mylist = document.getElementById("mylist"),
script = document.getElementById("list-item"),
templateText = script.text,
template = Handlebars.compile(templateText),
div = document.createElement("div"),
result;
/* 变量template包含了一个函数,当执行这个函数时则返回一个
格式化好的字符串,你需要做的仅仅是传入一个包含属性的对象,
参数会自动做HTML转义。*/
result = template({
text:text,
url:url
});
div.innerHTML = result;
mylist.appendChild(div.firstElementChild);
}
// 用法
addItem("/item/4", "Fourth item");

笔记

  • 要使用Handlebars首先要将Handlebars类库引入页面。这个类库会创建一个名为Handlebars的全局变量,用来将模板文本编译为一个函数。
    我是用的 bower 来安装 Handlebars 的:
1
bower install handlebars -g

(注:Bower 是 twitter 推出的一款包管理工具,基于nodejs的模块化思想,把功能分散到各个模块中,让模块和模块之间存在联系,通过 Bower 来管理模块间的这种联系。)
安装后记得引用进页面来:

1
<script src="/javascripts/handlebars.js"></script>
  • 在Handlebars模板中,占位符都记为一个名称,以便可以在JavaScript中设计其映射。Handlebars 建议将模板嵌入 HTML 页面中,并使用 type 属性为 “text/x-handlebars-template”的<script>标签来表示(如上例所示)

  • 变量template包含了一个函数,当执行这个函数时则返回一个格式化好的字符串,你需要做的仅仅是传入一个包含属性的对象。参数会自动做HTML转义,转义操作也是格式化的一部分。转义是为了增强模板的安全性,并确保简单的文本值不会破坏你的标签结构。比如,字符”&”会自动转义为 “&amp”;

  • 关于上面例子的写法,最初我尝试用之前的写法:

1
mylist.appendChild(div.firstChild);

可是无效,我又打印出div看,div 的 firstChild 确实是li,可是打印 div.firstChild 出来是“#text”它的值貌似是个回车。我发现打印div下有个 firstElementChild 和 firstChild 里面的东西一样的所以就用 firstElementChild 试了,还真可以,不过还是不懂为什么firstChild不行了,大家如果知道原因,求指点。

文章目录
  1. 1. 5.将HTML从JavaScript中抽离
    1. 1.1. 常见问题
    2. 1.2. 解决方法
      1. 1.2.1. 方式一:从服务器加载
      2. 1.2.2. 方式二:简单客户端模板
      3. 1.2.3. 方式三:复杂客户端模板