写在前面
在前面一篇文章《【前端模板之路】一、重构的兄弟说:我才不想看你的代码!把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 )
写在后面
罗里八嗦地写了这么多,终于实现了本文最前面提到的“让代码帮我们写代码”这个目的,实现原理很简单,代码也不复杂,不过真正调试的时候还是花了点时间。时间精力所限,代码难免有疏漏之处(不是无聊的谦词,比如“属性设置自动化”那里的坑还没填。。。),如发现,请指出!!!!!!!