在前端开发中,常常须要动态添加一些元素到页面上。那么如何经过一些技巧,优化动态建立页面元素的方式,使得代码更加优雅,而且更易于维护呢?接下来咱们经过研究一些实例,一步步地找出最优方案。html
这篇文章尽可能写得思路清晰且通俗易懂,由浅入深为刚入门前端的新手们带来一些思路和启发。前端
老手们也能够顺着看下去,当作复习一次。亦或者直接跳到后半部分,去看稍微深刻一点的模板数据替换示例,一块儿交流交流哦。jquery
因为DOM和HTML会存在必定的歧义,因此为了区别开来,文章中这两个术语的意思分别是:git
DOM :专指文档对象,是在JS上以对象的形式存在的。github
HTML:专指HTML文本,是一连串字符的集合。ajax
话很少说,咱们先来思考一下最基本的问题,如何用JS动态添加元素到页面中去呢?浏览器
假设在点击“添加一个乘客”按钮的时候,须要JS动态建立出一个新的输入框来填写姓名:app
<h2>乘客列表:</h2> <form class="form"> <div class="form-group"> 乘客姓名:<input type="text" class="form-control" name="member[]"> </div> <button class="create-passenger" type="button">添加一个乘客</button> <button type="submit">保存</button> </form>
从上面能够看出,要实现这个功能,咱们须要处理的HTML片断是:框架
<div class="form-group"> 乘客姓名:<input type="text" class="form-control" name="member[]"> </div>
那么咱们先来看看传统的作法是这样子的:dom
先直接手动复制粘贴HTML拼接成JS字符串,而后再插入到表单中。
$('.create-passenger').on('click', function() { // 先直接手动复制粘贴HTML拼接成JS字符串 var html = '<div class="form-group">' + ' 乘客姓名:<input type="text" class="form-control" name="member[]">' + '</div>'; // 而后再插入到表单中 $('.form').append(html); });
这是种偷懒的实现方式,在部分中小型网站、教科书上,最多见到它的身影。
在开发时的时候,某些状况下使用这种方案,的确可能会比较快速,直接复制粘贴HTML拼成JS字符串就能够了。
但知足这样的条件必须是:
要拼接的HTML字符串很短;
页面结构已经很稳定,能保证之后不会须要做出修改;
页面HMTL和JS的代码量都很少,或者已经直接把JS写在页面上了,因此即便设计不合理也能比较容易查看和维护。
没有作好HTML和JS的分离,脚本强烈耦合了HTML,不妥不妥。
要是后期页面上的HTML有了改动,必须同时记得去找出相关的脚本文件,在JS代码中搜索并修改里面写死的HTML字符串才行。
换个角度再想想,若是插入的HTML很复杂,有几百行的话。要在JS脚本中手动拼接庞大的字符串,是件很是的麻烦事情,还十分容易出差错。
模板分离原则:将定义模板的那一部分,与JS的代码逻辑分离开来,让代码更加优雅且利于维护。
经过分析页面咱们能够知道,表单初始的时候是至少会存在一个乘客输入项的。
因此咱们能够复制表单上第一个乘客的DOM来做为模板:
$('.create-passenger').on('click', function() { // 复制第一个乘客的DOM做为模板 var template = $('.form .form-group:first-child').clone(); // 将DOM模板插入到表单中,造成新的一行 $('.form').append(template); });
实例中用了jquery的clone()
方法,能够复制整个jquery包装过的DOM对象(不包括对象绑定的事件,若是要连事件也一块儿复制的话,能够加个参数clone(true)
哦)。
有时候复制过来的DOM对象有可能不是最原始的状态,因此记得要初始化一下。例若有像input
这样的输入项,要记得把value
的值先初始化哦template.find('input').val('')
。
若是页面原本就没有相关的DOM,这时候能够手动新建一个隐藏的<div>,而后在里面定义咱们的模板:
<div id="passenger-template" style="display: none;"> <div class="form-group"> 乘客姓名:<input type="text" class="form-control" name="member[]"> </div> </div>
接下来用JS去取这个元素的内容做为模板:
var template = $('#passenger-template > div').clone();
用一个标签来包裹模板的理由,
一是取模板的时候能够很方便,直接clone()
或者html()
就能够了;
二是为了更好地分类和规范。例如定义模板时,要求你们都用同一种标签和CSS类:<div class="template">
固然不必定去用<div>,也可使用别的标签,或者自定义一个<template>
标签专门放模板,不过这时候要注意IE8下面自定义标签会有些许问题哦。
若是想更加清晰地区分开模板和正常的页面元素的话,还能够用<script>标签:
<script id="passenger-template" type="text/html"> <!- 注意标签内的type属性 -!> <div class="form-group"> 乘客姓名:<input type="text" class="form-control" name="member[]"> </div> </script>
<script>
标签内的type="text/html"
,它能告诉浏览器这个标签里面的内容不是JS脚本,能够直接忽略不用去解析。这样浏览器就不会报错了。
还有一点是这时候就不能直接使用clone()
了哦,由于<script>
标签里面的内容不是DOM对象,而是字符串类型的HTML片断。
因此记得要经过html()
方法获取咱们字符串形式的模板:
var template = $('#passenger-template').html(); // 获取的是字符串,不是DOM对象
JS和HTML作到了彻底的解耦,十分利于后期的修改和维护。
脚本上没有了多余的代码,咱们在开发的时候,只需关注业务逻辑了。
不用再去手动复制粘贴HTML来拼接JS字符串,写HTML比拼JS字符串要来的轻松,并且不容易出错。因此是一个明智之举,也算是有技巧地偷懒。
若是复制页面现有的DOM做为模板的话,能够彻底脱离后期须要维护模板的限制。之后即便页面有修改了,JS这个“添加一个乘客”的功能,也同样能正常工做,适应性极强。
咱们继续之前面的主题展开研究。不过此次的重点,是探究几种添加数据的实现方式,一步步找出最佳的方案。
新增的需求是这样的:若是想把特定的乘客信息,添加到新增的页面元素中,那样该怎么办呢?
$.ajax({ url: '/getPassengers', // 后台获取全部乘客的信息 success: function(passengers) { var html = ''; // 储存要插入到页面的HTML片断 var len = passengers.length; for (var i = 0; i < len; i++) { // 获取带有该乘客信息的HTML片断 html += get_passenger_html(passengers[i]); // 后面将详细讲这个函数的实现方式 } $('.form').append(html); } });
下面将集中讲一下,改如何生成带有指定乘客信息的HTML片断,也就是这个get_passenger_html()
的内部实现方式。
function get_passenger_html(passenger) { var html = ''; html += '<div class="form-group">'; html += ' 乘客姓名:<input type="text" class="form-control" name="member[]" '; html += ' value="' + passenger.name + '">'; // 将乘客姓名拼接到HTML字符串中 html += '</div>'; return html; }
这个也是最传统的数据跟HTML字符串拼接的的方式,没有用到模板,脚本上会存在冗长的HTML字符串拼接代码。
这种作法没办法使用以前提到的模板技术,后期维护难是一个重大问题。
数据多一点或者html复杂一点,手动拼接字符串耗费精力、容易出错的弊端就会愈来愈显现。
能不能先定义好模板,而后再作数据插入的操做呢?这样就能够将模板定义和数据操做分离开来了,跟JS的字符串拼接Say good bye啦。
下面展现两种分离数据操做和模板定义的实现方式:
若是要插入的数据恰好是在某个标签或属性内,可使用操做DOM对象的方式来插入数据:
function get_passenger_html(passenger) { var html = $('#passenger-template').html(); // 获取HTML字符串模板 var dom = $(html); // 先即将HTML字符转成DOM对象 dom.find('.name').html(passenger.name); // 找到存放乘客姓名的DOM节点并插入数据 dom.find('.tel').html(passenger.tel); // 找到存放乘客电话的DOM节点并插入数据 // 把处理完毕的DOM转回HTML字符串并返回 return dom.prop("outerHTML"); }
若是模板不是clone()
得来的,要先用$(html)
将HTML字符串转成DOM对象,而后才能用find()
去找到对应的DOM节点来操做哦。
html()
方法只能获取子元素的HTML字符串,要获取包括本身的HTML字符串的话,要去读取outerHTML
属性,这是个DOM对象原生的属性,因此要用prop()
才能获取获得哦。
第一步先安照前面讲到的模板分离原则,定义了一个模板。在定义这个模板的时候,顺带添加一些带有特殊含义的占位符:{name}
和{tel}
。
<script id="passenger-template" type="text/html"> <ul class="passenger-list"> <li> 乘客姓名: <span class="name">{name}</span> </li> <li> 乘客电话: <span class="tel">{tel}</span> </li> </ul> </div>
第二步就是利用String.replace()
逐个替换掉这些自定义的占位符:
function get_passenger_html(passenger) { var html = $('#passenger-template').html(); // 获取HTML字符串模板 // 用乘客姓名替换掉咱们自定义的占位符 html = html.replace(passenger.name, '{name}'); // 替换姓名占位符 html = html.replace(passenger.tel, '{tel}'); // 替换电话占位符 return html; }
占位符的边界要特殊一点,例如用{
和}
,这样子就能避免在替换的时候,把其余有类似的字符被抹掉了。
介绍通用方案前,假设咱们获取到的模板是下面这一段字符串:
var template = '乘客姓名:{name},他的电话是:{tel},哈哈哈哈哈。';
想要替换掉占位符的JSON数据是:
var data = { name: '小神游', tel: 12312423423 };
按以前介绍的方法,要一个个写死:
template.replace('{name}', data.name); template.replace('{tel}', data.tel);
太麻烦了,原本已经模板上定了一次占位符。可是到了对应的JS上也要再手写一次,而且数据属性名也要手写,才可以保证能够替换成功。这样子代码写得一点都不优雅。
懒惰的咱们,从不喜欢重复劳动。这时候新建了个通用方法,能将特定模板和对应数据智能地匹配。
使用方法是这样的:
// 直接传入模板和数据便可 var html = template_replace(template, data); console.log(html); // 输出替换了数据的模板字符:乘客姓名:小神游,他的电话是:12312423423,哈哈哈哈哈。
哈哈哈,直接搞定!可以智能匹配模板和数据,并且还能复用在别的地方,之后能够偷懒了!
那么怎样写这个方法,把模板和数据智能地匹配呢?
以替换占位符{name}
为例,大致思路是:
找出模板占位符的左右边界,也就是{
和}
获取边界内的字符串,获得数据属性名,也就是name
把整个占位符用属性值替换掉,也就是{name}
替换成data['name']
// 将模板和数据结合起来 var template_replace = function(template, data) { // 内部方法:获取第一个匹配到的占位符位置,也就是"{"和"}"的索引 function get_next_placeholder_index_range() { ... } // 内部方法:将索引范围内的字符串,替换成data中具体的属性值 function set_replacement(indexRange) { ... } // 内部方法:替换全部占位符成为对应数据 function begin_replace() { ... } // 开始执行替换 begin_replace(); return template; // 返回替换完毕的模板字符串 };
这个内部方法get_next_placeholder_index_range()
,
用于获取第一个匹配到的占位符位置,也就是"{"和"}"的索引
{
的索引值var leftIndex = template.indexOf('{', 0);
}
的索引值var rightIndex = template.indexOf('}', leftIndex);
if (leftIndex === -1 || rightIndex === -1) { // 没有搜素到匹配的占位符 return false; } else { // 存在占位符,返回开始和结束的索引 return {left: leftIndex, right: rightIndex}; }
注意点:若是没有匹配的项,indexOf()
会返回-1
。
这个内部方法set_replacement()
,用于将索引范围内的字符串,替换成data中具体的属性值。
{
和}
var key = template.slice(indexRange.left + 1, indexRange.right);
注意点:slice()
的第一个参数表示从哪一个index开始截取(包括这个index的字符),因此若是要忽略{
的话,要从indexRange.left + 1
开始截取。
注意点:slice()
的第二个参数表示获取这个index值以前的字符串,因此恰好能够直接写indexRange.right
来忽略}
了。
{
和}
template = template.replace('{' + key + '}', data[key]);
注意点:示例没有作二维三维数据的转换,有须要的话能够扩展下代码:
var key = template.slice(indexRange.left + 1, indexRange.right); var keys = key.split('.'); // 根据点语法获取各级的属性名 var value = ''; // 属性值 switch (keys.length) { case 1: // 一维,如{name} value = data[keys[0]]; break; case 2: // 二维,如{name.firstName} value = data[keys[0]][keys[1]]; break; case 3: // 三维,如{name.firstName.firstWord} value = data[keys[0]][keys[1]][keys[2]]; break; default:; } template = template.replace('{' + key + '}', value);
不过扩展时要注意适度的权衡。当咱们扩展的代码愈来愈多的时候,就证实这个自定义的函数已经开始知足不了需求了,这时候建议转向使用第三方解决方案,后面会有介绍一个最佳的模板框架。
注意点:这个简单示例没有作容错机制,目的是展现数据替换的方法。因此前提是假设模板的占位符都已经和数据是对应的哦。
begin_replace(); // 继续递归替换
利用begin_replace
方法,检查模板中还有没有下一个占位符,若是存在下一个占位符的话,begin_replace
会继续递归调用get_replacement
来替换下一个,这两个函数的互相调用会一直轮回,直到模板全部占位符替换结束为止。
这个方法begin_replace()
将会调用前面的定义两个内部函数,目的是为了统筹递归替换数据的操做。
var indexRange = get_next_placeholder_index_range();
若是有占位符,能够开始进行替换了:
if (indexRange) { // set_replacement(indexRange); }
var template = '乘客姓名:{name},他的电话是:{tel},哈哈哈哈哈。'; var data = { name: '小神游', tel: 12312423423 }; // 直接传入模板和数据便可 var html = template_replace(template, data); console.log(html); // 乘客姓名:小神游,他的电话是:12312423423,哈哈哈哈哈。
var template_replace = function(template, data) { function get_next_placeholder_index_range() { var leftIndex = template.indexOf('{', 0); var rightIndex = template.indexOf('}', leftIndex); if (leftIndex === -1 || rightIndex === -1) { return false; } else { return {left: leftIndex, right: rightIndex}; } } function set_replacement(indexRange) { var key = template.slice(indexRange.left + 1, indexRange.right); template = template.replace('{' + key + '}', data[key]); begin_replace(); } function begin_replace() { var indexRange = get_next_placeholder_index_range(); if (indexRange) { set_replacement(indexRange); } } begin_replace(); return template; };
代码最后大概20行左右,今后就能够大大提升生产力,也让之后写的代码都更加优雅。
当你须要更Power的模板功能的时候,不必定要本身写,更理智的作法是使用成熟的模板引擎。
这里给出我多年一直在使用的、认为是最好的模板引擎:Artemplate.js
Github地址是:https://github.com/aui/artTem...
ArtTemplate是腾讯出的模板引擎,支持不少高级的模板操做,例如循环遍历、条件分支等等;而且它的解析速度是众多模板引擎中最快的。
哈哈,在咱们尝试写过简单的模板解析,理解了应该怎样善用模板和处理模板,让代码更加优雅且利于维护以后。用起第三方的模板引擎的时候会更加的感动:个人天,这东西怎么会这么方便。
经过对比“勉强的方案”,和介绍各类“更好的方案”,其实总结起来都离不开一句话:让代码更加优雅且利于维护。
记住这一点,在“够用”和“更好”之间,咱们总要逆流而上,敢于在实践中寻找“更好”。
其实不止是这篇文章中提到的一些小技巧,咱们在开发中还须要去处理各类类型的问题,对应的解决方案也确定不止一个,并且正在使用的方案也不必定是最优。因此要时刻有不将就的精神,多花点时间去优化。你能够的!
文章中若有错误但愿你们多多指正和多多包涵,我会当即改正的哈。还有要多多评论,多多交流,多多点赞哦~
谢谢你看到了最后,你们一块儿加油!