本文基于的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
项目