创建一个简单的工具,通过它把一段HTML代码转换为DOM代码。
DOM生成工具的HTML文件
在HTML文件中最主要的是具有两个<textarea>和一个<button>元素:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>DOM Generation</title> <title>AdvancED DOM Scripting Sample Document</title> <link rel="stylesheet" type="text/css" href="styles/source.css" /> <link rel="stylesheet" type="text/css" href="styles/style.css" /> <!-- 自定义的ADS库--> <script type="text/javascript" src="libs/ADS-final-verbose.js"></script> <!-- 日志对象 --> <script type="text/javascript" src="libs/myLogger.js"></script> <!-- 生成DOM的函数 --> <script type="text/javascript" src="generateDOM.js"></script> <!-- 事件处理 --> <script type="text/javascript" src="load.js"></script> </head> <body> <h1>DOM Generation</h1> <div id="content"> <form id="generator" action=""> <fieldset> <h2>Source</h2> <label for="source">Enter an HTML document fragment</label> <textarea id="source" cols="30" rows="15"> <li id="$commentId" class="$commentClass" style="background-color:white"> <a href="$countHref" class="counter" title="$countTitle" onclick="window.open(this.href); return false;">$countContent</a> <span class="commentauthor"><a href="$authorHref" rel="external nofollow">$authorContent</a></span> <small class="comment-meta"> <a href="$metaHref" title="$metaTitle">$metaContent</a> </small> <div class="comment-content"> $commentContent </div> </li> </textarea> <input id="generate" type="button" value="↓ generate ↓" /> <h2>DOM Code</h2> <label for="result">and voila! DOM goodness:</label> <textarea id="result" cols="30" rows="15"></textarea> </fieldset> </form> </div> </body> </html>
这个页面完全依赖于JavaScript,其中包含一个generateDOM.js文件和注册事件侦听器的load.js脚本:
// 注册事件侦听器 ADS.addEvent(window, 'load', function() { // 注册click事件侦听器 ADS.addEvent('generate','click', function(W3CEvent) { // 取得HTML源代码 var source = ADS.$('source').value; // 输出结果 ADS.$('result').value = generateDOM(source); }); });
要转换成DOM代码的HTML代码片段
<li id="comment-1" class="comment c1 c-y2007 c-m01 c-d01 c-h05 alt"> <a href="#comment-1" class="counter" title="Permanent Link to this Comment">1</a> <span class="commentauthor"> <a href="http://wordpress.org/" rel="external nofollow">Mr WordPress</a> </span> <small class="comment-meta"> <a href="#comment-1" title="Permanent Link to this ?Comment">Aug 22nd, 2006 at 5:09 pm</a> </small> <div class="comment-content"> <p>Hi, this is a comment.<br>To delete a comment, just log ?in, and view the posts' comments, there you will have the option ?to edit or delete them.</p> </div> </li>
这个工具的目标就是取得类似下面的HTML代码片段:
<a href="http://wordpress.org/" rel="external nofollow">Mr WordPress</a>
并将它转换为等价的DOM代码:
var a = document.createElement('A'); a.setAttribute('href','http://wordpress.org'); a.setAttribute('rel','external nofollow');
上面代码的链接只是指向了http://wordpress.org,为了增加通用性,将HTML代码片段中需要变化的地方修改成变量,并使用美元符合($)作为前缀:
<li id="$commentId" class="$commentClass" style="background-color:white"> <a href="$countHref" class="counter" title="$countTitle"?onclick="window.open(this.href); return false;">$countContent</a> <span class="commentauthor"><a href="$authorHref" rel="external?nofollow">$authorContent</a></span> <small class="comment-meta"> <a href="$metaHref" title="$metaTitle">$metaContent</a> </small> <div class="comment-content"> $commentContent </div> </li>
扩充ADS库
在构建generateDOM.js之前,需要向ADS.js库添加几个方法。首先利用字符串的prototype属性添加两个新方法:
生成重复的字符串:
/** * 利用核心对象的 prototype 属性 * 重复1个字符串 */ if (!String.repeat) { String.prototype.repeat = function(l){ return new Array(l+1).join(this); } }
上面的函数生成一个参数长度加1的空白字符串,然后使用字符串作为分隔符,例如:
var example = 'a'.repeat(5);
//example is now: aaaaa
[/code]
清除字符串两端的空白字符:
/**
* 移除字符串头部和结尾的空白字符
*/
if (!String.trim) {
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g,'');
}
}
下面是添加到ADS命名空间的camelize()方法:
/**
* 将word-word类型的字符串转换成wordWord类型的字符串
*/
function camelize(s) {
return s.replace(/-(\w)/g, function (strMatch, p1){
return p1.toUpperCase();
});
}
window['ADS']['camelize'] = camelize;
generateDOM的框架
框架创建了一个新的命名空间,然后包含一些辅助方法和属性,最后是为window对象赋值:
/* generateDOM对象的新命名空间 */
(function(){
function encode(str) { }
function checkForVariable(v) { }
function processAttribute(tabCount,refParent) { }
function processNode(tabCount,refParent) { }
var domCode = '';
var nodeNameCounters = [];
var requiredVariables = '';
var newVariables = '';
function generate(strHTML,strRoot) { }
window['generateDOM'] = generate;
})();
encode()方法
generateDOM中的第一个方法是encode()方法。对于生成DOM的方法而言,encode()方法用于保证字符串是一个安全的JavaScript字符串。只需要转义反斜杠、单引号和换行符即可:
function encode(str) {
if (!str) return '';
// 转义反斜杠
str = str.replace(/\\/g,'\\\\');
// 转义单引号
str = str.replace(/';/g, "\\'");
// 转义换行符
str = str.replace(/\s+^/mg, "\\n");
return str;
}
checkForVariable()方法
generateDOM中的第二个方法是checkForVariable()方法。该方法检查字符串中是否包含一个美元符号,如果是,则返回一个带引号的字符串或者变量名。同时,将变量添加到requiredVariable中,以便在输出结果:
function checkForVariable(v) {
if(v.indexOf('$') == -1) {
v = '\'' + v + '\'';
} else {
// 取得该字符串从$到结尾处的子字符串
v = v.substring(v.indexOf('$')+1)
requiredVariables += 'var ' + v + ';\n';
}
return v;
}
generate()方法
generate()方法是核心方法。它遍历DOM树并检测其中所有的节点,然后按照节点的类型生成DOM代码:
function generate(strHTML,strRoot) {
//将HTML代码添加到页面主体中
var domRoot = document.createElement('DIV');
domRoot.innerHTML = strHTML;
// 重置变量
domCode = '';
nodeNameCounters = [];
requiredVariables = '';
newVariables = '';
// 使用processNode()方法处理domRoot中的所有子节点
var node = domRoot.firstChild;
while(node) {
ADS.walkTheDOMRecursive(processNode,node,0,strRoot);
node = node.nextSibling;
}
// 输出结果
domCode =
'/* requiredVariables in this code\n' + requiredVariables + '*/\n\n'
+ domCode + '\n\n'
+ '/* new objects in this code\n' + newVariables + '*/\n\n';
return domCode;
}
processNode()方法
当遍历domRoot中的子节点时,使用processNode()方法分析树中的每个节点,确定节点的类型、值和属性,以便重新创建适当的DOM代码:
function processNode(tabCount,refParent) {
// 根据树的深度重复制表符
// 以便对每一行进行适当的缩进
var tabs = (tabCount ? '\t'.repeat(parseInt(tabCount)) : '');
// 确定节点类型
// 处理元素节点和文本节点
switch(this.nodeType) {
case ADS.node.ELEMENT_NODE:
// 递增计数器
// 结合标签和计数器来表示变量,例如: a1,a2,a3
if(nodeNameCounters[this.nodeName]) {
++nodeNameCounters[this.nodeName];
} else {
nodeNameCounters[this.nodeName] = 1;
}
var ref = this.nodeName.toLowerCase()
+ nodeNameCounters[this.nodeName];
// 创建元素节点
domCode += tabs
+ 'var '
+ ref
+ ' = document.createElement(\'' + this.nodeName +'\');\n';
// 将心变量添加到列表中
newVariables += '' + ref + ';\n';
// 遍历属性
// 使用processAttribute()方法遍历属性
if (this.attributes) {
for(var i=0; i < this.attributes.length; i++) {
ADS.walkTheDOMRecursive(
processAttribute,
this.attributes[i],
tabCount,
ref
);
}
}
break;
case ADS.node.TEXT_NODE:
// 编码文本节点,并去掉空白符
var value = (this.nodeValue ? encode(this.nodeValue.trim()) : '' );
if(value) {
// 递增计数器
// 使用txt和计数器来表示变量,例如: txt1,txt2,txt3
if(nodeNameCounters['txt']) {
++nodeNameCounters['txt'];
} else {
nodeNameCounters['txt'] = 1;
}
var ref = 'txt' + nodeNameCounters['txt'];
// 检查格式。格式要求类似于$var
value = checkForVariable(value);
// 创建文本节点
domCode += tabs
+ 'var '
+ ref
+ ' = document.createTextNode('+ value +');\n';
// 将新变量添加到列表中
newVariables += '' + ref + ';\n';
} else {
// 如果只有空白符则返回
// 即这个节点将不会添加到父节点中
return;
}
break;
default:
// 忽略其它情况
break;
}
// 添加到父节点
if(refParent) {
domCode += tabs + refParent + '.appendChild('+ ref + ');\n';
}
return ref;
}
[/code]
该方法主要做了以下几件事:
1. 基于递归的深度来确定DOM代码缩进的级别,并按照需要重复制表符。这种缩进并不是必要的,但它使生成的代码更清晰,也更容易理解:
[code lang="js"]
var tabs = (tabCount ? '\t'.repeat(parseInt(tabCount)) : '');
[/code]
2. 按照节点类型来做相应的处理。本方法中需要处理两种类型的节点:ADS.node.ELEMENT_NODE(类型值为1)ADS.node.TEXT_NODE(类型值为3)。主要逻辑如下:
[code lang="js"]
switch(node.nodeType) {
case ADS.node.ELEMENT_NODE:
//处理元素节点
break;
case ADS.node.TEXT_NODE:
//处理文本节点
break;
default:
//忽略其它情况
break;
}
[/code]
所有ELEMENT_NODE节点都可能具有属性。属性也是节点,它无法通过同辈定位的方法进行迭代。属性节点包含在node.attributes数组中,因此必须要单独对它们jinx那个遍历:
[code lang="js"]
if (node.attributes) {
for(var i=0; i < node.attributes.length; i++) {
myWalkTheDOM(processAttribute,node.attributes[i], tabCount, ref);
}
}
[/code]
3. 在处理完所有的节点之后,唯一要做的就是将其添加到父节点中:
[code lang="js"]
// 添加到父节点
if(refParent) {
domCode += tabs + refParent + '.appendChild('+ ref + ');\n';
}
[/code]
在创建processAttribute()方法之前,使用示例的HTML代码片段试验这个工具,将会得到如下的DOM代码:
[code lang="js"]
/* requiredVariables in this code
var countContent;
var authorContent;
var metaContent;
var commentContent;
*/
var li1 = document.createElement('li');
document.body.appendChild(li1);
var a1 = document.createElement('a');
li1.appendChild(a1);
var txt1 = document.createTextNode(countContent);
a1.appendChild(txt1);
var span1 = document.createElement('span');
li1.appendChild(span1);
var a2 = document.createElement('a');
span1.appendChild(a2);
var txt2 = document.createTextNode(authorContent);
a2.appendChild(txt2);
var small1 = document.createElement('small');
li1.appendChild(small1);
var a3 = document.createElement('a');
small1.appendChild(a3);
var txt3 = document.createTextNode(metaContent);
a3.appendChild(txt3);
var div1 = document.createElement('div');
li1.appendChild(div1);
var txt4 = document.createTextNode(commentContent);
div1.appendChild(txt4);
/* new objects in this code
li1;
a1;
txt1;
span1;
a2;
txt2;
small1;
a3;
txt3;
[/code]
<strong><span style="font-size: large;">processAttribute()方法</span></strong>
上面的输出中包含所有的ELEMENT_NODE和TEXT_NODE节点,但还缺少节点中的属性,而这正式需要processAttribute()方法来解决的问题:
function processAttribute(tabCount,refParent) {
// 忽略文本节点
if(this.nodeType != ADS.node.ATTRIBUTE_NODE) return;
// 取得属性的值
var attrValue = (this.nodeValue ? encode(this.nodeValue.trim()) : '');
if(this.nodeName == 'cssText') alert('true');
// 如果没有值,则返回
if(!attrValue) return;
// 确定缩进的级别
var tabs = (tabCount ? '\t'.repeat(parseInt(tabCount)) : '');
// 根据nodeName进行判断
switch(this.nodeName){
default:
if (this.nodeName.substring(0,2) == 'on') {
// 如果属性名称以'on'开始,说明是一个事件属性
// 需要创建一个给属性赋值的函数
domCode += tabs
+ refParent
+ '.'
+ this.nodeName
+ '= function(){' + attrValue +'}\n';
} else{
// 对于其它情况,则使用setAttribute()方法
domCode += tabs
+ refParent
+ '.setAttribute(\''
+ this.nodeName
+ '\', '
+ checkForVariable(attrValue)
+');\n';
}
break;
case 'class':
// 将class属性替换为clasName
domCode += tabs
+ refParent
+ '.className = '
+ checkForVariable(attrValue)
+ ';\n';
break;
case 'style':
// 使用分好(;)和空白符来分割样式
var style = attrValue.split(/\s*;\s*/);
if(style){
for(pair in style){
if(!style[pair]) continue;
// 使用冒号(:)和空白符来分隔样式的属性和属性值
var prop = style[pair].split(/\s*:\s*/);
if(!prop[1]) continue;
// 将css-property格式的CSS属性转换为cssProperty格式
prop[0] = ADS.camelize(prop[0]);
var propValue = checkForVariable(prop[1]);
if (prop[0] == 'float') {
// 由于float是保留字
// 将float值替换成cssFloat
domCode += tabs
+ refParent
+ '.style.cssFloat = '
+ propValue
+ ';\n';
domCode += tabs
+ refParent
+ '.style.styleFloat = '
+ propValue
+ ';\n';
} else {
domCode += tabs
+ refParent
+ '.style.'
+ prop[0]
+ ' = '
+ propValue + ';\n';
}
}
}
break;
}
}
processAttribute()方法大致遵循了与processNode()方法相同的处理方式。处理过程大致如下:
1. 属性节点可以通过nodeValue属性取得节点的值,但在processNode()方法已经迭代了TEXT_NODE。所以如果要使用nodeValue属性,就必须跳过非ATTRIBUTE_NODE节点。
2. 如果不存在class或style属性,则使用setAttribute()方法创建相应的属性。但要检查嵌入的事件属性的情形。
3. 如果是class属性,就需要将class节点的值赋给节点的className属性。
4. 如果是style属性,则需要对其进行必要的分割。
至此将HTML代码转换为DOM代码的工具制作完毕,从而免去了许多无谓的DOM脚本编程的工作量。