用了babel還需要polyfill嗎原理解析
引言
前兩天一個同事跟我說了這么一個面試題,面試官上來就問他:“項目中用了babel還需要polyfill嗎?” 開始他的內(nèi)心是懵比的,怎么還有如此不按套路出牌的問題,按照面試的基本原則,答案一定是需要的,不然還怎么往下問啊。于是他說“要的”。當面試官深挖下去的時候他終于頂不住了。 其實平時開發(fā)的過程中用的大部分都是現(xiàn)成的腳手架,很少會仔細看babel的配置,更不會深挖babel的用法。那么今天我就來給大家好好回答一下這個問題。
首先說明:我們后面說的babel都是基于7.10.0這個版本。
啥是Babel
甩出中文官方文檔的定義
Babel 是一個工具鏈,主要用于將 ECMAScript 2015+ 版本的代碼轉(zhuǎn)換為向后兼容的 JavaScript 語法,以便能夠運行在當前和舊版本的瀏覽器或其他環(huán)境中。 下面列出的是 Babel 能為你做的事情:
- 語法轉(zhuǎn)換
- 通過 Polyfill 方式在目標環(huán)境中添加缺失的特性 (通過 @babel/polyfill 模塊)
- 源碼轉(zhuǎn)換 (codemods)
- 更多! (查看這些 視頻 獲得啟發(fā))
看完官方定義之后大家是不是覺得babel一個人就能把向后兼容的事情都做完了(不得不說確實有點誤導),其實根本不是這樣的。
真實的情況是babel只是提供了一個“平臺”,讓更多有能力的plugins入駐我的平臺,是這些plugins提供了將 ECMAScript 2015+ 版本的代碼轉(zhuǎn)換為向后兼容的 JavaScript 語法的能力。
那么他是咋做到的呢?這就不得不提大名鼎鼎的AST了。
Babel 工作原理
babel的工作過程分為三個階段:parsing(解析)、transforming(轉(zhuǎn)化)、printing(生成)
- parsing階段babel內(nèi)部的 babylon 負責將es6代碼進行語法分析和詞法分析后轉(zhuǎn)換成抽象語法樹
- transforming階段內(nèi)部的 babel-traverse 負責對抽象語法樹進行變換操作
- printing階段內(nèi)部的 babel-generator 負責生成對應的代碼
其中第二步的轉(zhuǎn)化是重中之重,babel的插件機制也是在這一步發(fā)揮作用的,plugins在這里進行操作,轉(zhuǎn)化成新的AST,再交給第三步的babel-generator。 所以像我們上面說的如果沒有這些plugins進駐平臺,那么babel這個“平臺”是不具備任何能力的。就好像這樣:
const babel = code => code;
因此我們可以有信心的說出那個答案“需要”。不僅需要polyfill,還需要大量的plugins。
下面我們通過例子來說明,以及還需要哪些plugins才能將 ECMAScript 2015+ 版本的代碼完美的轉(zhuǎn)換為向后兼容的 JavaScript 語法。
preset-env, polyfill, plugin-transform-runtime 區(qū)別
現(xiàn)在我們通過 npm init -y 來創(chuàng)建一個例子,然后安裝 @babel/cli 和 @babel/core。 通過命令 babel index.js --out-file compiled.js
把 index 文件用 babel 編譯成compiled.js
// index.js const fn = () => { console.log("wens"); }; const p = new Promise((resolve, reject) => { resolve("wens"); }); const list = [1, 2, 3, 4].map(item => item * 2);
不加任何plugins
為了印證上面的說法,我們首先測試不加任何plugins的情況,結(jié)果如下
//compiled.js const fn = () => { console.log("wens"); }; const p = new Promise((resolve, reject) => { resolve("wens"); }); const list = [1, 2, 3, 4].map(item => item * 2);
編譯好的文件沒有任何變化,印證了我們上面的說法。接下來我們加入 plugins。
在加入plugins測試之前我們需要知道一些前置知識,babel將ECMAScript 2015+ 版本的代碼分為了兩種情況處理:
- 語法層: let、const、class、箭頭函數(shù)等,這些需要在構(gòu)建時進行轉(zhuǎn)譯,是指在語法層面上的轉(zhuǎn)譯
- api方法層:Promise、includes、map等,這些是在全局或者Object、Array等的原型上新增的方法,它們可以由相應es5的方式重新定義
babel對這兩種情況的轉(zhuǎn)譯是不一樣的,我們需要給出相應的配置。
加入preset-env
上面的例子種const,箭頭函數(shù)屬于語法層面的,而promise和map屬于api方法層面的,現(xiàn)在我們加入 preset-env 看看效果
// babel.config.js module.exports = { presets: ["@babel/env"], plugins: [] };
babel 官方定義的 presets 配置項表示的是一堆plugins的集合,省的我們一個個的寫plugins,他直接定義好了類似處理react,typescript等的preset
//compiled.js "use strict"; var fn = function fn() { console.log("wens"); }; var p = new Promise(function (resolve, reject) { resolve("wens"); }); var list = [1, 2, 3, 4].map(function (item) { return item * 2; });
果然從語法層面都降級了。那么api層面要如何處理呢? 下面我們加入 @bable/polyfill
加入polyfill
對于 polyfill 的定義,相信經(jīng)常逛 mdn 的同學一定不會陌生, 他就是把當前瀏覽器不支持的方法通過用支持的方法重寫來獲得支持。
在項目中安裝 @bable/polyfill ,然后 index.js 文件中引入
// index.js import "@bable/polyfill"; const fn = () => { console.log("wens"); }; const p = new Promise((resolve, reject) => { resolve("wens"); }); const list = [1, 2, 3, 4].map(item => item * 2);
再次編譯我們看看結(jié)果
// compiled.js "use strict"; require("@bable/polyfill"); var fn = function fn() { console.log("wens"); }; var p = new Promise(function (resolve, reject) { resolve("wens"); }); var list = [1, 2, 3, 4].map(function (item) { return item * 2; });
沒有別的變化,就多了一行require("@bable/polyfill"),其實這里就把這個庫中的所有的polyfill都引入進來了,就好比我們項目中一股腦引入了全部的lodash方法。這樣就支持了Promise和map方法。
細心的同學一定會發(fā)現(xiàn)這樣有點“蠢”啊,lodash都提供了按需加載,你這個一下都引入進來了,可我只需要Promise和map啊。別慌,我們接著往下看。
配置 useBuiltIns
上面我們通過 import "@bable/polyfill"
的方式來實現(xiàn)針對api層面的“抹平”。然而從 babel v7.4.0開始官方就不建議采取這樣的方式了。 因為引入 @bable/polyfill 就相當于在代碼中引入下面兩個庫
import "core-js/stable"; import "regenerator-runtime/runtime";
這意味著不僅不能按需加載還有全局空間被污染的問題。因為他是通過向全局對象和內(nèi)置對象的prototype上添加方法來實現(xiàn)的。
因此 babel 決定把這兩個人的工作一并交給上面我們提到的@babel/env,不僅不會全局污染還支持按需加載,豈不是妙哉?,F(xiàn)在我們再來看看改造后的配置
// index.js // 去掉了polyfill const fn = () => { console.log("wens"); }; const p = new Promise((resolve, reject) => { resolve("wens"); }); const list = [1, 2, 3, 4].map(item => item * 2);
// webpack.config.js module.exports = { presets: [ [ "@babel/env", { useBuiltIns: "usage", // 實現(xiàn)按需加載 corejs: { version: 3, proposals: true } } ] ], plugins: [] };
通過給 @babel/env 配置 useBuiltIns 和 corejs 屬性,我們實現(xiàn)了polyfill方法的按需加載。關于全部配置項,參加官方文檔
// compiled.js "use strict"; require("core-js/modules/es.array.map"); require("core-js/modules/es.object.to-string"); require("core-js/modules/es.promise"); var fn = function fn() { console.log("wens"); }; var p = new Promise(function (resolve, reject) { resolve("wens"); }); var list = [1, 2, 3, 4].map(function (item) { return item * 2; });
編譯后的js文件只require了需要的方法,完美。那么我們還有可以優(yōu)化的空間嗎?有的有的,我們接著往下看。
加入 @babel/plugin-transform-runtime
改造上面的例子
// index.js class Person { constructor(name) { this.name = name; } say() { console.log(this.name); } }
只轉(zhuǎn)換一個 Person 類,我們看看轉(zhuǎn)換后的文件長啥樣
// compiled.js "use strict"; require("core-js/modules/es.function.name"); require("core-js/modules/es.object.define-property"); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } var Person = /*#__PURE__*/function () { function Person(name) { _classCallCheck(this, Person); this.name = name; } _createClass(Person, [{ key: "say", value: function say() { console.log(this.name); } }]); return Person; }();
除了require的部分,還多了好多自定義的函數(shù)。同學們想一想,現(xiàn)在只有一個index文件需要轉(zhuǎn)換,然而實際項目開發(fā)中會有大量的需要轉(zhuǎn)換的文件,如果每一個轉(zhuǎn)換后的文件中都存在相同的函數(shù),那豈不是浪費了,怎么才能把重復的函數(shù)去掉呢?
plugin-transform-runtime 閃亮登場。
上面出現(xiàn)的_classCallCheck,_defineProperties,_createClass三個函數(shù)叫做輔助函數(shù),是在編譯階段輔助 Babel 的函數(shù)。
當使用了plugin-transform-runtime插件后,就可以將babel轉(zhuǎn)譯時添加到文件中的內(nèi)聯(lián)輔助函數(shù)統(tǒng)一隔離到babel-runtime提供的helper模塊中,編譯時,直接從helper模塊加載,不在每個文件中重復的定義輔助函數(shù),從而減少包的尺寸,下面我們看下效果:
// webpack.config.js module.exports = { presets: [ [ "@babel/env", { useBuiltIns: "usage", corejs: { version: 3, proposals: true } } ] ], plugins: ["@babel/plugin-transform-runtime"] };
// compiled.js "use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); require("core-js/modules/es.function.name"); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var Person = /*#__PURE__*/function () { function Person(name) { (0, _classCallCheck2["default"])(this, Person); this.name = name; } (0, _createClass2["default"])(Person, [{ key: "say", value: function say() { console.log(this.name); } }]); return Person; }();
完美的解決了代碼冗余的問題。 你們以為這就結(jié)束了嗎,還沒有。仔細看到這里的同學應該注意到了雖然上面使用 useBuiltIns
配置項實現(xiàn)了poilyfill的按需引用,可是他還存在全局變量污染的情況,就好比這句代碼,重寫了array的prototype方法,造成了全局污染。
require("core-js/modules/es.array.map");
最后再改造一次babel的配置文件
// webpack.config.js module.exports = { presets: ["@babel/env"], plugins: [ [ "@babel/plugin-transform-runtime", { corejs: { version: 3 } } ] ] };
我們看到去掉了 @babel/env 的相關參數(shù),而給 plugin-transform-runtime 添加了corejs參數(shù),最終轉(zhuǎn)換后的文件不會再出現(xiàn)polyfill的require的方法了。
// compiled.js "use strict"; var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault"); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass")); var Person = /*#__PURE__*/function () { function Person(name) { (0, _classCallCheck2["default"])(this, Person); this.name = name; } (0, _createClass2["default"])(Person, [{ key: "say", value: function say() { console.log(this.name); } }]); return Person; }();
綜上所述,plugin-transform-runtime 插件借助babel-runtime實現(xiàn)了下面兩個重要的功能
- 對輔助函數(shù)的復用,解決轉(zhuǎn)譯語法層時出現(xiàn)的代碼冗余
- 解決轉(zhuǎn)譯api層出現(xiàn)的全局變量污染
結(jié)束
終于寫完了,從翻閱文檔到產(chǎn)出文章足足耗費了兩天的時間。希望看到文章的同學也能和我一樣有收獲~~
更多關于babel polyfill原理的資料請關注腳本之家其它相關文章!
相關文章
antd vue表格可編輯單元格以及求和實現(xiàn)方式
這篇文章主要介紹了antd vue表格可編輯單元格以及求和實現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04Vue+Element實現(xiàn)動態(tài)生成新表單并添加驗證功能
這篇文章主要介紹了Vue+Element實現(xiàn)動態(tài)生成新表單并添加驗證功能,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-05-05