Netscape 最初開發 Mozilla 瀏覽器的時候,明智地決定支援 W3C 標準。因此,Mozilla 和 Netscape Navigator 4.x 以及 Microsoft Internet Explorer 遺留代碼不完全向後相容,比如後面將提到 Mozilla 不支援 <layer>
。Internet Explorer 4 這些在 W3C 標準的概念出現之前建立的瀏覽器繼承了很多古怪之處。本文中將討論 Mozilla 的特殊模式,它為 Internet Explorer 和其他遺留瀏覽器提供了強大的 HTML 向後相容功能。
我還將討論 Mozilla 支援的非標準技術,如 XMLHttpRequest 和富文本編輯,因為當時 W3C 還沒有對應的標準。其中包括:
- HTML 4.01 和 XHTML 1.0/1.1
- 階層式樣式表(CSS):CSS Level 1、CSS Level 2 以及 CSS Level 3 的部分。
- 文件物件模型(DOM):DOM Level 1、 DOM Level 2 和 DOM Level 3 的部分
- 數學標記語言:MathML Version 2.0
- 可延伸標記語言 (XML)(XML):XML 1.0、Namespaces in XML、Associating Style Sheets with XML Documents 1.0、Fragment Identifier for XML
- XSL 轉換:XSLT 1.0
- XML 路徑語言:XPath 1.0
- 資源描述架構:RDF
- 簡易物件存取通訊協定 (SOAP):SOAP 1.1
- ECMA-262 修訂版 3(JavaScript 1.5):ECMA
通用的跨瀏覽器編碼技巧
雖然存在 Web 標準,但不同瀏覽器的行為並不完全相同(實際上同一個瀏覽器在不同的平台上行為也不相同)。很多瀏覽器,如 Internet Explorer 依然支援 W3C 之前的、從未在 W3C 符合瀏覽器中獲得廣泛支援的 API。
深入討論 Mozilla 和 Internet Explorer 的區別之前,首先介紹一下使 Web 應用程式具備可擴充性以便日後增加新瀏覽器支援的一些基本方法。
因為不同的瀏覽器有時會為同樣的功能使用不同的 API,因此經常會在代碼中看到很多 if() else()
塊,來區別對待不同的瀏覽器。下面的代碼塊用於 Internet Explorer:
. . . var elm; if (ns4) elm = document.layers["myID"]; else if (ie4) elm = document.all["myID"]; |
上述代碼不具備可擴充性,如果需要支援新的瀏覽器,必須修改 Web 應用程式中所有這樣的代碼塊。
避免為新瀏覽器重新編碼最簡單的辦法就是抽象功能。不要使用層層嵌套的 if() else()
塊,把通用的任務抽象成單獨的函數可以提高效率。這樣不但代碼更易於閱讀,還便於增加新客戶機支援:
var elm = getElmById("myID"); function getElmById(aID){ var element = null; if (isMozilla || isIE5) ?element = document.getElementById(aID) else if (isNetscape4) element = document.layers[aID] else if (isIE4) element = document.all[aID]; return element; } |
上述代碼仍然存在瀏覽器嗅探 或者檢測使用者使用何種瀏覽器的問題。瀏覽器嗅探一般通過使用者代理程式完成,比如:
Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.5) Gecko/20031016 |
雖然使用使用者代理程式來嗅探瀏覽器提供了所用瀏覽器的詳細資料,但是出現新的瀏覽器版本時處理使用者代理程式的代碼可能出錯,因而需要修改代碼。
如果瀏覽器的類型無關緊要(假設禁止不支援的瀏覽器訪問 Web 應用程式),最好通過瀏覽器本身的能力來嗅探。一般可以通過測試需要的 JavaScript 功能來完成。比如,與其使用:
不如用:
if (document.getElementById) |
這樣不用任何修改,在其他支援該方法的瀏覽器如 Opera 或 Safari 上也能工作。
但是如果準確性很重要,比如要驗證瀏覽器是否滿足 Web 應用程式的版本要求或者嘗試避免某個 bug,則必須使用使用者代理程式嗅探。
JavaScript 還允許使用內嵌條件陳述式,有助於提高代碼的可讀性:
var foo = (condition) ? conditionIsTrue : conditionIsFalse; |
比如,要檢索一個元素,可以用如下代碼:
function getElement(aID){ return (document.getElementById) ? document.getElementById(aID) : document.all[aID];} |
Mozilla 和 Internet Explorer 的區別
首先討論 Mozilla 和 Internet Explorer 在 HTML 行為方式上的區別。
工具提示
遺留瀏覽器在 HTML 中引入了工具提示,在連結上顯示 alt
屬性作為工具提示的內容。最新的 W3C HTML 規範增加了 title
屬性,用於包含連結的詳細說明。現代瀏覽器應該使用 title
屬性顯示工具提示,Mozilla 僅支援用該屬性顯示工具提示而不能用 alt
屬性。
實體
HTML 標籤可以包含多種實體,W3 標準體 專門作了規定。可以通過數字或者字元引用來引用這些實體。比如,可以用 #160 或者等價的字元引用
來引用空白字元
。
一些舊式瀏覽器,如 Internet Explorer,有一些怪異的地方,比如允許用正常常值內容替換實體後面的分號(;
):
Mozilla 將把上面的  
呈現為空白格,雖然違反了 W3C 規範。如果後面緊跟著更多字元,瀏覽器就不能解析  
,如:
這樣的代碼在 Mozilla 中無效,因為違反了 W3 標準。為了避免瀏覽器的差異,應堅持使用正確的形式(
)。
DOM 差異
文件物件模型(DOM)是包含文件項目的樹狀結構。可以通過 JavaScript API 來操縱它,對此 W3C 已有標準。但是在 W3C 標準化之前,Netscape 4 和 Internet Explorer 4 以類似的方式實現了這種 API。Mozilla 僅實現了 W3C 標準不支援的那些遺留 API。
訪問元素
未按照跨瀏覽器的方式檢索元素的引用,應使用 document.getElementById(aID)
,該方法可用於 Internet Explorer 5.5+、Mozilla,是 DOM Level 1 規範的一部分。
Mozilla 不支援通過 document.elementName
甚至按照元素名來訪問元素,而 Internet Explorer 則支援這種方法(也稱為全域名稱空間汙染)。Mozilla 也不支援 Netscape 4 的 document.layers
方法和 Internet Explorer 的 document.all
方法。除了 document.getElementById
可以檢索元素之外,還可用 document.layers
和 document.all
獲得具有特定標籤名稱的全部文件項目列表,比如所有的 <div>
元素。
W3C DOM Level 1 使用 getElementsByTagName()
方法獲得所有相同標籤名的元素的引用。該方法在 JavaScript 中返回一個數組,可用於 document
元素,也可用於其他節點只檢索對應的子樹。要獲得 DOM 樹中所有元素的列表,可使用 getElementsByTagName(*)
。
表 1 中列出了 DOM Level 1 方法,大部分用於把元素移動到特定位置或切換其可視性(菜單、動畫)。Netscape 4 使用 <layer>
標籤(Mozilla 不支援)作為可以任意定位的 HTML 元素。在 Mozilla 中,可使用 <div>
標籤定位元素,Internet Explorer 也用它,HTML 規範中也包含它。
表 1. 用於訪問元素的方法
方法 |
說明 |
document.getElementById( aId ) |
返回具有指定 ID 的元素的引用。 |
document.getElementsByTagName( aTagName ) |
返迴文檔中具有指定名稱的元素數組。 |
遍曆 DOM
Mozilla 通過 JavaScript 支援遍曆 DOM 樹的 W3C DOM API(如表 2 所示)。文檔中每個節點都可使用這些 API 方法,可以在任何方向上遍曆樹。Internet Explorer 也支援這些 API,還支援原來用於遍曆 DOM 樹的 API,比如 children
屬性。
表 2. 用於遍曆 DOM 的方法
屬性/方法 |
說明 |
childNodes |
返回元素所有子節點的數組。 |
firstChild |
返回元素的第一個子節點。 |
getAttribute( aAttributeName ) |
返回指定屬性的值。 |
hasAttribute( aAttributeName ) |
返回一個 Boolean 值表明當前節點是否包含指定名稱的屬性。 |
hasChildNodes() |
返回一個布爾指表明當前節點是否有子節點。 |
lastChild |
返回元素的最後一個子節點。 |
nextSibling |
返回緊接於當前節點之後的節點。 |
nodeName |
用字串返回當前節點的名稱。 |
nodeType |
返回當前節點的類型。
值 |
說明 |
1 |
元素節點 |
2 |
屬性節點 |
3 |
文本節點 |
4 |
CDATA 選擇節點 |
5 |
實體引用節點 |
6 |
實體節點 |
7 |
處理指示節點 |
8 |
注釋節點 |
9 |
文檔節點 |
10 |
文件類型節點 |
11 |
文檔片斷節點 |
12 |
符號節點 |
|
nodeValue |
返回當前節點的值。對於包含文本的節點,如文本和注釋節點返回其字串值。對於屬性節點返回屬性值。其他節點返回 null 。 |
ownerDocument |
返回包含當前節點的 document 對象。 |
parentNode |
返回當前節點的父節點。 |
previousSibling |
返回當前節點之前的相鄰節點。 |
removeAttribute( aName ) |
從當前節點中刪除指定的屬性。 |
setAttribute( aName, aValue ) |
設定指定屬性的值。 |
Internet Explorer 有一種非標準的特殊行為,這些 API 很多跳過(比如)新行字元產生的空白文本節點。Mozilla 則不跳過,因此有時候需要區分這些節點。每個節點都有一個 nodeType
屬性指定了節點類型。比如,元素節點類型是 1,文本節點是 3,而注釋節點是 8。僅處理元素節點最好的辦法是遍曆所有子節點,然後處理那些 nodeType 為 1 的節點:
HTML: <div id="foo"> <span>Test</span> c </div> JavaScript: var myDiv = document.getElementById("foo"); var myChildren = myXMLDoc.childNodes; for (var i = 0; i < myChildren.length; i++) { if (myChildren[i].nodeType == 1){ // element node } } |
產生和操縱內容
Mozilla 支援向 DOM 動態增加內容的遺留方法,如 document.write
、document.open
和 document.close
。Mozilla 也支援 Internet Explorer 的 InnerHTML
方法,該方法基本上可以在任何節點上使用。但是不支援 OuterHTML
(圍繞著元素委任標記,標準中也沒有等價的方法)和 innerText
(設定節點的文本值,在 Mozilla 中可使用 textContent
)。
Internet Explorer 有一些非標準的、Mozilla 不支援的內容操作方法,包括檢索值、插入文本以及鄰近某個節點插入元素,比如 getAdjacentElement
和 insertAdjacentHTML
。表 3 說明了 W3C 標準和 Mozilla 操縱內容的方法,這些方法適用於任何 DOM 節點。
表 3. Mozilla 用於操縱內容的方法
方法 |
說明 |
appendChild( aNode ) |
建立新的子節點。返回建立子節點的引用。 |
cloneNode( aDeep ) |
建立調用節點的副本並返回。如果 aDeep 為 true,則複製該節點的整個子樹。 |
createElement( aTagName ) |
建立並返回一個 aTagName 指定類型的無父 DOM 節點。 |
createTextNode( aTextValue ) |
建立並返回一個新的無父 DOM 文本節點,值由 aTextValue 指定。 |
insertBefore( aNewNode, aChildNode ) |
在 aChildNode 前插入 aNewNode,前者必須是當前節點的子節點。 |
removeChild( aChildNode ) |
刪除 aChildNode 並返回對它的引用。 |
replaceChild( aNewNode, aChildNode ) |
用 aNewNode 替換 aChildNode 並返回被刪除節點的引用。 |
文檔片斷
出於效能方面的原因,可以在記憶體中建立文檔而不是處理已有文檔的 DOM。DOM Level 1 Core 引入了文檔片斷,這是一種輕型文檔包含一般文檔介面的一個子集。比如沒有 getElementById
但是有 appendChild
。很容易向已有文檔添加文檔片斷。
Mozilla 使用 document.createDocumentFragment()
建立文檔片斷,該方法返回一個空的文檔片斷。
但是,Internet Explorer 的文檔片斷實現沒有遵循 W3C 標準,僅僅返回一般的文檔。
JavaScript 差異
Mozilla 和 Internet Explorer 的多數差異都和 JavaScript 有關。但問題通常源自瀏覽器向 JavaScript 公開的 API,比如 DOM 鉤子。兩種瀏覽器在核心 JavaScript 上區別不大,遇到的問題通常和時間有關。
JavaScript date 差異
Date
惟一的區別是 getYear
方法。根據 ECMAScript 規範(這是 JavaScript 所遵循的規範),該方法沒有解決千年問題,在 2004 年運行 new Date().getYear()
將返回“104”。根據 ECMAScript 規範,getYear
返回的年份減去 1900 最初是為了在 1998 年返回“98”。ECMAScript Version 3 廢止了 getYear
,用 getFullYear()
代替。Internet Explorer 修改了 getYear()
使其和 getFullYear()
類似,消除了千年問題,而 Mozilla 堅持採用標準的行為方式。
JavaScript 執行差異
不同的瀏覽器執行 JavaScript 的方式是不同的。比如,下列代碼假設 script
塊執行的時候 div
節點已經存在於 DOM 中:
... <div id="foo">Loading...</div> <script> document.getElementById("foo").innerHTML = "Done."; </script> |
但是並不能保證這一點。為了保證所有的元素已經存在,應該對 <body>
元素使用 onload
事件處理常式:
<body onload="doFinish()"> <div id="foo">Loading...</div> <script> function doFinish() { var element = document.getElementById("foo"); element.innerHTML = "Done."; } </script> ... |
這類與時間有關的問題也和硬體有關,速度慢的系統可能會發現速度快的系統隱藏起來的 bug。一個具體的例子是 window.open
,它開啟新的視窗:
<script> function doOpenWindow(){ var myWindow = window.open("about:blank"); myWindow.location.href = "http://www.ibm.com"; } </script> |
這段代碼的問題在於 window.open
是非同步,在視窗開啟之前沒有阻塞 JavaScript 的執行。因此,window.open
後面的行有可能在新視窗開啟之前執行。可以在新視窗的 onload
處理常式中解決這個問題,然後回調開啟它的視窗(使用 window.opener
)。
JavaScript 產生 HTML 的差別
JavaScript 可以通過 document.write
即時用字串產生 HTML。主要的問題在於,如果內嵌在 HTML 文檔(因此也在一個 <script>
標籤中)的 JavaScript 產生的 HTML 又包含 <script>
標籤怎麼辦。如果文檔採用 嚴格呈現模式,就會把字串中的 </script>
解釋成外層 <script>
的結束標籤。下面的代碼很好地說明了這一點:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> ... <script> document.write("<script>alert("Hello")</script>") </script> |
由於該頁面採用strict 模式,Mozilla 解析器就會看到第一個 <script>
並解析它直到發現第一個結束標籤,即第一個 </script>
。這是因為解析器不知道 JavaScript(或者其他任何語言)何時採用strict 模式。在特殊模式下,解析器在解析的過程中分析 JavaScript(因而降低了速度)。Internet Explorer 總是採用特殊模式,因此不真正支援 XHTML。為了在 Mozilla 中使用strict 模式,需要將字串分解成兩部分:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> ... <script> document.write("<script>alert("Hello")</" + "script>") </script> |
調試 JavaScript
Mozilla 提供了多種方法調試為 Internet Explorer 建立的應用程式中的 JavaScript 相關問題。第一個工具是內建的 JavaScript 控制台, 1 所示,它記錄了錯誤和警告資訊。在 Mozilla 中選擇 Tools -> Web Development -> JavaScript Console,或者在 Firefox(來自 Mozilla 的獨立瀏覽器產品)中選擇 Tools -> JavaScript Console 就能開啟它。
圖 1. JavaScript 控制台
JavaScript 控制台可以顯示完整的日誌列表,也可以分別顯示錯誤、警告和訊息。圖 1 中的錯誤訊息表明,aol.com 第 95 行訪問的變數 is_ns70 不存在。單擊該連結可以開啟 Mozilla 內部的查看原始碼視窗,反白出錯的一行。
控制台還允許對 JavaScript 求值。要計算輸入的 JavaScript 文法,在輸入欄位中輸入 1+1
然後按 Evaluate,結果 2 所示。
圖 2. JavaScript 控制台求值
Mozilla 的 JavaScript 引擎內建了對調試的支援,從而為 JavaScript 開發人員提供了強大的工具。圖 3 所示的 Venkman 是一種強大的跨平台 JavaScript 調試器,它與 Mozilla 整合在一起。它通常和 Mozilla 發行包捆綁在一起,可以通過選擇 Tools -> Web Development -> JavaScript Debugger 開啟它。Firefox 沒有捆綁這個調試器,但是可以從 http://www.mozilla.org/projects/venkman/ 下載安裝。還可以在開發頁面上找到相關教程,開發頁面的 URL 為 http://www.hacksrus.com/~ginda/venkman/。
圖 3. Mozilla 的 JavaScript 調試器
JavaScript 調試器可以調試在 Mozilla 瀏覽器視窗中啟動並執行 JavaScript。它支援斷點管理、查看調用棧和變數/對象檢查這樣的標準調試特性。所有特性都可通過使用者介面或者調試器的互動控制台來訪問。通過控制台,可以在和調試的 JavaScript 代碼同一範圍內執行任何 JavaScript。