这是一篇读书笔记,对书中的描述进行精简,归纳,总结,
有时也会自己写一些例子,感想和扩展 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
| function loadDialog(name, oncomplete){ Y.one("#dlg-holder").load("/js/dialog/" + name, oncomplete); }
|
1 2 3 4
| 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
| <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 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="/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标签语句的地方,有四种值可用:
- beforeBegin: 插入到标签开始前
- afterBegin:插入到标签开始标记之后
- beforeEnd:插入到标签结束标记前
- afterEnd:插入到标签结束标记后
模板位置二:放在一个带有自定义type属性的<script>元素。
浏览器会默认将<script>元素中的内容识别为JavaScript代码,但你可以通过给type赋值为浏览器不识别的类型,来告诉浏览器这不是一段JavaScript脚本。
完整实例如下:
1 2 3 4 5 6
| <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
| <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转义,转义操作也是格式化的一部分。转义是为了增强模板的安全性,并确保简单的文本值不会破坏你的标签结构。比如,字符”&”会自动转义为 “&”;
关于上面例子的写法,最初我尝试用之前的写法:
1
| mylist.appendChild(div.firstChild);
|
可是无效,我又打印出div看,div 的 firstChild 确实是li,可是打印 div.firstChild 出来是“#text”它的值貌似是个回车。我发现打印div下有个 firstElementChild 和 firstChild 里面的东西一样的所以就用 firstElementChild 试了,还真可以,不过还是不懂为什么firstChild不行了,大家如果知道原因,求指点。