创建一个简单的工具,通过它把一段HTML代码转换为DOM代码。
DOM生成工具的HTML文件
在HTML文件中最主要的是具有两个<textarea>和一个<button>元素:
1 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" |
3 | < html xmlns = "http://www.w3.org/1999/xhtml" > |
4 | < head > |
5 | < title >DOM Generation</ title > |
6 | < title >AdvancED DOM Scripting Sample Document</ title > |
7 | < link rel = "stylesheet" type = "text/css" href = "styles/source.css" /> |
8 | < link rel = "stylesheet" type = "text/css" href = "styles/style.css" /> |
9 |
10 | <!-- 自定义的ADS库--> |
11 | < script type = "text/javascript" src = "libs/ADS-final-verbose.js" ></ script > |
12 | <!-- 日志对象 --> |
13 | < script type = "text/javascript" src = "libs/myLogger.js" ></ script > |
14 | <!-- 生成DOM的函数 --> |
15 | < script type = "text/javascript" src = "generateDOM.js" ></ script > |
16 | <!-- 事件处理 --> |
17 | < script type = "text/javascript" src = "load.js" ></ script > |
18 | </ head > |
19 | < body > |
20 | < h1 >DOM Generation</ h1 > |
21 | < div id = "content" > |
22 | < form id = "generator" action = "" > |
23 | < fieldset > |
24 | < h2 >Source</ h2 > |
25 | < label for = "source" >Enter an HTML document fragment</ label > |
26 | < textarea id = "source" cols = "30" rows = "15" > |
27 | < li id = "$commentId" class = "$commentClass" style = "background-color:white" > |
28 | < a href = "$countHref" class = "counter" title = "$countTitle" onclick = "window.open(this.href); return false;" >$countContent</ a > |
29 | < span class = "commentauthor" >< a href = "$authorHref" rel = "external nofollow" >$authorContent</ a ></ span > |
30 | < small class = "comment-meta" > |
31 | < a href = "$metaHref" title = "$metaTitle" >$metaContent</ a > |
32 | </ small > |
33 | < div class = "comment-content" > |
34 | $commentContent |
35 | </ div > |
36 | </ li > |
37 | </ textarea > |
38 | < input id = "generate" type = "button" value = "↓ generate ↓" /> |
39 | < h2 >DOM Code</ h2 > |
40 | < label for = "result" >and voila! DOM goodness:</ label > |
41 | < textarea id = "result" cols = "30" rows = "15" ></ textarea > |
42 | </ fieldset > |
43 | </ form > |
44 | </ div > |
45 | </ body > |
46 | </ html > |
这个页面完全依赖于JavaScript,其中包含一个generateDOM.js文件和注册事件侦听器的load.js脚本:
1 | // 注册事件侦听器 |
2 | ADS.addEvent(window, 'load' , function () { |
3 |
4 | // 注册click事件侦听器 |
5 | ADS.addEvent( 'generate' , 'click' , function (W3CEvent) { |
6 | |
7 | // 取得HTML源代码 |
8 | var source = ADS.$( 'source' ).value; |
9 | |
10 | // 输出结果 |
11 | ADS.$( 'result' ).value = generateDOM(source); |
12 | |
13 | }); |
14 |
15 | }); |
要转换成DOM代码的HTML代码片段
1 | < li id = "comment-1" class = "comment c1 c-y2007 c-m01 c-d01 c-h05 alt" > |
2 | < a href = "#comment-1" class = "counter" title = "Permanent Link to this Comment" >1</ a > |
3 | < span class = "commentauthor" > |
4 | < a href = "http://wordpress.org/" rel = "external nofollow" >Mr WordPress</ a > |
5 | </ span > |
6 | < small class = "comment-meta" > |
7 | < a href = "#comment-1" title = "Permanent Link to this ?Comment" >Aug 22nd, 2006 at 5:09 pm</ a > |
8 | </ small > |
9 | < div class = "comment-content" > |
10 | < 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 > |
11 | </ div > |
12 | </ li > |
这个工具的目标就是取得类似下面的HTML代码片段:
1 | < a href = "http://wordpress.org/" rel = "external nofollow" >Mr WordPress</ a > |
并将它转换为等价的DOM代码:
1 | var a = document.createElement( 'A' ); |
2 | a.setAttribute( 'href' , 'http://wordpress.org' ); |
3 | a.setAttribute( 'rel' , 'external nofollow' ); |
上面代码的链接只是指向了http://wordpress.org,为了增加通用性,将HTML代码片段中需要变化的地方修改成变量,并使用美元符合($)作为前缀:
1 | < li id = "$commentId" class = "$commentClass" style = "background-color:white" > |
2 | < a href = "$countHref" class = "counter" title = "$countTitle" ? onclick = "window.open(this.href); return false;" >$countContent</ a > |
3 | < span class = "commentauthor" >< a href = "$authorHref" rel = "external?nofollow" >$authorContent</ a ></ span > |
4 | < small class = "comment-meta" > |
5 | < a href = "$metaHref" title = "$metaTitle" >$metaContent</ a > |
6 | </ small > |
7 | < div class = "comment-content" > |
8 | $commentContent |
9 | </ div > |
10 | </ li > |
扩充ADS库
在构建generateDOM.js之前,需要向ADS.js库添加几个方法。首先利用字符串的prototype属性添加两个新方法:
生成重复的字符串:
1 | /** |
2 | * 利用核心对象的 prototype 属性 |
3 | * 重复1个字符串 |
4 | */ |
5 | if (!String.repeat) { |
6 | String.prototype.repeat = function (l){ |
7 | return new Array(l+1).join( this ); |
8 | } |
9 | } |
上面的函数生成一个参数长度加1的空白字符串,然后使用字符串作为分隔符,例如:
var example = 'a'.repeat(5);
//example is now: aaaaa
[/code]
清除字符串两端的空白字符:
1
/**
2
* 移除字符串头部和结尾的空白字符
3
*/
4
if
(!String.trim) {
5
String.prototype.trim =
function
() {
6
return
this
.replace(/^\s+|\s+$/g,
''
);
7
}
8
}
下面是添加到ADS命名空间的camelize()方法:
1
/**
2
* 将word-word类型的字符串转换成wordWord类型的字符串
3
*/
4
function
camelize(s) {
5
return
s.replace(/-(\w)/g,
function
(strMatch, p1){
6
return
p1.toUpperCase();
7
});
8
}
9
window[
'ADS'
][
'camelize'
] = camelize;
generateDOM的框架
框架创建了一个新的命名空间,然后包含一些辅助方法和属性,最后是为window对象赋值:
1
/* generateDOM对象的新命名空间 */
2
(
function
(){
3
4
function
encode(str) { }
5
6
function
checkForVariable(v) { }
7
8
function
processAttribute(tabCount,refParent) { }
9
10
function
processNode(tabCount,refParent) { }
11
12
var
domCode =
''
;
13
var
nodeNameCounters = [];
14
var
requiredVariables =
''
;
15
var
newVariables =
''
;
16
17
function
generate(strHTML,strRoot) { }
18
19
window[
'generateDOM'
] = generate;
20
21
})();
encode()方法
generateDOM中的第一个方法是encode()方法。对于生成DOM的方法而言,encode()方法用于保证字符串是一个安全的JavaScript字符串。只需要转义反斜杠、单引号和换行符即可:
1
function
encode(str) {
2
if
(!str)
return
''
;
3
// 转义反斜杠
4
str = str.replace(/\\/g,
'\\\\'
);
5
// 转义单引号
6
str = str.replace(/
';/g, "\\'
");
7
// 转义换行符
8
str = str.replace(/\s+^/mg,
"\\n"
);
9
return
str;
10
}
checkForVariable()方法
generateDOM中的第二个方法是checkForVariable()方法。该方法检查字符串中是否包含一个美元符号,如果是,则返回一个带引号的字符串或者变量名。同时,将变量添加到requiredVariable中,以便在输出结果:
1
function
checkForVariable(v) {
2
if
(v.indexOf(
'$'
) == -1) {
3
v =
'\''
+ v +
'\''
;
4
}
else
{
5
// 取得该字符串从$到结尾处的子字符串
6
v = v.substring(v.indexOf(
'$'
)+1)
7
requiredVariables +=
'var '
+ v +
';\n'
;
8
}
9
return
v;
10
}
generate()方法
generate()方法是核心方法。它遍历DOM树并检测其中所有的节点,然后按照节点的类型生成DOM代码:
1
function
generate(strHTML,strRoot) {
2
3
//将HTML代码添加到页面主体中
4
var
domRoot = document.createElement(
'DIV'
);
5
domRoot.innerHTML = strHTML;
6
7
// 重置变量
8
domCode =
''
;
9
nodeNameCounters = [];
10
requiredVariables =
''
;
11
newVariables =
''
;
12
13
// 使用processNode()方法处理domRoot中的所有子节点
14
var
node = domRoot.firstChild;
15
while
(node) {
16
ADS.walkTheDOMRecursive(processNode,node,0,strRoot);
17
node = node.nextSibling;
18
}
19
20
// 输出结果
21
domCode =
22
'/* requiredVariables in this code\n'
+ requiredVariables +
'*/\n\n'
23
+ domCode +
'\n\n'
24
+
'/* new objects in this code\n'
+ newVariables +
'*/\n\n'
;
25
26
return
domCode;
27
}
processNode()方法
当遍历domRoot中的子节点时,使用processNode()方法分析树中的每个节点,确定节点的类型、值和属性,以便重新创建适当的DOM代码:
1
function
processNode(tabCount,refParent) {
2
// 根据树的深度重复制表符
3
// 以便对每一行进行适当的缩进
4
var
tabs = (tabCount ?
'\t'
.repeat(parseInt(tabCount)) :
''
);
5
6
// 确定节点类型
7
// 处理元素节点和文本节点
8
switch
(
this
.nodeType) {
9
case
ADS.node.ELEMENT_NODE:
10
// 递增计数器
11
// 结合标签和计数器来表示变量,例如: a1,a2,a3
12
if
(nodeNameCounters[
this
.nodeName]) {
13
++nodeNameCounters[
this
.nodeName];
14
}
else
{
15
nodeNameCounters[
this
.nodeName] = 1;
16
}
17
18
var
ref =
this
.nodeName.toLowerCase()
19
+ nodeNameCounters[
this
.nodeName];
20
21
// 创建元素节点
22
domCode += tabs
23
+
'var '
24
+ ref
25
+
' = document.createElement(\''
+
this
.nodeName +
'\');\n'
;
26
27
// 将心变量添加到列表中
28
newVariables +=
''
+ ref +
';\n'
;
29
30
// 遍历属性
31
// 使用processAttribute()方法遍历属性
32
if
(
this
.attributes) {
33
for
(
var
i=0; i <
this
.attributes.length; i++) {
34
ADS.walkTheDOMRecursive(
35
processAttribute,
36
this
.attributes&
#91;i],
37
tabCount,
38
ref
39
);
40
}
41
}
42
43
break
;
44
45
case
ADS.node.TEXT_NODE:
46
47
// 编码文本节点,并去掉空白符
48
var
value = (
this
.nodeValue ? encode(
this
.nodeValue.trim()) :
''
);
49
if
(value) {
50
51
// 递增计数器
52
// 使用txt和计数器来表示变量,例如: txt1,txt2,txt3
53
if
(nodeNameCounters&
#91;'txt']) {
54
++nodeNameCounters&
#91;'txt'];
55
}
else
{
56
nodeNameCounters&
#91;'txt'] = 1;
57
}
58
var
ref =
'txt'
+ nodeNameCounters&
#91;'txt'];
59
60
// 检查格式。格式要求类似于$var
61
value = checkForVariable(value);
62
63
// 创建文本节点
64
domCode += tabs
65
+
'var '
66
+ ref
67
+
' = document.createTextNode('
+ value +
');\n'
;
68
// 将新变量添加到列表中
69
newVariables +=
''
+ ref +
';\n'
;
70
71
}
else
{
72
// 如果只有空白符则返回
73
// 即这个节点将不会添加到父节点中
74
return
;
75
}
76
break
;
77
78
default
:
79
// 忽略其它情况
80
break
;
81
}
82
83
// 添加到父节点
84
if
(refParent) {
85
domCode += tabs + refParent +
'.appendChild('
+ ref +
');\n'
;
86
}
87
return
ref;
88
}
89
&
#91;/code]
90
该方法主要做了以下几件事:
91
1. 基于递归的深度来确定DOM代码缩进的级别,并按照需要重复制表符。这种缩进并不是必要的,但它使生成的代码更清晰,也更容易理解:
92
&
#91;code lang="js"]
93
var
tabs = (tabCount ?
'\t'
.repeat(parseInt(tabCount)) :
''
);
94
&
#91;/code]
95
2. 按照节点类型来做相应的处理。本方法中需要处理两种类型的节点:ADS.node.ELEMENT_NODE(类型值为1)ADS.node.TEXT_NODE(类型值为3)。主要逻辑如下:
96
&
#91;code lang="js"]
97
switch
(node.nodeType) {
98
case
ADS.node.ELEMENT_NODE:
99
//处理元素节点
100
break
;
101
case
ADS.node.TEXT_NODE:
102
//处理文本节点
103
break
;
104
default
:
105
//忽略其它情况
106
break
;
107
}
108
&
#91;/code]
109
所有ELEMENT_NODE节点都可能具有属性。属性也是节点,它无法通过同辈定位的方法进行迭代。属性节点包含在node.attributes数组中,因此必须要单独对它们jinx那个遍历:
110
&
#91;code lang="js"]
111
if
(node.attributes) {
112
for
(
var
i=0; i < node.attributes.length; i++) {
113
myWalkTheDOM(processAttribute,node.attributes&
#91;i], tabCount, ref);
114
}
115
}
116
&
#91;/code]
117
3. 在处理完所有的节点之后,唯一要做的就是将其添加到父节点中:
118
&
#91;code lang="js"]
119
// 添加到父节点
120
if
(refParent) {
121
domCode += tabs + refParent +
'.appendChild('
+ ref +
');\n'
;
122
}
123
&
#91;/code]
124
在创建processAttribute()方法之前,使用示例的HTML代码片段试验这个工具,将会得到如下的DOM代码:
125
&
#91;code lang="js"]
126
/* requiredVariables in this code
127
var countContent;
128
var authorContent;
129
var metaContent;
130
var commentContent;
131
*/
132
var
li1 = document.createElement(
'li'
);
133
document.body.appendChild(li1);
134
var
a1 = document.createElement(
'a'
);
135
li1.appendChild(a1);
136
var
txt1 = document.createTextNode(countContent);
137
a1.appendChild(txt1);
138
var
span1 = document.createElement(
'span'
);
139
li1.appendChild(span1);
140
var
a2 = document.createElement(
'a'
);
141
span1.appendChild(a2);
142
var
txt2 = document.createTextNode(authorContent);
143
a2.appendChild(txt2);
144
var
small1 = document.createElement(
'small'
);
145
li1.appendChild(small1);
146
var
a3 = document.createElement(
'a'
);
147
small1.appendChild(a3);
148
var
txt3 = document.createTextNode(metaContent);
149
a3.appendChild(txt3);
150
var
div1 = document.createElement(
'div'
);
151
li1.appendChild(div1);
152
var
txt4 = document.createTextNode(commentContent);
153
div1.appendChild(txt4);
154
/* new objects in this code
155
li1;
156
a1;
157
txt1;
158
span1;
159
a2;
160
txt2;
161
small1;
162
a3;
163
txt3;
164
[/code]
165
166
<strong><span style="font-size: large;">processAttribute()方法</span></strong>
167
168
上面的输出中包含所有的ELEMENT_NODE和TEXT_NODE节点,但还缺少节点中的属性,而这正式需要processAttribute()方法来解决的问题:
169
170
function processAttribute(tabCount,refParent) {
171
172
// 忽略文本节点
173
if(this.nodeType != ADS.node.ATTRIBUTE_NODE) return;
174
175
// 取得属性的值
176
var attrValue = (this.nodeValue ? encode(this.nodeValue.trim()) : '');
177
if(this.nodeName == 'cssText') alert('true');
178
// 如果没有值,则返回
179
if(!attrValue) return;
180
181
// 确定缩进的级别
182
var tabs = (tabCount ? '\t'.repeat(parseInt(tabCount)) : '');
183
184
// 根据nodeName进行判断
185
switch(this.nodeName){
186
default:
187
if (this.nodeName.substring(0,2) == 'on') {
188
// 如果属性名称以'on'开始,说明是一个事件属性
189
// 需要创建一个给属性赋值的函数
190
domCode += tabs
191
+ refParent
192
+ '.'
193
+ this.nodeName
194
+ '= function(){' + attrValue +'}\n';
195
} else{
196
197
// 对于其它情况,则使用setAttribute()方法
198
domCode += tabs
199
+ refParent
200
+ '.setAttribute(\''
201
+ this.nodeName
202
+ '\', '
203
+ checkForVariable(attrValue)
204
+');\n';
205
}
206
break;
207
case 'class':
208
// 将class属性替换为clasName
209
domCode += tabs
210
+ refParent
211
+ '.className = '
212
+ checkForVariable(attrValue)
213
+ ';\n';
214
break;
215
case 'style':
216
// 使用分好(;)和空白符来分割样式
217
var style = attrValue.split(/\s*;\s*/
);
218
219
if
(style){
220
for
(pair
in
style){
221
222
if
(!style[pair])
continue
;
223
224
// 使用冒号(:)和空白符来分隔样式的属性和属性值
225
var
prop = style[pair].split(/\s*:\s*/);
226
if
(!prop[1])
continue
;
227
228
// 将css-property格式的CSS属性转换为cssProperty格式
229
prop[0] = ADS.camelize(prop[0]);
230
231
var
propValue = checkForVariable(prop[1]);
232
if
(prop[0] ==
'float'
) {
233
// 由于float是保留字
234
// 将float值替换成cssFloat
235
domCode += tabs
236
+ refParent
237
+
'.style.cssFloat = '
238
+ propValue
239
+
';\n'
;
240
domCode += tabs
241
+ refParent
242
+
'.style.styleFloat = '
243
+ propValue
244
+
';\n'
;
245
}
else
{
246
domCode += tabs
247
+ refParent
248
+
'.style.'
249
+ prop[0]
250
+
' = '
251
+ propValue +
';\n'
;
252
}
253
}
254
}
255
break
;
256
}
257
}
processAttribute()方法大致遵循了与processNode()方法相同的处理方式。处理过程大致如下:
1. 属性节点可以通过nodeValue属性取得节点的值,但在processNode()方法已经迭代了TEXT_NODE。所以如果要使用nodeValue属性,就必须跳过非ATTRIBUTE_NODE节点。
2. 如果不存在class或style属性,则使用setAttribute()方法创建相应的属性。但要检查嵌入的事件属性的情形。
3. 如果是class属性,就需要将class节点的值赋给节点的className属性。
4. 如果是style属性,则需要对其进行必要的分割。
至此将HTML代码转换为DOM代码的工具制作完毕,从而免去了许多无谓的DOM脚本编程的工作量。