react 項目的一個ie8兼容性問題


來源:http://www.aliued.com/?p=3240

相信現在很多人在使用react+webpack做項目,然后通過babel來解決ES6/7的兼容性問題,對於ie8的兼容也有一些經驗和方案。不過今天在解決匯金賬房的ie8兼容過程中仍然遇到一個坑,同時發現現有資料的一些問題。

首先說一下兼容ie8的通用方案,大家一般是按照 Make your React app work in IE8 中的方法去做,網上也會搜到一些方法,比如添加 transform-es3-property-literals,transform-es3-member-expression-literal , add-module-exports 插件等,不過它們可能是不同時期的不同解決方案,實際上是在解決同一類問題,即es3環境對es5語法的兼容。我們來分析一下 Make your React app work in IE8 中提到的問題,其實主要分三類。

一、es3ify解決es3環境兼容

對於這個問題,主要是解決es3的保留字在es3環境下的正確使用,default是暴露最多的問題,因為大家都在寫export default xx。對於這個問題,目前比較快捷的方式就是使用es3ify,在webpack中就是添加es3ify-loader,代碼如下:

    module: { loaders: [{ test: /\.jsx?$/, loaders: ['es3ify-loader'], } ] } 

它主要做的事情就是對於一些保留字的使用做了es3兼容,以及一些額外的處理,比如去除數組尾部的多余逗號:

// babel轉換前 var a = { class: "haha" } a.class = "bb"; var arr = [1,2,3,]; //babel轉換后 var a = { "class": "haha" } a["class"] = "bb"; var arr = [1,2,3]; 

有了它,其它的一些插件transform-es3-property-literals,transform-es3-member-expression-literal,add-module-exports 就沒有必要了,你會發現這些插件就是在解決部分問題:

transform-es3-property-literals:保證在對象屬性中的保留字正確

// babel轉換前 var a = { class: "haha" //變動處 } a.class = "bb"; //babel轉換后 var a = { "class": "haha" } a.class = "bb"; //變動處 

transform-es3-member-expression-literal:保證在對象屬性訪問中的保留字正確

// babel轉換前 var a = { class: "haha" } a.class = "bb"; //變動處 //babel轉換后 var a = { class: "haha" } a["class"] = "bb";//變動處 

所以也會把export.default轉為export[“default”], 即解決了default不兼容問題。

add-module-exports:僅僅解決default的問題

二、babel-polyfill 解決缺失API問題

先跨過Object.defineProperty問題,因為那里是重要的坑點。而對於上面的這三個問題,實際上屬於同一類問題,即對一些ES6 API缺失的模擬。比如常見的Object.assign,Promise對象,fetch等等,這些可以通過統一引用“babel-polyfill”來解決,如果感覺“babel-polyfill”太重,也可以針對所需要的API自行引用對應的polyfill。polyfill的應用可以有兩種方式:

  1. npm包的方式,在編譯入口文件通過require(“babel-polyfill”)引入執行。
  2. 也可以在頁面上,業務js前引入babel的script標簽。

這里最后一個問題:

以及console對象的兼容問題就比較簡單,也都可以通過對應polyfill解決,就不多做解釋。

三、最麻煩的Object.defineProperty

這里整個問題的說明已經滯后,且有錯誤:

首先,這里說的 “Object.defineProperty 在IE8中不存在” 是錯的,而是IE8中有自己實現的Object.defineProperty,它的行為和標准不同,且只能接受DOM對象,如果傳入普通javascript對象會拋異常。詳細說明在這里Object.defineProperty

其次,babel會把 export(非import) 編譯成 Object.defineProperty的方式。相信添加這個問題的時候,babel確實存在這樣的轉換,具體的issues也有人提過 babel-export ,而提供的解決方案—-引入es5-shim和es5-sham在這種情況下是也確實是可行的。不過目前的babel版本已經不會有這種轉換(卻還存另一個轉換的坑),但是es5-shim和es5-sham的引用是必要的,因為它是解決通用性的es3環境下es5 API的缺失問題,就像babel-polyfill一樣,Object.defineProperty是其中的一個API。

以上是現有常規的兼容方案,很多人使用也沒有存在太多問題。

遇到的問題和排查

本以為按照這樣的指引,進行了babel轉換,引入es3ify,babel-polyfill,es5-shim/es5-sham,console-polifill就大功告成了,可惜ie8運行起來還是崩了。先是錯誤定位到babel-polyfill中,去掉babel-polyfill又定位到es5-sham中,報的錯誤都是異常未捕獲,且在IE8下調試很艱難。后來根據es5-sham壓縮代碼的拋異常位置,查看es5-sham的源碼,結合es5-sham的文檔說明,基本定位為Object.defineProperty的問題。

首先其拋異常的代碼在這段:

代碼會在supportsAccessors為false且hasGetter或者hasSetter時拋異常,邏輯上講就是如果當前js引擎不支持訪問器屬性,但是卻在屬性描述符中設置了get,set,那么就會拋出異常。supportsAccessors用於判斷當前js引擎是否支持訪問器屬性,它的判斷邏輯在這里:


實際就是用Object.prototype.hasOwnProperty(“defineGetter“)做判斷,“defineGetter”的兼容情況是只兼容IE11,具體查看 defineGetter說明 ,雖然ie9,ie10同樣不支持defineGetter,不過他們直接支持Object.defineProperty方法和get語法,無需sham,所以代碼並不會走到異常這里。實際上es5-sham官方文檔也提到對Object.defineProperty的polyfill會存在限制和fail的情況:

具體查看: es5-shim , 雖然其說明和代碼實現存在些差異,不過結論是明確的:ie8下訪問器屬性不支持,會拋異常;

基本明確了是用Object.defineProperty()設置訪問器屬性的問題,那么就向上查找到底是哪里使用了訪問器屬性設置,在編譯后的源代碼里搜索“ :get”查到了這段代碼:

根據上面關鍵字syncHistoryWithStore,routerReducer等初步判斷是在react-router-redux中,node_modules查看react-router-redux源碼,果然,其lib中index.js里有很多對export的訪問器屬性設置。再查看對應src/index.js文件,兩份代碼如下:

//src/index.js export syncHistoryWithStore from './sync' export { LOCATION_CHANGE, routerReducer } from './reducer' export { CALL_HISTORY_METHOD, push, replace, go, goBack, goForward, routerActions } from './actions' export routerMiddleware from './middleware' 

babel編譯后部分代碼:

//lib/index.js 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.routerMiddleware = exports.routerActions = exports.goForward = exports.goBack = exports.go = exports.replace = exports.push = exports.CALL_HISTORY_METHOD = exports.routerReducer = exports.LOCATION_CHANGE = exports.syncHistoryWithStore = undefined; var _reducer = require('./reducer'); Object.defineProperty(exports, 'LOCATION_CHANGE', { enumerable: true, get: function get() { return _reducer.LOCATION_CHANGE; } }); Object.defineProperty(exports, 'routerReducer', { enumerable: true, get: function get() { return _reducer.routerReducer; } }); 

所以確定了是babel編譯問題,又回到了上面提到的,babel對直接的export會轉碼為Object.defineProperty,但是它們修復了這個問題,不過好死不死的,對於export 和 import結合的寫法—-export xx from ‘xxx’ 還是會轉碼為Object.defineProperty對exports進行屬性設置,更加嚴重的是,之前的Object.defineProperty設置的是數據屬性,直接指定的value,但是這次的Object.defineProperty 腦殘般的設置了訪問器屬性,如此一來es5-sham已經沒有辦法解決(上面已經說明)。

所以最終的排查結果就是主要3點:

  1. ie8不支持設置訪問器屬性,即便是引了es5-shim;
  2. Babel 會把export xxx from ‘xx’ 語法轉碼為訪問器屬性設置的exports對象。
  3. 而react-router-react的index.js 偏偏用了export xxx from ‘xx’這樣的語法。

解決方案

最直接的解決辦法就是Babel修復這種轉碼方式,目前已經有人提過issue,但是尚未解決:issue

其次react-router-redux 不要用export xx from ‘xx’的方式,也有人提過issue: issue

不過目前為了解決線上bug,肯定沒有辦法等待它們的修復,且尚未找到一種ie8下兼容訪問器屬性設置的方法,所以最終此項目的解決方案是修改react-router-react的源碼,把其中的export xx from ‘xx’的語法改成分開的方式,比如:

//修改前 export syncHistoryWithStore from './sync' //修改后 import syncHistoryWithStore from './sync'; export {syncHistoryWithStore} ; 

然后所有引用react-router-react的地方改為對src/index.js的引用,在項目中自己重新編譯,而不使用lib中編譯好的版本。

總結

原本對webpack和babel了解的就不是很多,差不多用了一天的時間來排查這個問題,感覺react的項目想要支持ie8坑還會很多,其中包括babel,webpack,第三方庫對ie8的兼容支持問題並不良好,且現在react@15.x.x版本已經放棄ie8。所以目前實踐的ie8兼容方案是:

  1. webpack 進行babel對ES6,7語法的轉碼。
  2. webpack 引用es3ify-loader 解決es3語法兼容問題。
  3. 全局引用babel-polyfill,es5-shim/es5-sham,console-polyfill,JSON的polyfill等
  4. 不要在代碼中用Object.defineProperty設置訪問器屬性,若第三方包中有,找到,改之。

各位還遇到哪些問題可以一起討論,積累經驗,整個排查過程也對很多知識理解的深刻了一點。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM