前言
本文首發於 github 部落格
如對你有幫助是我的榮幸,你的 star 是對我最大的支援!
大家都知道 babel 是相容對 ES6 支援不完善的低版本瀏覽器的轉換編譯器。
而 babel 其實主要做的只有兩件事情:
- 語法轉換
- 新 API 的 polyfill 相容
那麼廢話少說,我們直接點,直接說說常見幾個場景下相容舊版瀏覽器的方案。
實踐方案
polyfill.io
如果你的工程是用的語法是 ES5,但是用了一些 ES6+ 的API特性,那麼可以直接引入:
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>
複製程式碼
來相容 Web 應用不支援的 API。
原理大概是 polyfill.io 會讀取每個請求的User-Agent標頭,並返回適合請求瀏覽器的polyfill。具體的還可以自己指定載入哪些 特性的 polyfill,具體想了解更多的大家可以看看 官方文件。
優點:每個瀏覽器的裝置載入的 polyfill 都不一樣,最新的完全相容ES6棟瀏覽器基本載入的 polyfill 大小為0。
缺點:
- 必須先進行語法轉換,用了 async 語法在新瀏覽器上可以執行,但是在舊版瀏覽器就直接丟擲錯誤了。
- 不能按照程式碼所用到的新特性按需進行 polyfill,也就是說即便你的 Web 應用只用到了
es6.array.from
特性,polyfill.io 依然可能會把該瀏覽器所有不支援的特性(如:es6.promise,es6.string.includes等特性)全部載入進來。
@babel/preset-env 按需載入
上面提到了 polyfill.io 的一個缺點是無法按需引入,那麼現在就介紹下 babel7 @babel/preset-env
@babel/preset-env 預設根據 .browserslist 所填寫的需要相容的瀏覽器,進行必要的程式碼語法轉換和 polyfill
// .babelrc.js
module.exports = {
presets: [
[
"@babel/preset-env",
{
"modules": false, // 模組使用 es modules ,不使用 commonJS 規範,具體看文末附錄
"useBuiltIns": 'usage', // 預設 false, 可選 entry , usage
}
]
]
}
複製程式碼
此處重點介紹一下其新推出的 useBuiltIns 選項:
- false : 不啟用polyfill, 如果在業務入口
import '@babel/polyfill'
, 會無視.browserslist
將所有的 polyfill 載入進來。
polyfill 全部載入進來有 284 個特性包 - entry : 啟用,需要手動
import '@babel/polyfill'
才生效(否則會丟擲錯誤:regeneratorRuntime undefined), 根據.browserslist
過濾出 需要的polyfill
(類似 polyfill.io 方案)
使用entry根據browserslist(ie>10)載入進來的有 238 個特性包 - usage : 不需要手動
import '@babel/polyfill'
(加上也無妨,編譯時會自動去掉), 且會根據.browserslist
+ 業務程式碼使用到的新 API 按需進行 polyfill。
使用usage根據browserslist(ie>10)+程式碼用到的,載入進來的只有 51 個特性包 usage 風險項:由於我們通常會使用很多 npm 的 dependencies 包來進行業務開發,babel 預設是不會檢測 依賴包的程式碼的。
也就是說,如果某個 依賴包使用了
Array.from
, 但是自己的業務程式碼沒有使用到該API,構建出來的 polyfill 也不會有 Array.from, 如此一來,可能會在某些使用低版本瀏覽器的使用者出現 BUG。所以避免這種情況發生,一般開源的第三方庫釋出上線的時候都是轉換成 ES5 的。
上面提到的 useBuiltIns:'usage'
似乎已經很完美解決我們的需要了,但是我們構建的時候發現:
// es6+ 原始碼:
const asyncFun = async ()=>{
await new Promise(setTimeout, 2000)
return '2s 延時後返回字串'
}
export default asyncFun
複製程式碼
根據上述的 useBuiltIns:'usage'
配置編譯後:
import "core-js/modules/es6.promise";
import "regenerator-runtime/runtime";
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
var asyncFun =
/*#__PURE__*/
function () {
var _ref = _asyncToGenerator(
/*#__PURE__*/
regeneratorRuntime.mark(function _callee() {
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return new Promise(setTimeout, 2000);
case 2:
return _context.abrupt("return", '2s 延時後返回字串');
case 3:
case "end":
return _context.stop();
}
}
}, _callee, this);
}));
return function asyncFun() {
return _ref.apply(this, arguments);
};
}();
export default asyncFun;
複製程式碼
上述程式碼中,我們看到,asyncGeneratorStep
, _asyncToGenerator
這兩個函式是被內聯進來,而不是 import 進來的。
也就是說,如果你有多個檔案都用到了 async,那麼每個檔案都會內聯一遍 asyncGeneratorStep
, _asyncToGenerator
函式。
這程式碼明顯是重複了,那麼有什麼方法可以進行優化呢? 答案是 @babel/plugin-transform-runtime
@babel/plugin-transform-runtime
babel 在每個需要的檔案的頂部都會插入一些 helpers 程式碼,這可能會導致多個檔案都會有重複的 helpers 程式碼。 @babel/plugin-transform-runtime
的 helpers 選項就可以把這些模組抽離出來
// .babelrc.js
module.exports = {
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": false, // 預設值,可以不寫
"helpers": true, // 預設,可以不寫
"regenerator": false, // 通過 preset-env 已經使用了全域性的 regeneratorRuntime, 不再需要 transform-runtime 提供的 不汙染全域性的 regeneratorRuntime
"useESModules": true, // 使用 es modules helpers, 減少 commonJS 語法程式碼
}
]
],
presets: [
[
"@babel/preset-env",
{
"modules": false, // 模組使用 es modules ,不使用 commonJS 規範
"useBuiltIns": 'usage', // 預設 false, 可選 entry , usage
}
]
]
}
複製程式碼
// 新增新配置後編譯出來的程式碼
import "core-js/modules/es6.promise";
import "regenerator-runtime/runtime";
import _asyncToGenerator from "@babel/runtime/helpers/esm/asyncToGenerator";
var asyncFun =
/*#__PURE__*/
function () {
var _ref = _asyncToGenerator(
/*#__PURE__*/
regeneratorRuntime.mark(function _callee() {
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return new Promise(setTimeout, 2000);
case 2:
return _context.abrupt("return", '2s 延時後返回字串');
case 3:
case "end":
return _context.stop();
}
}
}, _callee, this);
}));
return function asyncFun() {
return _ref.apply(this, arguments);
};
}();
export default asyncFun;
複製程式碼
可以看到,已經沒有了內聯的 helpers 程式碼,大功告成。
總結
如果沒有什麼特殊的需求,使用 babel 7 的最佳配置是:
-
首先安裝依賴包:
npm i -S @babel/polyfill @babel/runtime && npm i -D @babel/preset-env @babel/plugin-transform-runtime
-
配置
.babelrc.js
// .babelrc.js
module.exports = {
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": false, // 預設值,可以不寫
"helpers": true, // 預設,可以不寫
"regenerator": false, // 通過 preset-env 已經使用了全域性的 regeneratorRuntime, 不再需要 transform-runtime 提供的 不汙染全域性的 regeneratorRuntime
"useESModules": true, // 使用 es modules helpers, 減少 commonJS 語法程式碼
}
]
],
presets: [
[
"@babel/preset-env",
{
"modules": false, // 模組使用 es modules ,不使用 commonJS 規範
"useBuiltIns": 'usage', // 預設 false, 可選 entry , usage
}
]
]
}
複製程式碼
PS: 如果想要瞭解更多有關 @babel/preset-env 和 @babel/plugin-transform-runtime 的選項配置用途,可以參考我的個人總結
思考與探索(Modern Build)
上述的方案,其實還一直隱藏著一個不算問題的問題,那就是如果使用最新的瀏覽器,其實不需要任何的語法轉換和polyfill。
那麼參考下上述的 polyfill 方案,能不能實現如果低版本瀏覽器,就使用usage方案按需 transform + polyfill 的程式碼,如果是較新瀏覽器,就不進行任何的語法轉換和 polyfill 呢?
必須能!
參考這篇文章 deploying es2015 code in production today,其中提出了基於 script 標籤的 type="module"
和 nomodule
屬性 區分出當前瀏覽器對 ES6 的支援程度。
具體原理體現在,對於以下程式碼:
<script type="module" src="main.js"></script>
<script nomodule src="main.legacy.js"></script>
複製程式碼
支援 ES Module 的瀏覽器能夠識別 type="module"
和 nomodule
,會載入 main.js
忽略 main.legacy.js
,
還未支援 ES module 的瀏覽器則恰恰相反,只會載入main.legacy.js
。
那麼怎麼實現優化就很清晰了:
- 通過配置上述 babel 最佳實踐的,給這類的程式碼檔案的 script 標間加上
nomodule
屬性 - 通過配置
@babel/preset-env
的選項target.esmodules = true
,不轉換所有的語法也不新增 polyfill,生成 ES6+ 的能被現代瀏覽器識別解析的程式碼,並給這類程式碼檔案的 script 標籤加上type="module"
vue-cli 3.0 官方提供 modern build 功能
create-react-app 預計在下一個版本3.0的迭代中才實現。 現階段實現需要自己寫 webpack 外掛來實現 module/nomodule 插入