Vue 相容 ie9 的全面解決方案

TerryZ發表於2018-06-19

前言

背景情況

  • vue - 2.5.11
  • vue-cli 使用模板 webpack-simple
  • http請求:axios

Vue 官方對於 ie 瀏覽器版本相容情況的描述是 ie9+,即是 ie9 及更高的版本。經過測試,Vue 的核心框架 vuejs 本身,以及生態的官方核心外掛(VueRouter、Vuex等)均可以在 ie9 上正常使用。

Vue 作者尤雨溪對於 Vue 的學習建議 中有提及為了將專案更好的生態化/工程化,要儘可能學習及使用新的 ECMAScript 規範。目前 ES6/ES2015 是可用度和穩定度較高的規範,文件齊全,國內還有 阮一峰 《ECMAScript 6 入門》 做了大量的文件翻譯,開發環境可謂完善。然而版本較舊的瀏覽器並不支援 es6 規範,尤其是 ie 瀏覽器,即使是最高的 ie11 版本,對於 es6 規範也支援得並不全。如此則需要對所有原生不支援 ES6 特性的瀏覽器做相容性處理。

本文將針對使用 Vue 生態開發完成的網站,以 ie9 版本為基礎相容目標,實現全功能正常使用的全面相容解決方案。

ES6相容

在 ie9 的環境上,es6 的部分新物件、表示式,並不支援,解決方案是使用 babel-polyfill 元件,它可以將 es6 的程式碼翻譯成低版本瀏覽器可以識別的 es5 程式碼

npm i babel-polyfill --save-dev
複製程式碼

安裝完成後,在專案的主入口檔案 main.js 的首行就可以直接引用

import 'babel-polyfill';
複製程式碼

在專案使用 vue-cli 生成的程式碼中,根目錄有一個 .babelrc 檔案,這是專案使用 babel 的配置檔案。在預設生成的模板內容中,增加 "useBuiltIns": "entry" 的設定內容,這是一個指定哪些內容需要被 polyfill(相容) 的設定

useBuiltIns 有三個設定選項

  • false - 不做任何操作
  • entry - 根據瀏覽器版本的支援,將 polyfill 需求拆分引入,僅引入有瀏覽器不支援的polyfill
  • usage - 檢測程式碼中 ES6/7/8 等的使用情況,僅僅載入程式碼中用到的 polyfill

這裡推薦設定為 entry ,完整的 .babelrc 內容如下:

{
  "presets": [
    [
      "env",
      {
        "modules": false,
        "useBuiltIns": "entry"
      }
    ],
    "stage-3"
  ]
}

複製程式碼

加入這些程式碼後,工程裡的大部分內容已可相容到 ie9 版本

Number物件

即使在使用 babel-polyfill 做程式碼翻譯後,發現還是有一些 es6 的新特性並沒有解決,比如說 Number 物件的 parseIntparseFloat 方法

es6 將全域性方法 parseInt()parseFloat() ,移植到 Number 物件上面,行為完全保持不變。這樣做的目的,是逐步減少全域性性方法,使得語言逐步模組化。

解決這個問題不需要引入包來解決,同樣在專案主入口檔案 main.js 加入以下程式碼(程式碼儘可能靠前,最好是在引用 babel-polyfill 之後 )

if (Number.parseInt === undefined) Number.parseInt = window.parseInt;
if (Number.parseFloat === undefined) Number.parseFloat = window.parseFloat;
複製程式碼

requestAnimationFrame方法

window.requestAnimationFrame 是瀏覽器用於定時迴圈操作的一個介面,類似於 setTimeout,主要用途是按幀對網頁進行重繪。

requestAnimationFrame 的優勢,在於充分利用顯示器的重新整理機制,比較節省系統資源。顯示器有固定的重新整理頻率(60Hz或75Hz),也就是說,每秒最多隻能重繪60次或75次,requestAnimationFrame 的基本思想就是與這個重新整理頻率保持同步,利用這個重新整理頻率進行頁面重繪。此外,使用這個API,一旦頁面不處於瀏覽器的當前標籤,就會自動停止重新整理。這就節省了CPU、GPU和電力。

不過有一點需要注意,requestAnimationFrame 是在主執行緒上完成。這意味著,如果主執行緒非常繁忙,requestAnimationFrame 的動畫效果會大打折扣。

window.requestAnimationFrame() 方法告訴瀏覽器您希望執行動畫並請求瀏覽器在下一次重繪之前呼叫指定的函式來更新動畫。該方法使用一個回撥函式作為引數,這個回撥函式會在瀏覽器重繪之前呼叫。

有部分第三方元件就使用了這個方法,例如部分檔案上傳、圖片處理類的元件;那麼在這型別的元件在 ie9 下使用時,會報出

SCRIPT5007: Expected object.
複製程式碼

window.requestAnimationFrame() 的最低相容 ie 版本為 10,那麼在 ie9 上做相容就需要製作 requestAnimationFrame polyfill

(function() {
    var lastTime = 0;
    var vendors = ['ms', 'moz', 'webkit', 'o'];
    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 
                                   || window[vendors[x]+'CancelRequestAnimationFrame'];
    }
 
    if (!window.requestAnimationFrame)
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
            var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 
              timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };
 
    if (!window.cancelAnimationFrame)
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
}());
複製程式碼

Gist:requestAnimationFrame polyfill

這部分程式碼同樣是儘可能在網站入口處就執行

http網路請求(跨域)

在大多數的 Web 專案中(以 JavaWeb 為例),網站的頁面和服務(至少是 controller 層)在同一個工程進行開發和部署,在大前端的新型模式下,我們建議儘可能對網站的前端和後端進行完全分離,前後端分離的好處和意義這裡不再贅述。

既然是前後端分離,那麼部署也必然是各自獨立部署,不同的訪問路徑,就會產生跨域訪問的問題(同一站點,不同埠號也是跨域)

在此設定背景情況:

  • 服務端已完整開啟 CROS 跨域支援
  • http 元件使用 axios
  • axios 設定 withCredentials 為 true 開啟跨域訪問時攜帶 cookie 資料

高版本瀏覽器(ie10+ 或 chrome, ff)僅需要完成背景情況中的功能,即可支援跨域資料請求功能

axios 進行資料請求時,預設使用 XMLHttpRequest 物件,在檢測到當前請求是跨域訪問時,axios 會測試瀏覽器是否支援 XDomainRequest 物件,若支援則優先使用。

ie8 / ie9 的 XMLHttpRequest 物件,不支援跨域訪問,該物件在 ie10 後才原生支援跨域訪問。微軟的解決方案是在 ie8 / ie9 中提供了 XDomainRequest(XDR) 物件來進行解決跨域問題,雖然使用該物件可以跨域訪問成功,並返回資料,但它卻依然是一個功能不完整的半成品,它的使用有諸多限制:

  • XDR 僅支援 GET 與 POST 兩種請求方式
  • XDR 不支援自定義的請求頭,若服務端使用 header 的自定義引數進行做身份驗證,則不可用
  • 請求頭的 Content-Type 只允許設定為 text/plain
  • XDR 不允許跨協議的請求,如果網頁在 HTTP 協議下,就只能請求 HTTP 協議下的介面,不能訪問 HTTPS 介面
  • XDR 只接受HTTP/HTTPS 的請求
  • 發起請求的時候,不會攜帶 authenticationcookies

微軟雖然提供瞭解決方案,但卻是不折不扣的雞肋,根本無法勝任系統中各種場景的資料請求需求,至此,axios 對 ie9 的跨域資料請求已無能為力。

完美解決方案:代理(proxy)

雖然 axios 對 ie9 跨域已無能為力,但前端專案打包的解決方案 webpack 提供了一個優雅而徹底解決問題的方式:代理

devServer.proxy

webpack 的 devServer.proxy 的功能是由 http-proxy-middleware 專案來實現的

實現原理是將目標位置的請求代理為前端服務本地的請求,既然是代理成為本地的請求,就不存在跨域的問題,axios 就會用回 XMLHttpRequest 物件進行資料請求,一切都恢復正常了,header、cookies、content-type、authentication 等內容都被正確傳遞到服務端。

專案中 webpack.config.js 的配置

devServer: {
    historyApiFallback: true,
    noInfo: true,
    overlay: true,
    proxy: {
        '/api': {
            target: 'http://localhost:8081/myserver',
            pathRewrite: {
                '^/api': ''
            }
        }
    }
}
複製程式碼

配置中指定了將 http://localhost:8081/myserver 服務的位置代理為本地前端服務的 http://localhost:8080/api。例如需要讀取使用者資訊的原請求是 http://localhost:8081/myserver/user/zhangsan,代理後,就變為 http://localhost:8080/api/user/zhangsan

即是 /api 的字首代表了服務端,所以在使用 axios 時,需要對每個服務端請求都增加上 /api 的字首;通常在專案開發中,需要對資料請求元件 axios 進行二次封裝,以達到統一設定預設引數,統一資料請求入口等目的,那麼此時就只需要在二次封裝的檔案裡統一調整請求字首即可。

不過,webpack 的 devServer.proxy 僅在開發模式下可用,生產模式下無法使用。開發模式下,除錯服務可以讀取 webpack.config.js 中的配置內容進行實時代理,而專案在部署到生產環境前,需要將工程進行編譯轉換成靜態的 js 檔案,沒有除錯服務的支撐自然是無法進行請求代理的。

nginx 配置

雖然 devServer.proxy 的功能僅能工作於開發模式,那麼在生產模式下,自然也是有解決方案的;通常 Vue 的專案在編譯成最終的 js 檔案後,僅需要靜態伺服器即可,這其中又以 nginx 為最優選擇方案,輕量、高效能、高併發、反向代理服務等均為其優點,這裡需要做的資料請求代理的功能就使用到了 nginx 的 反向代理 功能

conf/nginx.conf 檔案配置增加以下內容

location /api/ {
    proxy_pass http://localhost:8081/myserver/;
}
複製程式碼

該配置同樣是將 http://localhost:8081/myserver/ 的目標服務端位置代理為本地服務的 /api 路徑,如此,生產環境下的資料請求問題也得以解決

個人原創內容,轉載請說明出處

完整內容:github.com/TerryZ

相關文章