本文基於的babel版本是7.11.6,本文全部示例githubjavascript
Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments.html
Babel是一個工具鏈,主要用於將ECMAScript 2015+代碼轉換爲當前和較老的瀏覽器或環境中的向後兼容的JavaScript版本。java
針對於新出的ECMAScript標準,部分瀏覽器還不能徹底兼容,須要將這部分語法轉換爲瀏覽器可以識別的語法。好比有些瀏覽器不能正常解析es6中的箭頭函數,那經過babel轉換後,就能將箭頭函數轉換爲瀏覽器可以「認懂」得語法。node
針對於一些較老的瀏覽器,好比IE10或者更早以前。對一些最新的內置對象Promise/Map/Set
,靜態方法Arrary.from/Object.assign
以及一些實例方法Array.prototype.includes
,這些新的特性都不存在與這些老版本的瀏覽器中,那麼就須要給這些瀏覽器中的原始方法中添加上這些特性,即所謂的polyfill
。react
能夠作一些源碼的轉換,便可以直接使用babel中提供的API對代碼進行一些分析處理,例如webpack
const filename = 'index.js'
const { ast } = babel.transformSync(source, { filename, ast: true, code: false });
const { code, map } = babel.transformFromAstSync(ast, source, {
filename,
presets: ["minify"],
babelrc: false,
configFile: false,
});
複製代碼
下面講到的幾種轉換方式,其實本質上都是同樣的,都是調用babel-core中的API來進行直接轉換git
const source = ` const someFun = () => { console.log('hello world'); } `;
require("@babel/core").transform(source, {
plugins: ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-parameters"],
}, result => {
console.log(result.code);
});
複製代碼
babel提供了cli的方式,能夠直接讓咱們使用命令行的方式來使用babel,具體參照一下作法es6
## install
## 首先須要安裝 @babel/core @babel/cli
## @babel/cli是提供的命令行工具,會內部調用@babel/core來進行代碼轉換
npm install @babel/core @babel/cli --save-dev
## usage
npx babel ./cli/index.js
複製代碼
本地安裝完依賴後,就可使用babel來進行代碼轉換了,npx babel [options] files
,babel提供了一些經常使用的cli命令,可使用npx babel --help
來查看github
> $ npx babel --help ⬡ 12.13.0 [±master ●●●]
Usage: babel [options] <files ...>
Options:
-f, --filename [filename] The filename to use when reading from stdin. This will be used in source-maps, errors etc.
--presets [list] A comma-separated list of preset names.
--plugins [list] A comma-separated list of plugin names.
--config-file [path] Path to a .babelrc file to use.
--env-name [name] The name of the 'env' to use when loading configs and plugins. Defaults to the value of BABEL_ENV, or else NODE_ENV, or else
'development'.
複製代碼
下面是一個簡單的例子,好比有這麼一段源代碼,web
// cli/index.js
const arrayFn = (...args) => {
return ['babel cli'].concat(args);
}
arrayFn('I', 'am', 'using');
複製代碼
執行如下命令:npx babel ./cli/index.js --out-file ./cli/index.t.js
,結果以下圖:
代碼和源代碼居然是如出一轍的,爲何箭頭函數沒有進行轉換呢?這裏就會引入plugins以及preset的概念,這裏暫時不會具體講解,只須要暫時知道,代碼的轉換須要使用plugin進行。
轉換箭頭函數,咱們須要使用到@babel/plugin-transform-arrow-functions/parameters
,首先安裝完以後,在此執行轉換
npm install @babel/plugin-transform-arrow-functions @babel/plugin-transform-parameters --save-dev
npx babel ./cli/index.js --out-file ./cli/index.t.js --plugins=@babel/plugin-transform-arrow-functions,@babel/plugin-transform-parameters
複製代碼
執行完以後,再看生成的文件
建立webpack.config.js,編寫以下配置
// install
npm install webpack-cli --save-dev
// webpack/webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'index.bundle.js'
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
plugins: ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-parameters"]
}
}
}
]
}
};
// usage
cd webpack
npx webpack
複製代碼
能夠獲得轉換以後的代碼以下:
能夠對比查看babel-cli的轉換以後的代碼是一致的。
參看以上三種方式,都必須加載了plugins這個參數選項,尤爲是在cli方式中,若是須要加載不少插件,是很是不便於書寫的,同時,相同的配置也很差移植,好比須要在另一個項目中一樣使用相同的cli執行,那麼顯然插件越多,就會越容易出錯。鑑於此,babel提供了config的方式,相似於webpack的cli方式以及config方式。
babel在7.0以後,引入了babel.config.[extensions]
,在7.0以前,項目都是基於.babelrc
來進行配置,這裏暫時不會講解它們之間的區別。下面就是一個比較基於上面例子的一個.babelrc文件。
// .babelrc
{
"plugins": ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-parameters"]
}
複製代碼
咱們將這個文件放置在根目錄下,新建一個config
的文件夾,從cli目錄中將index.js文件copy到config目錄下,而後執行npx babel ./config/index.js --out-file ./config/index.t.js
,完成以後,會發現和cli執行的方式並無什麼差異。
babel.config.js是在babel第7版引入的,主要是爲了解決babel6中的一些問題,參看babeljs.io/docs/en/con…
另外若是隻使用.babelrc,在monorepo項目中會遇到一些問題,這得從.babelrc加載的兩條規則有關
.babelrc
文件,必須位於babel運行的root目錄下,或者是包含在babelrcRoots
這個option配置的目錄下,不然找到的配置會直接被忽略下面咱們在以前的例子上進行改造,文件結構以下:
在mod1文件夾中建立一個package.json文件,內容爲{}
。如今執行如下代碼:
npx babel ./config/mod1/index.js -o ./config/mod1/index.t.js
複製代碼
能夠發現,index.js沒有編譯,由於在向上查找的時候,找到了mod1中的package.json,可是在此目錄中並無找到.babelrc
文件,所以不會編譯。
下面,咱們將.babelrc
文件移至mod1中,而後再執行上面的命令,此次會編譯成功麼?
答案依舊是不會,由於當前的執行目錄是在src下面,因此在mod1
目錄中的配置文件將會被忽略掉。
這裏有兩種方法來解決這個問題:
進入到mod1目錄中直接執行 cd ./config/mod1 & npx babel index.js -o index.t.js
在執行的root目錄下,添加一個babel.config.json
文件,在其中添加babelrcRoots
將這個目錄添加進去
而後再執行npx babel ./config/mod1/index.js -o ./config/mod1/index.t.js
就能夠正常編譯了。
正是基於上述的一些問題,babel在7.0.0以後,引入了babel.config.[json/js/mjs/cjs]
,基於babel.config.json的配置會靈活得多。
通常babel.config.json
會放置在根目錄下,在執行編譯時,babel會首先去尋找babel.config.json
文件,以此來做爲整個項目的根配置。
若是在子目錄中不存在.babelrc的配置,那麼在編譯時,會根據根目錄下的配置來進行編譯,好比在config/index.js中添加以下代碼
執行npx babel ./config/index -o ./config/index.t.js
後會發現for..of
這段代碼會被原樣輸出,由於在config目錄中並無針對for..of
配置插件。如今在config文件中添加.babelrc
,內容以下:
{
"plugins": [
"@babel/plugin-transform-for-of"
]
}
複製代碼
再次執行完,會發現,for..of
會被babel編譯
說明,若是子文件夾中存在相應的babel配置,那麼編譯項會在根配置上進行擴展。
但這點在monorepo
項目中會有點例外,以前我在mod1文件家中放置了一個package.json
文件:
執行下面命令
npx babel ./config/mod1/index.js -o ./config/mod1/index.t.js
複製代碼
發現for..of
部分並無被babel編譯,這個緣由和以前在講bablerc的緣由是同樣的,由於執行的根目錄是src,所以在mod1中並不能去加載.babelrc配置,所以只根據根目錄中的配置來執行編譯。想要mod1中的配置也被加載,能夠按照相同的方法在babel.config.json
中配置babelrcRoots
。
另外若是子文件家中不存在相應的配置,好比在cli目錄下,在src目錄下執行config/index.js文件是沒有問題的,可是若是進入cli中,而後直接執行,會發現index.js文件不會被編譯。由此,你須要告訴babel去找到這個配置,這裏可使用rootMode: upward
來使babel向上查找babel.config.json,並以此做爲根目錄。
cd cli & npx babel ./index.js -o ./index.t.js --root-mode upward
複製代碼
monorepo
(能夠理解爲在一個項目中會有多個子工程)Babel is a compiler (source code => output code). Like many other compilers it runs in 3 stages: parsing, transforming, and printing.
Now, out of the box Babel doesn't do anything. It basically acts like
const babel = code => code;
by parsing the code and then generating the same code back out again. You will need to add plugins for Babel to do anything.
沒有plugins,babel將啥事也作不了。
babel提供了豐富的插件來對不一樣時期的代碼進行轉換。例如咱們在es6最常使用的箭頭函數,當須要轉化爲es5版本時,就用到了arrow-functions這個插件。
具體的插件列表,能夠查看plugins。
presets的中文翻譯爲預設,即爲一組插件列表的集合,咱們能夠沒必要再當獨地一個一個地去添加咱們須要的插件。好比咱們但願使用es6的全部特性,咱們可使用babel提供的ES2015這個預設。
// 若是plugin已經在發佈到npm中
// npm install @babel/plugin-transform-arrow-functions -D
// npm install @babel/preset-react -D
{
"plugins": ["@babel/plugin-transform-arrow-functions"],
"presets": ["@babel/preset-react"]
}
// 或者按照babel的規範,引入本身編寫的plugin/preset
{
"plugins": ["path/to/your/plugin"],
"presets": ["path/to/your/preset"],
}
複製代碼
任何一個插件均可以擁有自定義的屬性來定義這個插件的行爲。具體的寫法能夠爲:
{
"plugins": ["pluginA", ["pluginA"], ["pluginA", {}]],
"presets": ["presetA", ["presetA"], ["presetA", {}]]
}
// example
{
"plugins": [
[
"@babel/plugin-transform-arrow-functions",
{ "spec": true }
]
],
"presets": [
[
"@babel/preset-react",
{
"pragma": "dom", // default pragma is React.createElement (only in classic runtime)
"pragmaFrag": "DomFrag", // default is React.Fragment (only in classic runtime)
"throwIfNamespace": false, // defaults to true
"runtime": "classic" // defaults to classic
// "importSource": "custom-jsx-library" // defaults to react (only in automatic runtime)
}
]
]
}
複製代碼
下面咱們來作幾個例子測試一下,首先,官方給出的插件標準寫法以下(在此以前,強烈建議閱讀babel-handbook來了解接下來插件編碼中的一些概念):
// 1. babel使用babylon將接受到的代碼進行解析,獲得ast樹,獲得一系列的令牌流,例如Identifier就表明一個字
// 符(串)的令牌
// 2. 而後使用babel-traverse對ast樹中的節點進行遍歷,對應於插件中的vistor,每遍歷一個特定的節點,就會給visitor添加一個標記
// 3. 使用babel-generator對修改事後的ast樹從新生成代碼
// 下面的這個插件的主要功能是將字符串進行反轉
// plugins/babel-plugin-word-reverse.js
module.exports = function() {
return {
visitor: {
Identifier(path) {
console.log("word-reverse plugin come in!!!");
const name = path.node.name;
path.node.name = name
.split("")
.reverse()
.join("");
},
},
};
}
// 而後咱們再提供一個插件,這個插件主要是修改函數的返回值
// plugins/babel-plugin-replace-return.js
module.exports = function({ types: t }) {
return {
visitor: {
ReturnStatement(path) {
console.log("replace-return plugin come in!!!");
path.replaceWithMultiple([
t.expressionStatement(t.stringLiteral('Is this the real life?')),
t.expressionStatement(t.stringLiteral('Is this just fantasy?')),
t.expressionStatement(t.stringLiteral('(Enjoy singing the rest of the song in your head)')),
]);
},
},
};
}
複製代碼
首先咱們來測試一下原始代碼是否經過咱們自定義的插件進行轉換了,源代碼以下:
// plugins/index.js
const myPluginTest = (javascript) => {
return 'I love Javascript';
}
// 而後在plugins目錄下建立一個.babelrc文件,用於繼承默認的babel.config.json文件
// plugins/.babelrc
{
"plugins": ["./babel-plugin-word-reverse", "./babel-plugin-replace-return"]
}
// usage
npx babel ./plugins/index.js -o ./plugins/index.t.js
複製代碼
如下是執行完以後的結果
從截圖能夠看出,字符串被反轉了,以及返回的字符串也被替換掉了。
而後咱們再來看看執行的順序
能夠看到,排在插件列表以前的插件會在提早執行。
下面再新建一個插件,用於自定義的preset編寫
// presets/babel-plugin-word-replace.js
// 這個插件主要的功能是給每一個節點類型爲Identifier的名稱拼接一個_replace的後綴
module.exports = function() {
return {
visitor: {
Identifier(path) {
console.log("word-replace plugin come in!!!");
let name = path.node.name;
path.node.name = name += '_replace';
},
},
};
}
複製代碼
而後咱們藉助以前編寫的babel-plugin-word-reverse
來編寫兩個新的presets
// presets/my-preset-1.js
module.exports = () => {
console.log('preset 1 is executed!!!');
return {
plugins: ['../plugins/babel-plugin-word-reverse']
};
};
// presets/my-preset-2.js
module.exports = () => {
console.log('preset 2 is executed!!!');
return {
presets: ["@babel/preset-react"],
plugins: ['./babel-plugin-word-replace', '@babel/plugin-transform-modules-commonjs'],
};
};
// 建立.babelrc配置
// presets/.babelrc
{
"presets": [
"./my-preset-1",
"./my-preset-2"
]
}
// 測試代碼
// presets/index.jsx
import React from 'react';
export default () => {
const text = 'hello world';
return <div>{text}</div>;
}
// 執行
npx babel ./presets/index.jsx -o ./presets/index.t.js
複製代碼
能夠看到在.babelrc中,將preset-1放在了preset-2的前面,若是按照babel官網給出的解析,那麼preset2會被先執行,執行的順序以下
能夠看到控制檯打印的順序是preset1 -> preset2,這點與官網給出的preset執行順序是相反的???
而後再看編譯以後生成的文件,發現居然又是先執行了preset-2中的插件,而後在執行preset-1中的插件,如圖:
能夠看到顯然是首先通過了添加後綴_replace
,而後在進行了總體的reverse
。這裏是否是意味着,在presets列表中後聲明的preset中的插件會先執行呢???
懷着這個問題,去啃了下源代碼。發現babel所說的執行順序,實際上是traverse
訪問插件中vistor
的順序。由於presets其實也是一組插件的集合,通過程序處理以後,會使得presets末尾的plugins會出如今整個plugins列表的前面。
同時能夠看圖中控制檯的打印結果,word-replace
始終會在word-reverse
以前,而且是成對出現的。
// babel/packages/babel-core/src/transform.js [line 21]
const transformRunner = gensync<[string, ?InputOptions], FileResult | null>(
function* transform(code, opts) {
const config: ResolvedConfig | null = yield* loadConfig(opts);
if (config === null) return null;
return yield* run(config, code);
},
);
複製代碼
loadConfig(opts)
會被傳遞進來的plugins以及presets進行處理,進去看看發生了什麼?
// babel/packages/babel-core/src/config/full.js [line 59]
export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig( inputOpts: mixed, ): Handler<ResolvedConfig | null> {
const result = yield* loadPrivatePartialConfig(inputOpts);
// ...
const ignored = yield* (function* recurseDescriptors(config, pass) {
const plugins: Array<Plugin> = [];
for (let i = 0; i < config.plugins.length; i++) {
const descriptor = config.plugins[i];
if (descriptor.options !== false) {
try {
plugins.push(yield* loadPluginDescriptor(descriptor, context));
} catch (e) {
// ...
}
}
}
const presets: Array<{|
preset: ConfigChain | null,
pass: Array<Plugin>,
|}> = [];
for (let i = 0; i < config.presets.length; i++) {
const descriptor = config.presets[i];
if (descriptor.options !== false) {
try {
presets.push({
preset: yield* loadPresetDescriptor(descriptor, context),
pass: descriptor.ownPass ? [] : pass,
});
} catch (e) {
// ...
}
}
}
// resolve presets
if (presets.length > 0) {
// ...
for (const { preset, pass } of presets) {
if (!preset) return true;
const ignored = yield* recurseDescriptors(
{
plugins: preset.plugins,
presets: preset.presets,
},
pass,
);
// ...
}
}
// resolve plugins
if (plugins.length > 0) {
pass.unshift(...plugins);
}
})(//...)
}
複製代碼
loadPrivatePartialConfig
中會依次執行咱們定義的plugins以及presets,這也是爲何在上面的例子中preset1會打印在preset2。
// babel/packages/babel-core/src/config/config-chain.js [line 629]
function mergeChainOpts( target: ConfigChain, { options, plugins, presets }: OptionsAndDescriptors, ): ConfigChain {
target.options.push(options);
target.plugins.push(...plugins());
target.presets.push(...presets());
return target;
}
複製代碼
recurseDescriptors
這裏是一個遞歸函數,是用來在passes中存放解析事後的plugins以及presets的,passes經過unshift的方式解析每次循環以後的插件,所以presets的循環越靠後,在passes中的plugins反而會越靠前,這也是爲何presets列表中的執行順序是逆序的緣由。
// babel/packages/babel-core/src/config/full.js [line 195]
opts.plugins = passes[0];
opts.presets = passes
.slice(1)
.filter(plugins => plugins.length > 0)
.map(plugins => ({ plugins }));
opts.passPerPreset = opts.presets.length > 0;
return {
options: opts,
passes: passes,
};
複製代碼
設置解析後的plugins
,而後返回新的config。
Babel 7.4.0以後,
@babel/polyfill
這個包已經廢棄了,推薦直接是用core-js/stable
以及regenerator-runtime/runtime
import "core-js/stable"; import "regenerator-runtime/runtime"; 複製代碼
polyfill
的直接翻譯爲墊片,是爲了添加一些比較老的瀏覽器或者環境中不支持的新特性。好比Promise/ WeakMap
,又或者一些函數Array.form/Object.assign
,以及一些實例方法Array.prototype.includes
等等。
注意:這些新的特性會直接加載全局的環境上,在使用時請注意是否會污染當前的全局做用域
npm install --save @babel/polyfill
// commonJs
require('@babel/polyfill')
// es6
import('@babel/polyfill')
複製代碼
當在webpack中使用時,官方推薦和@babel/preset-env
一塊兒使用,由於這個preset會根據當前配置的瀏覽器環境自動加載相應的polyfill,而不是所有進行加載,從而達到減少打包體積的目的
// .bablerc
{
"presets": [
[
"@babel/preset-env", {
"useBuiltIns": "usage", // 'entry/false'
"corejs": 3
}
]
]
}
複製代碼
useBuiltIns
有三個選項
usage 當使用此選項時,只須要安裝@babel-polyfill
便可,不須要在webpack中引入,也不須要在入口文件中引入(require/import)
entry 當使用此選項時,安裝完@babel-polyfill
以後,而後在項目的入口文件中引入
false 當使用此選項時,須要安裝依賴包,而後加入webpack.config.js的entry中
module.exports = {
entry: ["@babel/polyfill", "./app/js"],
};
複製代碼
在瀏覽器中使用,能夠直接引入@bable/polyfill
中的dist/polyfill.js
<script src='dist/polyfill.js'></script>
複製代碼
經過配合使用@babel/preset-env
以後,咱們能夠來看看編譯以後生成了什麼?
// polyfill/.babelrc
{
"presets": [
[
"@babel/preset-env", {
"useBuiltIns": "usage", // 其餘兩個選項 'entry/false'
"corejs": 3 // 若是須要使用includes,須要安裝corejs@3版本
}
]
]
}
// polyfill/index.js
const sym = Symbol();
const promise = Promise.resolve();
const arr = ["arr", "yeah!"];
const check = arr.includes("yeah!");
console.log(arr[Symbol.iterator]());
複製代碼
編譯以後的結果以下
能夠看到,瀏覽器中缺失的方法、對象都是直接引入的。當你只須要在特定的瀏覽器中作兼容時,能夠顯式地聲明,使用方式能夠參照browserslist-compatible。
{
"targets": "> 0.25%, not dead",
// 或者指明特定版本
"targets": {
"chrome": "58",
"ie": "11"
}
}
複製代碼
A plugin that enables the re-use of Babel's injected helper code to save on codesize.
@babel/plugin-transform-runtime
的主要有三個用處
@babel/runtime/regenerator
,當你使用了generator/async
函數(經過regenerator
選項打開,默認爲true)corejs
選項(默認爲false),會自動創建一個沙箱環境,避免和全局引入的polyfill產生衝突。這裏說一下第三點,當開發本身的類庫時,建議開啓corejs選項,由於你使用的polyfill可能會和用戶期待的產生衝突。一個簡單的比喻,你開發的類庫是但願兼容ie11的,可是用戶的系統是主要基於chorme的,根本就不要去兼容ie11的一些功能,若是交給用戶去polyfill,那就的要求用戶也必需要兼容ie11,這樣就會引入額外的代碼來支持程序的運行,這每每是用戶不想看到的。
// dev dependence
npm install --save-dev @babel/plugin-transform-runtime
// production dependence
// 由於咱們須要在生產環境中使用一些runtime的helpers
npm install --save @babel/runtime
// .babelrc
// 默認配置
{
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModules": false,
"version": "7.0.0-beta.0"
}
]
]
}
複製代碼
說了這麼多,下面來看一個示例
// transform-runtime/.babelrc
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"helpers": false
}
]
]
}
// transform-runtime/index.js
const sym = Symbol();
const promise = Promise.resolve();
const arr = ["arr", "yeah!"];
const check = arr.includes("yeah!");
class Person {}
new Person();
console.log(arr[Symbol.iterator]());
複製代碼
這裏暫時關閉了helpers
,咱們來看看編譯以後會是什麼結果
能夠看到,編譯以後,將Person class
生成了一個函數_classCallCheck
,你可能以爲一個生成這樣的函數也沒什麼特別大的關係,可是若是在多個文件中都聲明瞭class
,那就意味着,將會在多個文件中生成一個這麼如出一轍的工具函數,那麼體積就會變大了。所以,開啓了helpers
以後,效果又是怎樣的呢?
能夠看到,須要生成的方法變成了引入的方式,注意引入的庫是@babel-runtime
下面來試試開啓了corejs
選項以後生成的文件是啥樣的?
能夠看到全部的工具方式都來自於@babel/runtime-corejs2
,由於是獨立於polyfill生成的,因此不會污染全局環境。
monorepo
項目