babel入门学习#
前言#
从我开始接触前端工程化到现在工作一年多,babel这个词就时不时出现在各种代码里,但是见到了并不意味着就知道了,尤其是各种babel的包结合起来配置,头都晕了,所以这次写个文章加深下自己的印象。
官方文档#
Babel · The compiler for next generation JavaScriptbabeljs.io/
简介#

开头直接简洁明了,就是一个javascript的解析器。主要就是将不兼容的ES6以上的api、语法等转换为大部分浏览器都兼容的es5。
主要能用于一下地方
- 语法转换,就是上面说的
- 帮你兼容一些低版本没有的
api - 源码转换,比如你用的
jsx,帮你转换为js - 其它。。。。
配置#
说的再多也不如直接实战
我们先创建一个文件夹和一个index.js文件,然后初始下环境
mkdir babel-study
cd babel-study
echo > indexjs
npm init --yes然后在我们的index.js文件中随便写点es6的东西,比如箭头函数或者Array.reduce之类的api。
const res = [[0, 1, 2].reduce((prev, current) => prev + current, 0)];
console.log(...res);然后在终端安装babel的核心包@babel/core、@babel/preset-env以及能让这两个包跑起来的命令工具@babel/cli
npm install @babel/core @babel/cil @babel/preset-env -D安装完包之后直接执行运行命令
./node_modules/.bin/babel index.js --out-dir lib然而你发现lib目录下生成的index.js里的代码依旧没改变,还是es6的东西,为什么没有转换呢?因为我们并没有做配置,命令读不到配置自然就按原来的输出了。
在介绍通过配置文件配置之前,我们先来看下通过参数传入,我们先安装@babel/plugin-transform-arrow-functions ,这个包一看就知道是用来转换箭头函数的。
npm install @babel/plugin-transform-arrow-functions -D那么我们要怎么用这个plugin呢?@babel/cli中提供了--plugins的option
./node_modules/.bin/babel index.js --out-dir lib --plugins=@babel/plugin-transform-arrow-functions
这里就转换完成了。但每次这样传参就麻烦,所以官方又提供了一种方式,通过配置文件。
让我们来创建一个babel.config.js的配置文件,当然建议用.json(>=7.8.0)的,js是旧版中的。
module.exports = {
plugins: ['@babel/plugin-transform-arrow-functions']
}
箭头函数正常转换了。但你应该注意到了reduce方法和...解构都没有转换为es5。
让我们改下配置
module.exports = {
presets: [
[ "@babel/preset-env" ],
],
};我们再重新跑一下指令。
./node_modules/.bin/babel index.js --out-dir lib 
箭头函数是转换了,...结构也通过apply的方式转换了,但是这个reduce并没有变。
在低版本是可以引入@babel/polyfill处理的,但是在7.4.0之后就废弃迁移到core-js包中了。
另外为什么没有自动转换呢?因为现在默认是不需要的,需要我们做其他配置。
首先,我们先安装下core-js,这个包中的代码都是参与打包的,所以不能放dev中。
npm install core-js --save然后我们改下babel.config.js文件中的配置
module.exports = {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: "3.25.5",
},
],
],
};useBuiltIns: 默认是false,不使用任何的polyfill。不为false的时候会直接从core-js中引入polyfill,其他无相关的将不会被引入。 其中usage会分析每一个module中需要引入的polyfill然后自动引入,是按需加载,同时代码是放在module内部的,不会污染全局,但当所有的module合并后自然会出现很多重复的导入(指编译后的代码,不影响实际效果,因为同一个polyfill只会被导入一次)。而entry需要开发者自己去全局入口引入core-js,这样全局就只有一份polyfill,但这份polyfill是全量的,会污染全局,不过你会有重复的导入问题。怎么用还是得看具体的场景,并不是说usage就会比较好。corejs: 你安装的core-js包版本,其中有2和3的版本,其中2的版本比3少了一些实例自身的属性。
另外,我们并不需要补充regenarator-runtime这个包,因为这个包如果没有配置会自动给我们加上。
让我们重新跑下指令

result
这下就正常转换了。并且只引入了我们需要的。
我们来看下useBuiltIns的usage和entry的不同。
usage
首先新建src文件夹,然后新增a.js、b.js、c.js以及index.js四个文件,其中abc三个文件导出方法除了名字其它都相同,index.js导入这三个文件导出的方法。
a\b\c.js
function a () {
const res = [1, 2, 3].reduce((prev, current) => (prev + current), 0);
console.log(res)
}
module.exports = {
a
}
index.js
const { a } = require('./a.js')
const { b } = require('./b.js')
const { c } = require('./c.js')
a();
b();
c(); 然后改下我们package.json中build的指令
"build": "babel src --out-file build.js" 然后终端直接执行npm run build
"use strict";
require("core-js/modules/es.array.reduce.js");
require("core-js/modules/es.object.to-string.js");
function a() {
var res = [1, 2, 3].reduce(function (prev, current) {
return prev + current;
}, 0);
console.log(res);
}
module.exports = {
a: a
};
"use strict";
require("core-js/modules/es.array.reduce.js");
require("core-js/modules/es.object.to-string.js");
function b() {
var res = [1, 2, 3].reduce(function (prev, current) {
return prev + current;
}, 0);
console.log(res);
}
module.exports = {
b: b
};
"use strict";
require("core-js/modules/es.array.reduce.js");
require("core-js/modules/es.object.to-string.js");
function c() {
var res = [1, 2, 3].reduce(function (prev, current) {
return prev + current;
}, 0);
console.log(res);
}
module.exports = {
c: c
};
"use strict";
var _require = require('./a.js'),
a = _require.a;
var _require2 = require('./b.js'),
b = _require2.b;
var _require3 = require('./c.js'),
c = _require3.c;
a();
b();
c(); 可以看到编译后的文件中两个polyfill被重复引用。
entry
首先将babel.config.js中的useBuildIns改为entry,然后在我们的index.js中将core-js包导入,然后重新执行我们的npm run build指令。

可以看到所有的polyfill都被引入了。
这里越过了@babel/polyfill这个包的使用,因为目前已经是不推荐了,但如果你接手了一个老项目,那还是得看一下的,这里放下官网链接
https://babeljs.io/docs/en/usage#polyfillbabeljs.io/docs/en/usage#polyfill
关于配置文件#
配置文件支持多种
babel.onfig.js/json这两种都是一样的,推荐json格式,上边已经介绍过了这种方式,就不多说了。作用范围是整个项目。.babelrc.json,作用范围局部,当前文件夹下,执行转换时会先往上找最近的一个.babelrc.json文件直到根目录,然后将这个.babelrc.json和全局的合并之后再获取配置信息。用法如下:
{
"presets": [...],
"plugins": [...]
} - 嫌麻烦,可以直接将配置信息写到
package.json文件中。
{
"name": "my-package",
"version": "1.0.0",
"babel": {
"presets": [ ... ],
"plugins": [ ... ],
}
}一些常用的配置#
已经被内置了的就不说了。
@babel/preset-react,看名字就知道是用于react的配置,我们先安装
npm install @babel/preset-react -D 然后将这个配置加到babel.config.js文件中
module.exports = {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "entry",
corejs: "3.25.5",
},
],
["@babel/preset-react"]
],
};接着创建react的文件夹,新建index.jsx,随便写点react的代码
import { ReactDOM } from 'React'
function test () {
const className = 'test'
const text = '666'
return (
<div className={className}>{ text }</div>
)
}
ReactDOM.render(test(), document.body) 然后package.json中新增script
"build-react": "babel react --out-dir lib" 然后直接执行指令npm run build-react

@babel/preset-typescript,看名字也知道这是个用来识别转换typescript的配置
npm install @babel/preset-typescript -D然后新增一个ts的文件夹,创建一个index.ts,随便搞点东西
interface Test {
(name: string | undefined): string | undefined
}
const test: Test = (name) => name;接着在package.json中新增指令,需要注意ts文件需要提供extensions的option
"build-ts": "babel ts --out-dir typescript --extensions .ts" 最后在babel.config.js中新增一个预设
[
"@babel/preset-typescript",
]然后运行指令npm run build-ts

实现一个babel插件#
如果大家用过webpack的插件,应该能发现都有同一个格式,需要定义class.apply才能被调用。
而babel也有自己的格式。
我们先创建一个plugins文件夹,新建log.js文件
module.exports = function() {
return {
visitor: {
Identifier(path) {
const name = path.node.name;
if (name === 'res') {
console.log(path.node)
}
},
},
};
}当找到name是res的变量时,打印这个节点。
然后我们将它引入到babel.config.js中。
module.exports = {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: 'usage',
corejs: '3.25.5'
}
],
[
"@babel/preset-typescript",
{
allExtensions: true
}
]
],
plugins: [require('./plugins/log')]
}
然后我们执行npm run build,目标既是我们的index.js文件

这里面标记了行列的位置,注释,起始结束位置等。

正好就是第一行第六列到第九列。
通过这个例子可以看出这个格式
module.exports = function() {
return {
visitor: {
// 监听对象,不一定是`identifier`
Identifier(args) {
//...
},
},
};
}这里就只展示这一个小demo,实际上一个plugin能做的事情很多,可以监听的东西也很多。因为传入visitor中的是被core编译转换后的ast(抽象语法树)
@babel/cli#
你是否觉得这个指令的执行者./node_modules/.bin/babel很奇怪?实际上node_modules/.bin中存放的是所有第三方包里的命令执行文件入口,如果这个包配合commander和全局安装,就能做到类似npm这样的全局命令执行工具。可以看下我之前的文章。
https://zhuanlan.zhihu.com/p/569233377
而这个.bin中的命令来源是包自身带有的bin文件夹中的,比如这个@babel/cli中的bin文件夹。

要怎么分辨呢?直接去看package.json中如何配置的即可。

如果你嫌弃./node_modules...这种执行太冗长,你可以在package.json中新增一个script
"build": "babel index.js --out-dir lib"为什么这样会生效呢?因为@bael/cli这个包配置了这个npm指令即上面提到的package.json文件中的bin.babel。
我们跟着进入babel.js中,发现是导入cli/lib/babel这个包,入口自然是index.js文件
#!/usr/bin/env node
"use strict";
var _options = require("./options");
var _dir = require("./dir");
var _file = require("./file");
const opts = (0, _options.default)(process.argv);
if (opts) {
const fn = opts.cliOptions.outDir ? _dir.default : _file.default;
fn(opts).catch(err => {
console.error(err);
process.exitCode = 1;
});
} else {
process.exitCode = 2;
}这段代码很简单,就是将命令行带上的参数传入options.js中,转换后判断是否有配置--out-dir,如果有则按照传入的来,否则使用默认输出路径。
简单的说下options.js中做了什么。 就不看代码了,毕竟这不是源码分析。。。
其实这里面也用了commander,装载了一堆的option,唯独没有配置指令的名字。。。
抛去commander的配置,还剩200多行代码。
主要做了以下几件事。
- 获取命令行中传入的参数
- 判断参数是否符合要求,不符合报错
return - 以对象的格式返回转换后的参数
然后参数传入dir.js中。
这个文件中代码写的很绕,一个套一个的,最终找到的compile方法居然在utils里。。。。
这里面主要做了以下的俩件事
- 读文件,没有读怎么写呢,是吧?doge
- 遍历读到的符合要求的文件(比如非
ignore的文件),调用compile方法,引入core包,将这些文件里的源码转换成我们想要的兼容低版本的代码。
@babel/preset-env#
这个包相信大家也差不多猜到了,在学的过程中需要通过它来做兼容判断。
先来看下官方对它的描述

简单的说就是可以帮你自动去做兼容等,不需要你去做额外的处理比如引入插件等。当然,有些东西需要额外配置才能生效,比如polyfill,需要配置useBuiltIns以及corejs。
同样不分析源码,简单的说先是判断当前环境,比如node,然后通过options判断加载不同plugin。
来看下常用的option
targets:你希望支持的哪些浏览器的哪些版本,会根据你的配置,往你的bundle中加点兼容性东西,让你的代码可以正常运行在你想要的浏览器中。 当然,如果你配置了browserlist,也会根据browserlist的来做判断。默认不配置,会根据默认情况执行,尽量帮你兼容所有浏览器的低版本。include:和下面的exclude同类型的作用,用于让某些plugin一定会被调用。exclude:让某些plugin一定不会被调用。useBuiltIns:用来处理polyfill的,默认false不加载polyfill。其它不多说了,配置这一块里有对usage和entry的分析。corejs:只有配置了useBuiltIns: usage/entry这个字段才会生效。默认是版本2,2不可以传入具体的版本 ,3则可以。用于加载core-js的包,而core-js需要自行安装并确保包版本和你这配置里的是一致的(指的是core-js2只能配置2,不能配置3,core-js3同样道理)。browserlistEnv:基于browserlist的配置。
@babel/core#
看名字就知道这个包的重要性,是用来处理源码转换代码的核心包。
我们直接来实战
我们先创建一个transform的文件夹,新建index.js文件。
mkdir transform
cd transform
echo > index.js 然后我们在index.js中将@babel/core包中的transform方法导入。
const { transform } = require('@babel/core');
const test = `
const a = name => name;
`
transform(test, {}, (...args) => {
console.log(args)
}) 我们执行下这个文件node transform/index.js

可以看到我们的箭头函数确实被转换成function的形式了。
这里依旧不会去分析源码,只需要知道这里面有一个解析器@babel/parser,将代码转换成object,经过@babel/traverse加工处理后,再调用@babel/generator从object转换为最终的代码。
@babel/parser#
上面说到了,将你的代码"字符串·"转换成抽象语法树(AST),至于怎么转的,这里就不说了,因为这块逻辑是很复杂的,简单的说就是将你的code进行遍历,将每个字符都过一遍,当然这不是简单的遍历。它里面内置了几十个javscript特殊字段(比如const、let、var这些,尤其是空格),根据这些字段,将“不稳定”的状态 转变为稳定状态(比如你定义一个变量,最开始是不确定你这个是不是变量的,但是遍历过前后之后再配上这些特殊字段判断就能确定了),最终将你的code string变成一颗”树“object。AST的应用是非常非常常见的,比如你的vue的虚拟dom就是一种AST,还有你现在正在用的IDE,都是基于AST的。
我们来调用下这个包
const parser = require('@babel/parser')
const test = `
const a = name => name;
`
const parseResult = parser.parse(test);
console.log(parseResult)
又或者你可以将上面@babel/core中的transform传入的参数第二个插入一个options,里面传入ast: true,也是一样的。
const { transform } = require('@babel/core');
const test = `
const a = name => name;
`
transform(test, {
ast: true
}, (...args) => {
console.log(args)
})

@babel/generator#
这个core包中也提到了,主要是将你的ast转换为string。当然,这里拿到的ast不是原版从parser中拿到的ast,是经过@babel/traverse执行各种plugin加工过后的。
我们直接来调用下这个包
const { transform } = require('@babel/core');
const parser = require('@babel/parser')
const generator = require('@babel/generator')
const test = `
const a = name => name;
`
transform(test, {
ast: true
}, (...args) => {
// console.log(args)
})
const parseResult = parser.parse(test);
// console.log(parseResult)
const generateResult = generator.default(parseResult)
console.log(generateResult)
这里的code里的箭头函数居然没有转换成function的形式,这是为什么呢?因为我们没有配置。。。。。之前@babel/core能正常转换是因为里面做了读取配置的处理,而generate/parser都是功能单一的包,不可能会做读取的处理。 这也符合官方的描述 --“默认不处理代码,处理都交由插件“。那么要怎么变成我们想要的呢?
你应该注意到了generator处理的是ast,所以如果我们改动ast的话到时它输出也就是我们改动了的,官方包里也是这么做的。
而改动我们需要用到另一个包@babel/traverse来帮助我们将箭头函数变为function的形式。
@babel/traverse#
这个包是用来更新ast的。
接着我们上面的代码
const { transform } = require('@babel/core');
const parser = require('@babel/parser')
const generator = require('@babel/generator')
const traverse = require('@babel/traverse')
const test = `
const a = name => name;
`
transform(test, {
ast: true
}, (...args) => {
// console.log(args[1].ast)
})
const parseResult = parser.parse(test);
traverse.default(parseResult, {
ArrowFunctionExpression (path) {
path.arrowFunctionToExpression({
allowInsertArrow: false,
noNewArrows: true,
specCompliant: false
});
}
})
const generateResult = generator.default(parseResult, {
jsescOption: {
es6: false
}
})
console.log(generateResult)然后终端执行下node transform/index.js

转换成功!
@babel/types#
提供了babel内置的类型,比如上面提到过的Identifier和ArrowFunctionExpression。
这里就不多说了,感兴趣的大佬可自行点下【18】去官网看下。
@babel/template#
看名字就知道是和模板有关的。
我们简单的搞下即可,因为重要的包前面几个已经讲完。
官方提供了两种替换变量的方式
%%xxx%%的方式XXX的方式
const template = require('@babel/template');
const getTemplate = template.default(`console.log(TEST)`)
const ast = getTemplate({
TEST: '666'
})
console.log(ast)
template`方法会返回一个`function@babel/runtime#
还记得上面提到过的polyfill和配置不?是通过注入代码参与打包来实现转换的,而这些一般叫做runtime。
babel也是需要注入一些runtime来帮助代码进行兼容的。
我们先在transform文件夹中创建一个文件test-runtime.js
async function test () {
await console.log(123);
return 666
}然后package.json新增script
"build-runtime": "babel transform/test-runtime.js --out-file transform/buildRuntime.js" 接着npm run build-runtime
输出的buildRuntime.js中多了一堆意义不明的玩意儿。

而这些东西就是async await的辅助函数。
让我们在创建一个test-runtime2.js,然后让这个文件也参与编译

发现这些辅助函数每次都会被引入,这也就意味着存在大量的重复代码。
为了处理这个问题,官方就将这些个runtime都抽出来放到了这个@babel/runtime包中。
那么要这么做才能处理掉这些重复代码呢?
我们直接在入口引入对应的包即可。
var _asyncToGenerator = require('@babel/runtime/helpers/asyncToGenerator')
var _regeneratorRuntime = require('@babel/runtime/helpers/regeneratorRuntime')
// ...但这样每次都得手动去引入,很麻烦。有没有办法能做到和core-js一样自动引入?
有的,官方又提供了一个包@babel/plugin-transform-runtime,这个包能自动引入。
我们先安装
npm install @babel/plugin-transform-runtime -D然后在我们的babel.config.js中配置下
module.exports = {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: 'usage',
corejs: '3.25.5'
}
],
[
"@babel/preset-typescript",
{
allExtensions: true
}
]
],
plugins: [require('./plugins/log'), "@babel/plugin-transform-runtime"]
}
这回就自动引入啦
@babel/code-frame#
这个包提供了自定义行列位置的log,就不多说了,感兴趣的大佬可以点击【19】看下。
随便搞下。
const { codeFrameColumns } = require('@babel/code-frame')
const rawLines = `class Foo {
constructor() {
console.log("hello");
}
}`;
const location = { start: { line: 2, column: 16 } };
const result = codeFrameColumns(rawLines, location, {
/* options */
});
console.log(result);
那么babel的基础就学完了,如果觉得对你有用麻烦点个赞~谢谢!
参考#
- core-js https://www.npmjs.com/package/core-js
- useBuiltIns https://babeljs.io/docs/en/babel-preset-env#usebuiltins
- @babel/preset-react https://babeljs.io/docs/en/babel-preset-react
- @babel/preset-typescript https://babeljs.io/docs/en/babel-preset-typescript
- preset-env https://babeljs.io/docs/en/babel-preset-env
- @babel/preset-env_targets https://babeljs.io/docs/en/babel-preset-env#targets
- @babel/preset-env_include https://babeljs.io/docs/en/babel-preset-env#include
- @babel/preset-env_exclude https://babeljs.io/docs/en/babel-preset-env#exclude
- useBuiltIns https://babeljs.io/docs/en/babel-preset-env#usebuiltins
- @babel/preset-env_corejs https://babeljs.io/docs/en/babel-preset-env#corejs
- browserlistEnv https://babeljs.io/docs/en/babel-preset-env#browserslistenv
- @babel/core https://babeljs.io/docs/en/babel-core
- @babel/parser https://babeljs.io/docs/en/babel-parser
- @babel/generator https://babeljs.io/docs/en/babel-generator
- @babel/parser https://babeljs.io/docs/en/babel-parser
- @babel/generator https://babeljs.io/docs/en/babel-generator
- @babel/traverse https://babeljs.io/docs/en/babel-traverse
- @babel/types https://babeljs.io/docs/en/babel-types
- @babel/code-frame https://babeljs.io/docs/en/babel-code-frame
发布于 2022-10-24 18:17
