【前端模板之路】二、人肉非智舉,讓代碼幫我們寫代碼才是王道


寫在前面

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

寫在后面

羅里八嗦地寫了這么多,終於實現了本文最前面提到的“讓代碼幫我們寫代碼”這個目的,實現原理很簡單,代碼也不復雜,不過真正調試的時候還是花了點時間。時間精力所限,代碼難免有疏漏之處(不是無聊的謙詞,比如“屬性設置自動化”那里的坑還沒填。。。),如發現,請指出!!!!!!!

碼字不易,如覺得內容還湊合,請。。。請點擊下推薦。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM