【前端模板之路】二、人肉非智举,让代码帮我们写代码才是王道

  1. 云栖社区>
  2. 博客>
  3. 正文

【前端模板之路】二、人肉非智举,让代码帮我们写代码才是王道

虫师2016 2016-04-29 15:41:11 浏览1504
展开阅读全文

写在前面

在前面一篇文章【前端模板之路】一、重构的兄弟说:我才不想看你的代码!把HTML给我交出来!中,我们举了一个人肉各种createElement的例子,那繁琐程度绝对是惨绝人寰。人生本就苦短,每天加班又占据了不少时间,这么折腾下去,还让人怎么活。面对这种场景,我们该怎么做。

无需复杂的构建工具,仅几个简单的工具函数,帮我们告别重复意义的劳动:让代码帮我们写代码!

从最简单的例子说起

让代码帮我们写代码,似乎很豪迈的话,但相信部分童鞋听着还是有些丈二和尚摸不着头脑。那我们暂且抛开这句不知所云的话,来看看下面这个例子。一段简单的HTML

<h3>小卡的测试号</h3>

现在让我们来“人肉”创建下这个节点,无非就createElement、createTextNode两个操作

var nick = document.createElement('h3');  // 元素节点
var nickTxt = document.createTextNode('小卡的测试号');  // 文本节点
nick.appendChild(nickTxt);

现在让我们在节点上加多点内容

<h3 class="title">小卡的测试号</h3>

继续我们的人肉操作,与上文类似,只是多了个setAttribute的步骤

var nick = document.createElement('h3');  // 元素节点
nick.setAttribute('class', 'title');  // 设置节点属性
var nickTxt = document.createTextNode('小卡的测试号');  // 文本节点
nick.appendChild(nickTxt);

很简单的例子,到这里为止。可能你有这样的疑惑:这样的例子跟我们的“让代码帮我们写代码”有什么关系。是的,一切的谜底就在其中,请往下看。

创建节点三部曲——你究竟看到了什么

从上面的代码,我们可以看出,人肉创建一个节点——我们用节点P来表示,包含以下三个步骤:

1. 创建节点P

2. 给节点P设置属性

3. 创建节点P的子节点C

其中,步骤3 创建子节点,跟创建一个节点P的过程完全一致,也就是说,这里的关键,是dom树的遍历过程

那么,我们将要做什么

上面我们已经简单分析了一个节点创建的几个逻辑步骤,那么,现在说下,“让代码帮我们写代码”究竟是什么意思。很简单,那就是:随便给一段HTML文本,自动生成上面那堆createElement、createTextNode、setAttribute

整体目标已经明确,现在我们来分解下子任务:

1. 节点创建(createElement...)自动化

2. 属性设置(setAttribute...)自动化

3. 代码自动格式化

一点必要的准备工作

上面我们提到,代码写代码,实现的关键点在于dom树的遍历。现在我们手头上只有一段HTML文本(字符串),如何遍历?正则什么的有点高端不敢碰,来点奇淫技巧,先把文本转成dom节点,现在,我们的HTML文本就转成可遍历的节点了,即wrapper.childeNodes

 

var wrapper = document.createElement('div');
wrapper.innerHTML = html;

var childNodes = wrapper.childNodes;  // 我们真正要遍历的节点

 

目标一:节点创建自动化

废话不多说,直接上代码,逻辑很简单,关键是区分三种不同的节点类型即可。实际上节点类型不止三种,但动态创建过程中常见的也就Element、TextNode两种,如果有需要,可自行补充

复制代码
function createNode(childNode){
    var arr  = [],
        childNodeName = getName( childNode );  // 一个工具方法,返回一个变量名,实现细节先不管它

    switch(childNode.nodeType){
        case 3:  // 文本节点
            arr = arr.concat( 'var ' + childNodeName + ' = ' + 'document.createTextNode("'+ childNode.nodeValue +'")' );
            break;
        case 8:  // 注释
            arr = arr.concat( 'var ' + childNodeName + ' = ' + 'document.createComment("'+ childNode.nodeValue +'")' );
            break;
        default:  // 其他
            arr.push( 'var '+ childNodeName + ' = ' + 'document.createElement("'+ childNode.nodeName.toLowerCase() +'")' );
            break;
    }
    return arr;
}
复制代码

 

目标二:属性设置自动化

直接上代码,我们知道,节点的属性存在一个叫做attributes的特性里,attributes是个NamedNodeMap,名字很奇怪,知道下面几点即可:

1. attributes里存的是节点的属性,举例来说,上面class="title",这个class就是节点的属性

2. attributes是个类数组,可遍历,有个length属性,表示节点属性的个数

3. 每个attributes元素是个对象,该对象有两个关键的属性,即name(节点属性名)和value(节点属性值),如下面代码所示

于是我们得到如下代码

复制代码
function createAttribute(childNode, childNodeName, tabNum){
    var attributes = childNode.attributes,
        arr = [],
        childNodeName = getName( childNode );

    for(var j=0; j<attributes.length; j++){
        var attribute = attributes[j];
        arr.push( childNodeName +'.setAttribute("' + attribute.name + '", "' + attribute.value + '");' );
    }
    return arr;
}
复制代码

 之前在jQuery源码分析系列里写了篇文章jQuery源码-jQuery.fn.attr与jQuery.fn.prop,看了你就会知道,上面这段代码其实是有坑的,但是先不引入额外的复杂度,有时间我再补充(程序员最大的谎言:TODO)

目标三:代码自动格式化 

代码多了,一堆createElement、appendChild神马的,一下就把人看晕了,完全看不出层级结构,这个时候加上合理的缩进是很有必要的,缩进的数目跟dom树的深度成正比,直接看个例子

复制代码
var div_1 = document.createElement("div")
div_1.setAttribute("nick", "casepr");
    var h1_1 = document.createElement("h1")
    h1_1.setAttribute("class", "title");
    div_1.appendChild( h1_1 )
        var text_1 = document.createTextNode("标题")
        h1_1.appendChild( text_1 )
复制代码

这里只贴个简单的工具方法,比如repeat('a', 3)返回 'aaa'

// 返回num个str拼成的字符串
function repeat(str, num){
    return new Array(num+1).join(str);
}

终极奥义——完整的代码实现

简单把代码封装了下,需要关注的是Util.getCode方法,举个例子Util.getCode('<h3 class="title">小卡的测试号</h3>'),看看输出是什么 :)

代码注释写得算是比较详细了,不缀述~~

复制代码
var Util = (function(){

    var map = {};
    //console.log( arr.join('\n') );

    /**
     * 核心方法,遍历一个节点,返回创建这个节点需要的完整步骤
     * 
     * @param  {HTMLElement} parentNode           dom节点
     * @param  {Boolean} needCreateParentNode true: 需要添加parentNode本身的创建步骤;false:不需要
     * @param  {Number} tabNum               tab缩进的数目
     * @param  {String} parentNodeName       我们已经为parentNode生成的变量名,如无,则为空字符串
     * @return {Array}                      创建parentNode所需要的完整步骤
     */
    function getCodeRecursively(parentNode, needCreateParentNode, tabNum, parentNodeName){
        
        var childNodes = parentNode.childNodes,
            i =0,
            len = childNodes.length,
            arr = [];

        parentNodeName = parentNodeName || getName(parentNode);
        if( needCreateParentNode ){
            arr = arr.concat( createNode(parentNode, parentNodeName, tabNum) );    // 1、create父节点,给父节点setAttribute    
        }
        
        ++tabNum;

        for(; i<len; i++){
            
            var childNode = childNodes[i];

            if( shouldTravel(childNode) ){
                
                var childNodeName = getName(childNode);

                arr = arr.concat( createNode(childNode, childNodeName, tabNum) );
                arr.push( repeat('\t', tabNum) + parentNodeName +'.appendChild( '+ childNodeName +' )' );    // 3、塞子节点
                arr = arr.concat( getCodeRecursively( childNode, false, tabNum, childNodeName ) );
            }
        }
        return arr;
    }

    /**
     * 创建属性
     * @param  {HTMLElement} node     节点
     * @param  {String} variName 为node起的变量名
     * @param  {Number} tabNum   缩进数目
     * @return {Array}          详细步骤
     */
    function createAttribute(node, variName, tabNum){
        var attributes = node.attributes,
            arr = [];
        for(var j=0; j<attributes.length; j++){
            var attribute = attributes[j];
            arr.push( repeat('\t', tabNum) + variName +'.setAttribute("' + attribute.name + '", "' + attribute.value + '");' );
        }
        return arr;
    }

    /**
     * 创建节点
     * @param  {HTMLElement} node     节点
     * @param  {String} variName 为node起的变量名
     * @param  {Number} tabNum   缩进数目
     * @return {Array}          详细步骤
     */
    function createNode(node, variName, tabNum){
        var arr  = [];

        switch(node.nodeType){
            case 3:  // 文本节点
                arr = arr.concat( repeat('\t', tabNum) + 'var ' + variName + ' = ' + 'document.createTextNode("'+ node.nodeValue +'")' );
                break;
            case 8:  // 注释
                arr = arr.concat( repeat('\t', tabNum) + 'var ' + variName + ' = ' + 'document.createComment("'+ node.nodeValue +'")' );
                break;
            default:  // 其他
                arr.push( repeat('\t', tabNum) + 'var '+ variName + ' = ' + 'document.createElement("'+ node.nodeName.toLowerCase() +'")' );
                arr = arr.concat( createAttribute(node, variName, tabNum) );
                break;
        }
        return arr;
    }

    /**
     * 是否应该遍历节点(这个方法是否恰当??)
     * @param  {HTMLElement} node 节点
     * @return {Boolean}      true:应该遍历;false:不应该遍历
     */
    function shouldTravel( node ){
        return node.nodeType==1 || node.nodeValue.trim()!='';
    }

    /**
     * 返回一个变量名,
     * @param  {HTMLElement} node 
     * @return {String}      变量名,格式为 nodeName_XXX,其中nodeName是节点名的小写,XX为数字,例: div_1
     */
    function getName(node){
        var nodeName = node.nodeName.toLowerCase().replace('#', '');
        if(!map[nodeName]){
            map[nodeName] = 1;
        }else{
            map[nodeName]++;
        }
        return nodeName+ '_' +map[nodeName];
    }

    /**
     * 返回num个str拼成的字符串
     * @param  {String} str 一段字符
     * @param  {Number} num 重复次数
     * @return {String}     num个str拼成的字符串
     */
    function repeat(str, num){
        return new Array(num+1).join(str);
    }

    return {
        /**
         * 根据html字符串,返回这段字符串对应的dom节点的完整创建过程
         * @param  {String} html HTML字符串
         * @return {Array}      创建步骤
         */
        getCode: function(html){
            var arr = [],
                // map = {},
                i = 0,
                len = 0,
                childNodes = [];
map = {};
var wrapper = document.createElement('div'); wrapper.innerHTML = html; childNodes = wrapper.childNodes; // 这段代码也是可以提取的,TODO吧 len = childNodes.length; for(; i<len; i++){ var childNode = childNodes[i]; if(shouldTravel(childNode)){ arr = arr.concat( getCodeRecursively(childNode, true, 0, '') ); } } return arr; } }; })();
复制代码

你让我肿么相信你——测试用例

附上简短测试用例一枚:

复制代码
var html = '<div nick="casepr">\
                <h1 class="title">标题</h1>\
                纯文本节点\
                <!--注释-->\
                <div class="content">\
                    <div class="preview">预览</div>\
                    <div class="content">正文</div>\
                </div>\
                <label for="box" class="select">选择:</label>\
                <input type="checkbox" id="box" name="box" checked="checked" />\
            </div>';
console.log( Util.getCode(html).join('\n') );
复制代码

输出结果:

复制代码
var div_1 = document.createElement("div")
div_1.setAttribute("nick", "casepr");
    var h1_1 = document.createElement("h1")
    h1_1.setAttribute("class", "title");
    div_1.appendChild( h1_1 )
        var text_1 = document.createTextNode("标题")
        h1_1.appendChild( text_1 )
    var text_2 = document.createTextNode("                纯文本节点                ")
    div_1.appendChild( text_2 )
    var comment_1 = document.createComment("注释")
    div_1.appendChild( comment_1 )
    var div_2 = document.createElement("div")
    div_2.setAttribute("class", "content");
    div_1.appendChild( div_2 )
        var div_3 = document.createElement("div")
        div_3.setAttribute("class", "preview");
        div_2.appendChild( div_3 )
            var text_3 = document.createTextNode("预览")
            div_3.appendChild( text_3 )
        var div_4 = document.createElement("div")
        div_4.setAttribute("class", "content");
        div_2.appendChild( div_4 )
            var text_4 = document.createTextNode("正文")
            div_4.appendChild( text_4 )
    var label_1 = document.createElement("label")
    label_1.setAttribute("for", "box");
    label_1.setAttribute("class", "select");
    div_1.appendChild( label_1 )
        var text_5 = document.createTextNode("选择:")
        label_1.appendChild( text_5 )
    var input_1 = document.createElement("input")
    input_1.setAttribute("type", "checkbox");
    input_1.setAttribute("id", "box");
    input_1.setAttribute("name", "box");
    input_1.setAttribute("checked", "checked");
    div_1.appendChild( input_1 ) 
复制代码

写在后面

罗里八嗦地写了这么多,终于实现了本文最前面提到的“让代码帮我们写代码”这个目的,实现原理很简单,代码也不复杂,不过真正调试的时候还是花了点时间。时间精力所限,代码难免有疏漏之处(不是无聊的谦词,比如“属性设置自动化”那里的坑还没填。。。),如发现,请指出!!!!!!!

网友评论

登录后评论
0/500
评论
虫师2016
+ 关注