以前對瀏覽器兼容性問題只是大概知道一些點,沒想到這次真正著手去做的時候,還是碰到了很多問題。剛開始的時候一邊解決問題,一邊想著:用 IE8 的都是神經病,到后來,我發現完了,I LOVE IE。
0x00 起源
在這次做小蜜 PC 版的時候,由于早于 PC 版,無線版已經重新設計了全新版,做了很多架構上的優化調整。所以在做的時候把無線版的前端架構拿了過來,主要的考慮就是品牌和功能保持跟無線版統一的同時,技術上也可相互支持以及組件復用。
無線版整個架構設計是同事做的,技術上主要采用 ES6 + Webpack + Babel 的方式,由于項目的獨特性和特殊需求,并沒有使用任何框架,只引入 zepto 作為一個標準支撐庫。
而 PC 版的架構跟無線版基本保持一致,主要是把 zepto 換成了 jQuery。
下面是一些基本的開發依賴:
{
"devDependencies": {
"babel-core": "~6.3.15",
"babel-loader": "~6.2.0",
"babel-preset-es2015": "~6.3.13",
"babel-preset-stage-0": "~6.3.13",
"babel-runtime": "~6.3.13",
"extract-text-webpack-plugin": "~0.9.1",
"less-loader": "~2.2.1",
"nunjucks-loader": "~1.0.7",
"style-loader": "~0.10.2",
"webpack": "~1.12.9",
"webpack-dev-server": "^1.10.1"
}
}
0x01 polyfill
由于 Babel 默認只轉換轉各種 ES2015 語法,而不轉換新的 API,比如 Promise,以及 Object.assign、Array.from 這些新方法,這時我們需要提供一些 ployfill 來模擬出這樣一個提供原生支持功能的瀏覽器環境。
主要有兩種方式:babel-runtime
和 babel-polyfill
。
babel-runtime
babel-runtime 的作用是模擬 ES2015 環境,包含各種分散的 polyfill 模塊,我們可以在自己的模塊里單獨引入,比如 promise:
import 'babel-runtime/core-js/promise'
它們不會在全局環境添加未實現的方法,只是這樣手動引用每個 polyfill 會非常低效,我們可以借助 Runtime transform
插件來自動化處理這一切。
首先使用 npm 安裝:
npm install babel-plugin-transform-runtime --save-dev
然后在 webpack 配置文件的 babel-loader 增加選項:
loader: ["babel-loader"],
query: {
plugins: [
"transform-runtime"
],
presets: ['es2015', 'stage-0']
}
babel-polyfill
而 babel-polyfill
是針對全局環境的,引入它瀏覽器就好像具備了規范里定義的完整的特性,一旦引入,就會跑一個 babel-polyfill
實例。用法如下:
1.安裝 babel-polyfill
npm install babel-polyfill --save
2.在入口文件中引用:
import 'babel-polyfill'
小結:
其實做到這些,在大部分瀏覽器就可以正常跑了,但我們做的是一個用戶環境很不確定的產品,對一些年代久遠但又不容忽視的運行環境,比如 IE8,我們做的還不夠。
接下來將開始講述我們在兼容性方面遇到的一些問題,和解決方法。
0x02 開始在 IE8 運行
最開始做的時候并沒有針對 IE 做一些兼容性方面的處理,結果在 IE8 上一跑一堆問題。
第一步,我們把 jQuery
換成 1.12.1 ,因為 2.X 已經不再支持 IE8。
但并沒有像我們想象中的那樣,只是簡單換一下 jQuery
版本就可以正常運行了。
0x03 default or catch
這是遇到的第一個問題。在兼容性測試過程中,對下面的代碼:
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
或者這種:
module.exports = _main2.default;
在 IE8 下會直接報”缺少標識符、字符串或數字”的錯。
我們得在對象的屬性上加 ''
才可以。就像下面這樣:
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { ‘default’: obj };
}
module.exports = _main2['default'];
至于原因,并不是 IE8 下對象的屬性必須得加 ''
才行,而是 default
的問題,作為一個關鍵字,同樣的問題還包括 catch
。
這兩種情況,可以通過使用 transform-es3-property-literals
和 transform-es3-member-expression-literals
這兩個插件搞定。
總之,在平時寫代碼的時候避免使用關鍵字,或者保留字作為對象的屬性值,尤其是在習慣不加引號的情況下。相關討論:Allow reserved words for properties
0x04 es5-shim、es5-sham
為了兼容像 IE8 這樣的老版本瀏覽器,我們引入 es5-shim
作為 polyfill。
但在遇到 Object.defineProperty
仍提示 "對象不支持此操作"
As currently implemented, the Object.defineProperty shim will not install on IE8 because IE8 already has such a method. However, the built-in IE8 method only works when applied to DOM objects.
其實 es5-shim 明確說明,這個方法的 polyfill 在 IE8 會失敗,因為 IE8 已經有個同名的方法,但只是用于 DOM 對象。
同樣的問題還包括 Object.create
,上述問題可以再引入 es5-sham 解決.
0x05 addEventListener
項目中有部分代碼直接使用 addEventListener
這個 API,但在 IE8 下的事件綁定并不是這個方法。
這個問題很容易解決,也無需去寫額外的 polyfill。我們已經把 jQuery 換成 1.x,所以只需把代碼中 addEventListener
換成 jQuery
的寫法就 Okay 了。
jQuery
其實為我們封裝了很多 API,并做了很多兼容性的封裝,類似的只要使用封裝好的就可以了。
0x06 無法獲取未定義或 null 引用的屬性
這個問題是在特定場景下【轉人工】出現的,出現問題的不是 IE8,而是 IE9 和 IE10。
原因是 ocs 實例創建失敗,因為沒有調用父類的構造函數。
通過安裝 transform-es2015-classes
和 transform-proto-to-assign
解決。
在配置項加上這兩個插件的配置:
{
"plugins": [
["transform-es2015-classes", { "loose": true }],
"transform-proto-to-assign"
]
}
0x07 postMessage
雖然 postMessage
是 HTML5 的特性,但 IE8 和 Firefox3 很早就實現了這個 API,當然,跟后來的標準并不一致。這其實也不能怪 IE8。
The postMessage method is supported in Internet Explorer from version 8, Firefox from version 3 and Opera from version 9.5.
我們可能會這樣去使用:
parent.postMessage({success: 'ok', name: ‘mirreal’}, ‘*’);
但是為了兼容 IE8,我們得轉成字符串:
parent.postMessage(JSON.stringify({success: 'ok', name: "mirreal"}), ‘*’);
另外一個需要注意的點是:在 IE8 下 window.postMessage
是同步的。
window.postMessage is syncronouse in IE 8
var syncronouse = true;
window.onmessage = function () {
console.log(syncronouse); // 在 IE8 下會在控制臺打印 true
};
window.postMessage('test', '*');
syncronouse = false;
0x08 IE8/IE9 的控制臺
遇到一個奇怪的問題,在剛開始遇到的時候(其實搞清楚原因,好像也挺正常的),小蜜在 IE8 IE9 無法加載。在 IE8 那個古老瀏覽器的左下角,好像也是唯一會在頁面提示腳本錯誤的瀏覽器,提示 script error
。
第一反應就是應該又是某個函數在 IE 下不支持,準備打開控制臺看看到底哪里報錯,結果卻什么事都沒有了,頁面竟然順暢地加載出來了,這下該怎么調試好呢?
開始思考:什么東西是依賴控制臺而存在的,到底會是什么呢。。。其實就是控制臺本身。
原因就是我們在代碼中添加了一些控制信息會打印在控制臺,而 IE8/IE9 要開啟 IE Dev Tools 才能使用 console
對象。
切忌把 IE8/9 想成 Chrome/Firefox,以為永遠有 window.console
可用.終于,IE10 改邪歸正,console
不再像段譽的六脈神劍時有時無。
console.log is there in IE8, but the console object isn't created until you open DevTools. Therefore, a call to console.log may result in an error, for example if it occurs on page load before you have a chance to open the dev tools.
但只要 IE8/9 還在一天,console 檢查還是不能少的
事實上,IE8/9 從未死去,所以
就像這樣:
if (window.console) {
console.log('log here');
}
要是有一堆 console.log
, console.count
, console.error
, console.time
, console.profile
,... 這樣去寫,那還不把人寫到惡心死。
寫個簡單的 console polyfill 吧,檢測是否存在 console
,不存在可以常見一個同名的空方法達到不報錯的目的。當然,生產環境的代碼其實也不會有那么多奇奇怪怪的 console
。
0x09 定義文檔兼容性
X-UA-Compatible
當初是針對 IE8 新加的一個配置。用于為 IE8 指定不同的頁面渲染模式,比如使用 IE7 兼容模式,或者是采用最新的引擎。
現在基本也不需要前者的降級模式,更多的是寫入 IE=edge
支持最新特性。而 chrome=1
則會激活 Google Chrome Frame,前提是你的 IE 安裝過這個插件。
有什么用呢,當然有用,有些 API 是作為新特性存在于 IE8 中的,比如 JSON
,不開啟的話就用不了。
為什么要用 X-UA-Compatible?
在 IE8 剛推出的時候,很多網頁由于重構的問題,無法適應較高級的瀏覽器,所以使用 X-UA-Compatible
強制 IE8 采用低版本方式渲染。
比如:使用下面這段代碼后,開發者無需考慮網頁是否兼容 IE8 瀏覽器,只要確保網頁在 IE6、IE7 下的表現就可以了。
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
而這段代碼:
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
IE=edge
告訴 IE 使用最新的引擎渲染網頁,chrome=1
則可以激活 Chrome Frame[1]。
0x0a 條件注釋 or 條件編譯
最后說說 IE 的條件注釋,用法如下:
! [if !IE] The NOT operator. This is placed immediately in front of the feature, operator, or subexpression to reverse the Boolean meaning of the expression.
lt [if lt IE 5.5] The less-than operator. Returns true if the first argument is less than the second argument.
lte [if lte IE 6] The less-than or equal operator. Returns true if the first argument is less than or equal to the second argument.
gt [if gt IE 5] The greater-than operator. Returns true if the first argument is greater than the second argument.
gte [if gte IE 7] The greater-than or equal operator. Returns true if the first argument is greater than or equal to the second argument.
( ) [if !(IE 7)] Subexpression operators. Used in conjunction with boolean operators to create more complex expressions.
& [if (gt IE 5)&(lt IE 7)] The AND operator. Returns true if all subexpressions evaluate to true
| [if (IE 6)|(IE 7)] The OR operator. Returns true if any of the subexpressions evaluates to true.
另外一個類似的東西是在 Javascript 中的條件編譯(conditional compilation)。我們可以使用這段簡單的代碼來做瀏覽器嗅探:
var isIE = /*@cc_on!@*/false
在其他瀏覽器中,false 前的被視為注釋,而在 IE 中,/*@cc_on .... @*/
之間的部分可以被 IE 識別并作為程序執行,同時啟用 IE 的條件編譯。
常用變量如下:
* @_win32 如果在 Win32 系統上運行,則為 true。
* @_win16 如果在 Win16 系統上運行,則為 true。
* @_mac 如果在 Apple Macintosh 系統上運行,則為 true。
* @_alpha 如果在 DEC Alpha 處理器上運行,則為 true。
* @_x86 如果在 Intel 處理器上運行,則為 true。
* @_mc680x0 如果在 Motorola 680x0 處理器上運行,則為 true。
* @_PowerPC 如果在 Motorola PowerPC 處理器上運行,則為 true。
* @_jscript 始終為 true。
* @_jscript_build 包含 JavaScript 腳本引擎的生成號。
* @_jscript_version 包含 major.minor 格式的 JavaScript 版本號。
Internet Explorer 11 之前的所有版本的 Internet Explorer 都支持條件編譯。 從 Internet Explorer 11 標準模式開始,Windows 8.x 應用商店應用不支持條件編譯。
后:
之前一直在做移動端的開發,沒想到做 PC 端也會遇到這么多的兼容性問題。不同于移動端設備的繁雜和不確定性,PC 版的兼容更側重于對特定瀏覽器的特性的了解,相比而言更為明確,而非因為某一款手機的詭異表現。
參考文檔:
Allow reserved words for properties
IE8 defineProperty/getOwnPropertyDescriptor clash with shim
babel-plugin-transform-runtime definitions
super() not calling parent's constructor on IE9