TechWeb-技术社区 » 系统设置 » 网页设计 » 超越DOM(轻松使用 DOM 的技巧和诀窍


2008-5-14 09:16 麻雀杀手
超越DOM(轻松使用 DOM 的技巧和诀窍

文档对象模型(Document Object Model,DOM)是用于操纵 XML 和 HTML 数据的最常用工具之一,然而它的潜力却很少被充分挖掘出来。通过利用 DOM 的优势,并使它更加易用,您将获得一款应用于 XML 应用程序(包括动态 Web 应用程序)的强大工具。+y3g,Q(Z+V3` H A

:r'pH3\`3e~2V     本期文章介绍了一位客串的专栏作家,同时也是我的朋友和同事 Dethe Elza。Dethe 在利用 XML 进行 Web 应用程序开发方面经验丰富,在此,我要感谢他对我在介绍使用 DOM 和 ECMAScript 进行 XML 编程这一方面的帮助。请密切关注本专栏,以了解 Dethe 的更多专栏文章。
h@yrdaS&e —— David Mertz %oLeY1QdeM w
?Ln\ Nd\$cd
    DOM 是处理 XML 和 HTML 的标准 API 之一。由于它占用内存大、速度慢,并且冗长,所以经常受到人们的指责。尽管如此,对于很多应用程序来说,它仍然是最佳选择,而且比 XML 的另一个主要 API —— SAX 无疑要简单得多。DOM 正逐渐出现在一些工具中,比如 Web 浏览器、SVG 浏览器、OpenOffice,等等。
xoy(NYMU $W;[o ]8O-oq c2|
    DOM 很好,因为它是一种标准,并且被广泛地实现,同时也内置到其他标准中。作为标准,它对数据的处理与编程语言无关(这可能是优点,也可能是缺点,但至少使我们处理数据的方式变得一致)。DOM 现在不仅内置于 Web 浏览器,而且也成为许多基于 XML 的规范的一部分。既然它已经成为您的工具的一部分,并且或许您偶尔还会使用它,我想现在应该充分利用它给我们带来的功能了。&WgM|~MG/k
v(rM*n1L/v r2m$i
    在使用 DOM 一段时间后,您会看到形成了一些模式 —— 您想要反复做的事情。快捷方式可以帮助您处理冗长的 DOM,并创建自解释的、优雅的代码。这里收集了一些我经常使用的技巧和诀窍,还有一些 javascript 示例。8{BT5m#Opx-B0Q h
-s%U6U'[?/KS `6w]:O+i
insertAfter 和 prependChild

2008-5-14 09:17 麻雀杀手
第一个诀窍就是“没有诀窍”。DOM 有两种方法将孩子节点添加到容器节点(常常是一个 Element,也可能是一个 Document 或 Document Fragment):appendChild(node) 和 insertBefore(node, referenceNode)。看起来似乎缺少了什么。假如我想在一个参考节点后面插入或者由前新增(prepend)一个子节点(使新节点位于列表中的第一位),我该怎么做呢?很多年以来,我的解决方法是编写下列函数:
6W0B{R?[O|+y#O
^p"n0K*B 清单 1. 插入和由前新增的错误方法8r)df#hY.dE#Jt6[eUb
function insertAfter(parent, node, referenceNode) {
o'r;C:LS;v#T     if(referenceNode.nextSibling) {%mj OD6[ H&jA\
        parent.insertBefore(node, referenceNode.nextSibling);
3q ~ D i,P1`+?Z     } else {3^ oC h;V3p(Zd"I%K
        parent.appendChild(node); m3p r,or8k;Y7]
    }
u4b+~*h#hl,Cn2I }xoX{l-W
function prependChild(parent, node) {
9r]3x+nO+X:O5u     if (parent.firstChild) {
3D6j*@5[1Kb#e4U*n         parent.insertBefore(node, parent.firstChild);:~(\KE jp6}
    } else {
/J?w[.bw*D         parent.appendChild(node);b.[j&iX
    }
aS]/ceY@HT8KB }j4\4_/db)O

%tx N4}g1f]     实际上,像清单 1 一样,insertBefore() 函数已经被定义为,在参考节点为空时返回到 appendChild()。因此,您可以不使用上面的方法,而使用 清单 2 中的方法,或者跳过它们仅使用内置函数:
yR(@(ix U8svd
!D Q^8K1H5hQ z Y2s 清单 2. 插入和由前新增的正确方法
)N l _g+E function insertAfter(parent, node, referenceNode) {+V4h&vT}4eDR2L
    parent.insertBefore(node, referenceNode.nextSibling);LUX k)?4Z-la
}(wu3gJL
function prependChild(parent, node) {
(]v4n;r{1v     parent.insertBefore(node, parent.firstChild);$tY-Z.u9tI i
}
;m8V3VU6k0Qa.Hz
^b[5v:c+l(m-U     如果您刚刚接触 DOM 编程,有必要指出的是,虽然您可以使多个指针指向一个节点,但该节点只能存在于 DOM 树中的一个位置。因此,如果您想将它插入到树中,没必要先将它从树中移除,因为它会自动被移除。当重新将节点排序时,这种机制很方便,仅需将节点插入到新位置即可。

2008-5-14 09:17 麻雀杀手
根据这种机制,若想交换两个相邻节点(称为 node1 和 node2)的位置,可以使用下列方案之一:
iN-f}T"JX w /U5Za:i @8sm2D
node1.parentNode.insertBefore(node2, node1); r\J9RC/A8QI0YU3|
\2J&Px bw.r
node1.parentNode.insertBefore(node1.nextSibling, node1);
dA3C z`%[/PUk "|GWFfV
    还可以使用 DOM 做什么?
sE3fVe@/R\.N _d m+m trID
    Web 页面中大量应用了 DOM。若访问 bookmarklets 站点(参阅 参考资料),您会发现很多有创意的简短脚本,它们可以重新编排页面,提取链接,隐藏图片或 Flash 广告,等等。
&gP-{ jLu'a
0mn%U\ p5M9rk     但是,因为 Internet Explorer 没有定义 Node 接口常量(可以用于识别节点类型),所以您必须确保在遗漏接口常量时,首先为 Web 在 DOM 脚本中定义接口常量。
@r;hspV
V3z3t9ySE 清单 3. 确保节点被定义0a#wMt;Q({%c
if (!window['Node']) {
D;Ya{E9m     window.Node = new Object();$P V#m`#\U
    Node.ELEMENT_NODE = 1;
1QKC/x,DXTT-T     Node.ATTRIBUTE_NODE = 2;
a6t)t P8c-]{-Y;`     Node.TEXT_NODE = 3; zPTAm_f2k
    Node.CDATA_SECTION_NODE = 4;t/m7O+Aa;Q+x/j"_
    Node.ENTITY_REFERENCE_NODE = 5;$XN-KT9Jy5T
    Node.ENTITY_NODE = 6;"a"[l6`@6x
    Node.PROCESSING_INSTRUCTION_NODE = 7;
bXB q:`     Node.COMMENT_NODE = 8;
x3f0b2m#{6^     Node.DOCUMENT_NODE = 9;$O+a|:r]&l M]U
    Node.DOCUMENT_TYPE_NODE = 10;
i)KM:l+|p     Node.DOCUMENT_FRAGMENT_NODE = 11;:Y\l;uI5|XK
    Node.NOTATION_NODE = 12;
w)eO{a5`pw }
Qia O ?$x
lj\$c5Q1{-w     清单 4 展示如何提取包含在节点中的所有文本节点:Ych-R_$R |P0W
VnH7tzO
清单 4. 内部文本 VE)LL6f8aO
function innerText(node) {
a.` Y/hX b.w     // is this a text or CDATA node?r~+`~7@| D
    if (node.nodeType == 3 || node.nodeType == 4) {-MS)O#C ]#^%w Ys6k/_1o
        return node.data;
a%Z.x&xl     }RT`)F!{f/HwV
    var i;
5O;q,Sc2rBjC!e     var returnValue = [];
e `hV[*y|n)R)h     for (i = 0; i < node.childNodes.length; i++) {
&RVB6q&AH}C5P]A"`         returnValue.push(innerText(node.childNodes[i]));j*q6U+MT W&M)h
    }
_dtx R     return returnValue.join('');z0L'O7wn!z+d;T
}

2008-5-14 09:25 麻雀杀手
快捷方式Em"Ux%W

_zMHF]+uP     人们常常抱怨 DOM 太过冗长,并且简单的功能也需要编写大量代码。例如,如果您想创建一个包含文本并响应点击按钮的 <div> 元素,代码可能类似于:Bq.A"` j[i2a

t DB_rZ6V B+y X4dkZ 清单 5. 创建 <div> 的“漫长之路”'JOLLbZ(g
function handle_button() {
)k&c(c7P(x'Jt     var parent = document.getElementById('myContainer');2Dy-Y8ws
    var div = document.createElement('div');
#PalM-A&`_     div.className = 'myDivCSSClass';
C P7QU yH\y1k ]     div.id = 'myDivId';b-T{1@U
    div.style.position = 'absolute';(wG'O9Wahc$V
    div.style.left = '300px';%IpF5@+z9is^
    div.style.top = '200px';tLR8gJ3_
    var text = "This is the first text of the rest of this code";
;Q8p|r0{o     var textNode = document.createTextNode(text);
;`VX5l1w.d'l F,Y-`     div.appendChild(textNode);
a(xgu }lo0}     parent.appendChild(div);^]1tQK2Y\
}.f#o2wxlQ6c5A

3i8G vB+lK _^%q /tz.hA:v$JJ$^o`
"TCbhy!j$v4U6VR
    若频繁按照这种方式创建节点,键入所有这些代码会使您很快疲惫不堪。必须有更好的解决方案 —— 确实有这样的解决方案!下面这个实用工具可以帮助您创建元素、设置元素属性和风格,并添加文本子节点。除了 name 参数,其他参数都是可选的。(W8C X M,_:ZJ7Q
4UWzc,@;FE&p,hc
清单 6. 函数 elem() 快捷方式
_v*@,O S he[l'w function elem(name, attrs, style, text) {r1a D`#Vu
    var e = document.createElement(name);
$N9z/b;o n:F5JE8a*Q`     if (attrs) { N;}^*bw`*d*Jc
        for (key in attrs) {N)y.^ P{2v:]z_
            if (key == 'class') {E9J"d d(ggq
                e.className = attrs[key];e6~VV/M D_*~
            } else if (key == 'id') {
LOAH4kiO0`'p'`9?                 e.id = attrs[key];*Lt,{zM#dm
            } else {zw^k2W A
                e.setAttribute(key, attrs[key]);
d5P:Hqx             }8PBGNqOv"O8a
        }6Z.e qg:[
    },RUJrl.} n3ox
    if (style) {
;pZ+@&`kl         for (key in style) {
4Me&bF.[.n-H             e.style[key] = style[key];e6?%o%O([ DC9W
        }4O+K6Aln-g#h.fr
    }
3w$c+pU[ Dg     if (text) {
W};s%K;H6s         e.appendChild(document.createTextNode(text));5]Z*lxBJg
    }e%gqQ t7K5ve7K y
    return e;
(P6P/|7H3Y"k$K`4P,C8Y?T }
/di,@3LB E#p@|n4{ #yq:O$|4\"{1]
    使用该快捷方式,您能够以更加简洁的方法创建 清单 5 中的 <div> 元素。注意,attrs 和 style 参数是使用 JavaScript 文本对象而给出的。
(mnCs.~f@ 1p;dr U}e ? Uu
清单 7. 创建 <div> 的简便方法
^X|%{s-iR function handle_button() {9j5a UP6wR&Bc&Hz9E+P
    var parent = document.getElementById('myContainer');
ElP`-wg5d     parent.appendChild(elem('div',&hbq9E3wE$L
      {class: 'myDivCSSClass', id: 'myDivId'}7La z#aP4O
      {position: 'absolute', left: '300px', top: '200px'},){4o+nX tc b"O-I
      'This is the first text of the rest of this code'));2yl*~a;[Mx,rO WY
}
$k9oY fqvL
Nvqf'{&v4B:]0r     在您想要快速创建大量复杂的 DHTML 对象时,这种实用工具可以节省您大量的时间。模式在这里就是指,如果您有一种需要频繁创建的特定的 DOM 结构,则使用实用工具来创建它们。这不但减少了您编写的代码量,而且也减少了重复的剪切、粘贴代码(错误的罪魁祸首),并且在阅读代码时思路更加清晰。

2008-5-14 09:25 麻雀杀手
接下来是什么?(jX"u+LY&Vk
    DOM 通常很难告诉您,按照文档的顺序,下一个节点是什么。下面有一些实用工具,可以帮助您在节点间前后移动:
8o;n o kf3`0H~/G 'ic u!lQ |P.nt
清单 8. nextNode 和 prevNode
~2a w_@%}[UKR // return next node in document order6~)Jau ~4x
function nextNode(node) {:UoB m7{-~S+Z3az
    if (!node) return null;
"REji7?u     if (node.firstChild){
aP%S/b;OKnwX x&? U         return node.firstChild;
.S/b3dA5o+v9t]     } else {:@ {\ qNij([cW7J
        return nextWide(node);V4?KqbX+mO
    }
)Vh6C(XA-}/I:t } o#^km} I-S I%wJ
// helper function for nextNode()[[-Dpf\6W
function nextWide(node) {r2F KP0e8z;TS
    if (!node) return null;
8a UE1M\'\1A/}S%V     if (node.nextSibling) {
]RO%`'g?I+S W         return node.nextSibling;
$S P^!GM+C wD     } else {
`7X4Jfl!`8N-E!wC         return nextWide(node.parentNode);r1v:J_oR``N X
    } @ m9s$j;k
}
k2mt Q.w'IY,wu // return previous node in document orderPV-G N}+~ H0J
function prevNode(node) {$_}'Mg6D+|-S9A/uY4y
    if (!node) return null;
\1W)@U5IE4b     if (node.previousSibling) {
9err+K'|:q!L%| w       return previousDeep(node.previousSibling);
%m/P;m5\U*{(eO     }
^Q`&H4t&Jw     return node.parentNode;
!jQny,Oi'x8x4C }L)S"uu0OI-?q:]
// helper function for prevNode()
!^b&TI(s0Hq function previousDeep(node) {
]0S_)_I5ONz     if (!node) return null;
8f^$s Pr4^8L     while (node.childNodes.length) {T$u Jk3C:@1Q3M)s
        node = node.lastChild;GDXqF@$m6A
    }.e2C_}7|%A9PB.x.J
    return node;
L+p1X}:t{ }3z/Z`s!mX{;_&b;R
7|mY7P$H#l Ot(@4XY

V0WYxnN\
6@{o6?+U7p t     轻松使用 DOM
DX C3FA2qY     有时候,您可能想要遍历 DOM,在每个节点调用函数或从每个节点返回一个值。实际上,由于这些想法非常具有普遍性,所以 DOM Level 2 已经包含了一个称为 DOM Traversal and Range 的扩展(为迭代 DOM 所有节点定义了对象和 API),它用来为 DOM 中的所有节点应用函数和在 DOM 中选择一个范围。因为这些函数没有在 Internet Explorer 中定义(至少目前是这样),所以您可以使用 nextNode() 来做一些
J:l/c}.I3A Mq9p _ 类似的事情。
,UQ-s#UD:}|3j4T
@"S JWI Q3Y     在这里,我们的想法是创建一些简单、普通的工具,然后以不同的方式组装它们来达到预期的效果。如果您很熟悉函数式编程,这看起来会很亲切。Beyond JS 库(参阅 参考资料)将此理念发扬光大。oz(_j6Yy o
/W&`JuRMW4sYa
清单 9. 函数式 DOM 实用工具
yfn df"}Q // return an Array of all nodes, starting at startNode and
hKy*u3b eV @ // continuing through the rest of the DOM tree
M ~3W^f T a function listNodes(startNode) {6qh"qwM6u ?:o
    var list = new Array(); @)~)uF3v%o
    var node = startNode;
F*MUu,@,c `     while(node) {
d!Xa6D!CB(m         list.push(node);
1p I)M-Cy%b9~ t6vs$W         node = nextNode(node);
.fytx&o\+Z!D9i*SS     }&|j XX B T KN
    return list; V} ~/[7|7k5x$bTD
}#Sdgz,s}3q/?
// The same as listNodes(), but works backwards from startNode.ny9znS;yP!r
// Note that this is not the same as running listNodes() and/_4j4A\#B&U
// reversing the list.
po ty4[@1K"]3dM function listNodesReversed(startNode) {
Pv#lyI|     var list = new Array();}QtA:D-U
    var node = startNode;
-Y9B#`+F+bx O6?(I     while(node) {
rz8eC5v_&zh         list.push(node);'G~5C@tu3FHw0K
        node = prevNode(node);
5\S pv(n V up%R     }
.{!UoeW;c+[%J     return list;
xdj$u [ }R1y8b,E }0BC Q\` w.n0T
// apply func to each node in nodeList, return new list of results!G;[;Ympy6x)Z_
function map(list, func) {
2p,m.DJg     var result_list = new Array();9OM Q9z'}&`i_
    for (var i = 0; i < list.length; i++) {
i.F K;^ LPg{         result_list.push(func(list[i]));
.ic8\Tb&ftB     }oD[fD
    return result_list;_C~;D'_t{!F
}
$zLl7N5S0Y?_ k/X // apply test to each node, return a new list of nodes for which
&A q,|h+A$eziMAX V&\ // test(node) returns true
J&}*z Q_,nS4yn*z function filter(list, test) {F$Pp9|pv(?o\
    var result_list = new Array();
fbt&?lTPWI     for (var i = 0; i < list.length; i++) {
*Y y6rRH J8b         if (test(list[i])) result_list.push(list[i]);
#oc Zu5~%PT     }
'|N e'~5F(i+b;aBi     return result_list;9MM^q _9e2F"W2C
}

2008-5-14 09:26 麻雀杀手
清单 9 包含了 4 个基本工具。listNodes() 和 listNodesReversed() 函数可以扩展到一个可选的长度,这与 Array 的 slice() 方法效果类似,我把这个作为留给您的练习。另一个需要注意的是,map() 和 filter() 函数是完全通用的,用于处理任何 列表(不只是节点列表)。现在,我向您展示它们的几种组合方式。0}.@tTF'y.So

#CKkk;j*z 清单 10. 使用函数式实用工具
f_B {%r ]#G u // A list of all the element names in document order5[/Mm:u ^T:KLu%j
function isElement(node) {1W1m4MP+tcFb
    return node.nodeType == Node.ELEMENT_NODE;0F"PF I;cEC
}
+f6D hAn}kb function nodeName(node) {a$^d*wW aw(p"g
    return node.nodeName;
7reX9Z pR([W#`]7}u/@ O }
$WJ_:I)s }p&X var elementNames = map(filter(listNodes(document),isElement), nodeName);
Y;}TF8B@Na#\;vV // All the text from the document (ignores CDATA)h#K1P rgu*?
function isText(node) {|Z8\{f6ppR
    return node.nodeType == Node.TEXT_NODE;
']E(oJ U.UH5|/L,l }t pY6X F!T
function nodeValue(node) {
"`m3Tl7qs:O     return node.nodeValue;
'Z/JwCb }
/z&|!K C\(}K-O var allText = map(filter(listNodes(document), isText), nodeValue);a L.H MG-k{6R7v'T

7Ls+v a%L*l'Hu@
W(D"eq$@"B wN/b@(f U     您可以使用这些实用工具来提取 ID、修改样式、找到某种节点并移除,等等。一旦 DOM Traversal and Range API 被广泛实现,您无需首先构建列表,就可以用它们修改 DOM 树。它们不但功能强大,并且工作方式也与我在上面所强调的方式类似。
GM8[3lZ$k
3N!o;X@N,_     DOM 的危险地带
i C[Ss3z&KE     注意,核心 DOM API 并不能使您将 XML 数据解析到 DOM,或者将 DOM 序列化为 XML。这些功能都定义在 DOM Level 3 的扩展部分“Load and Save”,但它们还没有被完全实现,因此现在不要考虑这些。每个平台(浏览器或其他专业 DOM 应用程序)有自己在 DOM 和 XML间转换的方法,但跨平台转换不在本文讨论范围之内。
!Q'aXGo [
)[3{(S S zZ/\-a]     DOM 并不是十分安全的工具 —— 特别是使用 DOM API 创建不能作为 XML 序列化的树时。绝对不要在同一个程序中混合使用 DOM1 非名称空间 API 和 DOM2 名称空间感知的 API(例如,createElement 和 createElementNS)。如果您使用名称空间,请尽量在根元素位置声明所有名称空间,并且不要覆盖名称空间前缀,否则情况会非常混乱。一般来说,只要按照惯例,就不会触发使您陷入麻烦的临界情况。
f2d?_4C-G,O :E8VD2z4DN
    如果您一直使用 Internet Explorer 的 innerText 和 innerHTML 进行解析,那么您可以试试使用 elem() 函数。通过构建类似的一些实用工具,您会得到更多便利,并且继承了跨平台代码的优越性。将这两种方法混合使用是非常糟糕的。
vT(yUp_:Hq .@ Uiha"fQ#a
    某些 Unicode 字符并没有包含在 XML 中。DOM 的实现使您可以添加它们,但后果是无法序列化。这些字符包括大多数的控制字符和Unicode 代理对(surrogate pair)中的单个字符。只有您试图在文档中包含二进制数据时才会遇到这种情况,但这是另一种转向(gotcha)情况。
Z3P|H,a8a.qe BG
o3NFd4X Fy(Q ?;{ ]8w Yi
    结束语"J(s/v$a$v N
    我已经介绍了 DOM 能做的很多事情,但是 DOM(和 JavaScript)可以做的事情远不止这些。仔细研究、揣摩这些例子,看看是如何使用它们来解决可能需要客户端脚本、模板或专用 API 的问题。,En0K0zd:ll R^

:w?D*h}S     DOM 有自己的局限性和缺点,但同时也拥有众多优点:它内置于很多应用程序中;无论使用 Java 技术、Python 或 JavaScript,它都以相同方式工作;它非常便于使用 SAX;使用上述的模板,它使用起来既简洁又强大。越来越多的应用程序开始支持 DOM,这包括基于 Mozilla的应用程序、OpenOffice 和 Blast Radius 的 XMetaL。越来越多的规范需要 DOM,并对它加以扩展(例如,SVG),因此 DOM 时时刻刻就在您的身边。使用这种被广泛部署的工具,绝对是您的明智之举。

页: [1]


Powered by Discuz! Archiver 5.5.0  © 2001-2006 Comsenz Inc.