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


    写在前面

    在前面一篇文章【前端模板之路】一、重构的兄弟说:我才不想看你的代码!把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('
    ') );
    
        /**
         * 核心方法,遍历一个节点,返回创建这个节点需要的完整步骤
         * 
         * @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('	', 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('	', 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('	', tabNum) + 'var ' + variName + ' = ' + 'document.createTextNode("'+ node.nodeValue +'")' );
                    break;
                case 8:  // 注释
                    arr = arr.concat( repeat('	', tabNum) + 'var ' + variName + ' = ' + 'document.createComment("'+ node.nodeValue +'")' );
                    break;
                default:  // 其他
                    arr.push( repeat('	', 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('
    ') );

    输出结果:

    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 ) 

    写在后面

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

    码字不易,如觉得内容还凑合,请。。。请点击下推荐。。。

  • 相关阅读:
    ReactNative: 使用分组列表组件SectionList组件
    ReactNative: 使用刷新组件RefreshControl组件
    ReactNative: 使用开关组件Switch组件
    ReactNative: 使用滑块组件Slider组件
    ReactNative: 使用分段组件SegmentedControlIOS组件
    ReactNative: 使用进度条组件ProgressViewIOS组件
    Swift语言实战晋级
    《Swift开发指南》
    《数据结构与算法JavaScript描述》
    《ASP.NET MVC 5 框架揭秘》
  • 原文地址:https://www.cnblogs.com/chyingp/p/create-code-with-code.html
Copyright © 2020-2023  润新知