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

写在前面

在前面一篇文章《【前端模板之路】一、重构的兄弟说:我才不想看你的代码!把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 ) 

写在后面

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

时间: 2024-12-03 14:03:57

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

【前端模板之路】一、重构的兄弟说:我才不想看你的代码!把HTML给我交出来!

写在前面 随着前端领域的发展和社会化分工的需要,继前端攻城湿之后,又一重要岗位横空出世--重构攻城湿!所谓的重构攻城湿,他们的一大特点之一,就是精通CSS配置文件的编写...前端攻城湿跟重构攻城湿是一对好基友,你写逻辑来,我写样式. 好吧,本文并不是介绍重构攻城湿这个职业的,而是通过一个简单的场景来说说: 1. 在用前端模板之前,我们是肿么动态创建节点的 2. 为什么要使用前端模板 一个简单的场景 下面这张图片看着应该很眼熟吧?没错,是从mac QQ的好友列表里面截出来的.作为一名前端攻城湿,相

旭日阳刚真实身份被网友人肉非真实“农民工”

旭日阳刚 在汪峰提出"禁唱"<春天里>要求后,草根组合旭日阳刚在央视元宵晚会上的选曲又成焦点. 汪峰禁唱事件后,有媒体采访到将旭日阳刚视频上传到网上的商海峰,他透露旭日阳刚并不是"农民工组合",这引发 网友人肉搜索旭日阳刚的真实身份.有网友指:主唱王旭,河南商丘市民权县人,打过短工,卖过水果.蔬菜,一度靠吃馒头咸菜度日,找工作被中介骗过,后在北京某单位做仓库保管员.吉他手刘刚,家在黑龙江省牡丹江地区穆棱市河西乡三兴村,当过兵,做过保安,摆过地摊,曾短期在

IT人的技术哲学书单:谁说写代码、做产品就不需要参禅悟道?

刚刚进入大学校门时老师曾经说过:"无论学习什么专业,只要研究到最后就是哲学."我们笑着问道:"那么,写代码写到最后也是哲学?"老师回答:"是的,那就是技术哲学."现在回想起来,的确如此,我们发现技术中无处存在着哲学.那编写代码来说,对于同样一个功能进行实现,有的同学就会使用了很多的设计思想和设计模式,这样的代码无论是在自己看来还是拿给其他人看都会是赏心悦目的,而且也非常便于后期的重构. 无论是科学家还是工程师,成长不能只局限于技术层面,也要学会如

百度的语音搜索=“人肉”搜索?

中介交易 http://www.aliyun.com/zixun/aggregation/6858.html">SEO诊断淘宝客 站长团购 云主机 技术大厅 看到网上有人说百度把百度推出语音搜索称为"人肉搜索",真是让人笑掉大牙,不知道是谁写的,估计是个不懂装懂的新记者吧. "人肉搜索"并不是个新名词,2006年沸沸扬扬的"铜须门"事件,第一次充分展示了"人肉搜索"的威力--在千千万万网民参与.提供并挖掘信息的

如何在前端编码时实现人肉双向编译

如何在前端编码时实现人肉双向编译 React+flux是目前最火的前端解决方案之一,但flux槽点颇多,例如store比较混乱,使用比较繁琐等,于是出现了很多第三方的基于flux优化的架构. 有人统计了目前主流的flux实现方案,感兴趣的可以看这里:Which Flux implementation should I use? 其中redux是目前github上star最多的一个方案,该方案完全独立于react,意味着这套理念可以作为架构层应用于其他的组件化方案.同时官方也提供了react-re

南方都市报:人肉搜索应该被规范而非禁止

中介交易 http://www.aliyun.com/zixun/aggregation/6858.html">SEO诊断 淘宝客 云主机 技术大厅 关于"人肉搜索"是否要入罪的问题,全国人大常委会法制工作委员会副主任郎胜2月28日表示,"人肉搜索"的概念涉及到它的界限如何确定,这些都还是研讨中的问题(中新网2月28日).而早在1月中旬,徐州市率先立法明确禁止人肉搜索,这一做法立即遭致法学家张千帆先生在<南方都市报>撰文批评,他认为&qu

人肉搜索引擎

中介交易 SEO诊断 淘宝客 云主机 技术大厅 人肉搜索引擎就是指更多的利用人工参与来提纯搜索引擎提供的信息的一种机制.猫扑的人肉搜索引擎就是其中一个比较成功的例子.后面我们会根据猫扑的人肉搜索引擎给出一个更加具体的描述. 针对搜索引擎的Link Farm和Spam也许永远不会停止,因为他们能够从他们的作恶中得到利益.我们知道得到利益不是作恶的唯一原因,然而大规模的工业化的作恶唯一的原因当然是那样可以得到利益. 所以,这是一场永远无法结束的战争,只要搜索引擎还是应用最广泛的一种互联网应用,只要搜

人肉搜索使大众接受尚需时日

中介交易 SEO诊断 淘宝客 云主机 技术大厅 信息化的泛滥,注定让网民产生不安全感.也许不经意间,您的银行卡,您的手机,您的公交卡,您的IP地址就会出卖您.十年前即时通讯刚刚成为时尚,大家用键盘神侃之余,往往说"网络上没有人知道您是一条狗",但这话目前已明显不合时宜了.因为现在,只要有必要,一定会有人会知道您是不是狗,或者是一条什么样的狗. 怎么找到真实存在但却大隐于屏幕后的狗,一个有效的办法就是"人肉搜索".有了这个暗器,虚拟却无所不在的力量会让一个倒霉蛋子在现

谁在人肉搜索?——网络人肉搜索主体的Logistic回归模型分析

本文发表于<广告大观(理论版)>2010年02期,主要使用SPSS软件对一份包含5758个样本的数据进行Logistic回归分析,考察网络人肉搜索参与主体的人口统计特征.基本网络使用情况以及具体的网络参与行为对其参与人肉搜索的影响程度.研究发现23-27岁的男性低收入群体.经常参与网络口水战的人.在网络上爱说谎的人.网络发言是为了发泄情绪的人.网络发言为了引起别人注意的人.更愿意讨论陌生人的私人话题的人.在网络和现实中表现不一的人更容易参与人肉搜索.而且是否参与人肉搜索与网龄和每天上网时间相关