寫在前面
在前面一篇文章《【前端模板之路】一、重構的兄弟說:我才不想看你的代碼!把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 )
寫在后面
羅里八嗦地寫了這么多,終於實現了本文最前面提到的“讓代碼幫我們寫代碼”這個目的,實現原理很簡單,代碼也不復雜,不過真正調試的時候還是花了點時間。時間精力所限,代碼難免有疏漏之處(不是無聊的謙詞,比如“屬性設置自動化”那里的坑還沒填。。。),如發現,請指出!!!!!!!
碼字不易,如覺得內容還湊合,請。。。請點擊下推薦。。。