9

【JS实用技巧】优化动态创建元素的方式,让代码更加优雅且利于维护

引言

在前端开发中,经常需要动态添加一些元素到页面上。那么如何通过一些技巧,优化动态创建页面元素的方式,使得代码更加优雅,并且更易于维护呢?接下来我们通过研究一些实例,一步步地找出最优方案。

这篇文章尽量写得思路清晰且通俗易懂,由浅入深为刚入门前端的新手们带来一些思路和启发。

老手们也可以顺着看下去,当做复习一次。亦或者直接跳到后半部分,去看稍微深入一点的模板数据替换示例,一起交流交流哦。

由于DOM和HTML会存在一定的歧义,所以为了区别开来,文章中这两个术语的意思分别是:

  • DOM :专指文档对象,是在JS上以对象的形式存在的。

  • HTML:专指HTML文本,是一连串字符的集合。

实例一:如何动态添加元素到页面中

话不多说,我们先来思考一下最基本的问题,如何用JS动态添加元素到页面中去呢?

假设在点击“添加一个乘客”按钮的时候,需要JS动态创建出一个新的输入框来填写姓名:

<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>

勉强的方案:手动复制粘贴HTML拼接成JS字符串

那么我们先来看看传统的做法是这样子的:

先直接手动复制粘贴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字符串就可以了。

但满足这样的条件必须是:

  1. 要拼接的HTML字符串很短;

  2. 页面结构已经很稳定,能保证以后不会需要作出修改;

  3. 页面HMTL和JS的代码量都不多,或者已经直接把JS写在页面上了,所以即使设计不合理也能比较容易查看和维护。

问题&思考

没有做好HTML和JS的分离,脚本强烈耦合了HTML,不妥不妥。

要是后期页面上的HTML有了改动,必须同时记得去找出相关的脚本文件,在JS代码中搜索并修改里面写死的HTML字符串才行。

换个角度再想一想,如果插入的HTML很复杂,有几百行的话。要在JS脚本中手动拼接庞大的字符串,是件非常的麻烦事情,还十分容易出差错。

更好的方案:模板分离原则

模板分离原则:将定义模板的那一部分,与JS的代码逻辑分离开来,让代码更加优雅且利于维护。

一、利用页面上现有的DOM元素作为模板

通过分析页面我们可以知道,表单初始的时候是至少会存在一个乘客输入项的。

所以我们可以复制表单上第一个乘客的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>标签:

<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对象

模板分离原则的好处

  1. JS和HTML做到了完全的解耦,十分利于后期的修改和维护。

  2. 脚本上没有了多余的代码,我们在开发的时候,只需关注业务逻辑了。

  3. 不用再去手动复制粘贴HTML来拼接JS字符串,写HTML比拼JS字符串要来的轻松,而且不容易出错。所以是一个明智之举,也算是有技巧地偷懒。

  4. 如果复制页面现有的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()的内部实现方式。

勉强的方案:手动将数据拼接到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对象来插入数据

如果要插入的数据刚好是在某个标签或属性内,可以使用操作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}为例,大体思路是:

  1. 找出模板占位符的左右边界,也就是{}

  2. 获取边界内的字符串,得到数据属性名,也就是name

  3. 把整个占位符用属性值替换掉,也就是{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()
用于获取第一个匹配到的占位符位置,也就是"{"和"}"的索引

从索引0开始,查找第一个匹配到的左边界{的索引值

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行左右,从此就可以大大提高生产力,也让以后写的代码都更加优雅。

第三方解决方案:ArtTemplate.js

当你需要更Power的模板功能的时候,不一定要自己写,更理智的做法是使用成熟的模板引擎。

这里给出我多年一直在使用的、认为是最好的模板引擎:Artemplate.js
Github地址是:https://github.com/aui/artTem...

ArtTemplate是腾讯出的模板引擎,支持很多高级的模板操作,例如循环遍历、条件分支等等;并且它的解析速度是众多模板引擎中最快的。

哈哈,在我们尝试写过简单的模板解析,理解了应该怎样善用模板和处理模板,让代码更加优雅且利于维护之后。用起第三方的模板引擎的时候会更加的感动:我的天,这东西怎么会这么方便。

结语

通过对比“勉强的方案”,和介绍各种“更好的方案”,其实总结起来都离不开一句话:让代码更加优雅且利于维护

记住这一点,在“够用”和“更好”之间,我们总要逆流而上,勇于在实践中寻找“更好”。

其实不止是这篇文章中提到的一些小技巧,我们在开发中还需要去处理各种类型的问题,对应的解决方案也肯定不止一个,而且正在使用的方案也不一定是最优。所以要时刻有不将就的精神,多花点时间去优化。你可以的!

文章中如有错误希望大家多多指正和多多包涵,我会立即改正的哈。还有要多多评论,多多交流,多多点赞哦~

谢谢你看到了最后,大家一起加油!


YT老秦
806 声望15 粉丝

Web 折腾狮 / 研究猿 - 终生学习,保持第一梯队。