babel 笔记录
学习初衷
在学习之前,我对 babel 的第一印象: 熟悉而又陌生。
为什么熟悉?
因为在开发过程中,我了解:
- TypeScript 转化为 JavaScript,需要 babel 处理
- React 的 jsx 转化为 JavaScript,需要 babel 处理
- 为了兼容浏览器,es6 转化为 es5,需要 babel 处理
- ...
那么为什么又陌生呢?
因为它就像一个黑盒,虽然知道它的存在,但是呢,具体怎么使用,内部的执行流程大致是怎么样的,这些都是未知的。
为了打开这个黑盒,跟随 coderwhy 老师深入学习了一下,加深自己对 babel 的理解,记录下此篇。
学习路线
为什么需要 babel ?
在前面的初衷已经提及到了,针对前端技术栈的开发,大多数都是离不开 babel 的。虽然,针对大多数开发人员不会直接去接触 babel(当然我也身处其中),但是学习 babel 对于理解代码从编写阶段到上线阶段的一些列转化过程,是至关重要的,想要提升自己,也是不可缺少的一环。
借用 coderwhy 老师常说的一句:了解真相,你才能获得真知的自由。
babel 概念描述及案例使用
babel 中文文档:babel.docschina.org/docs/en/
Babel 是一个 JavaScrip compiler(JavaScrip 编译器)。
babel 是一个工具链,主要用于在当前和旧的浏览器或环境中,将 ECMAScript 2015+ 代码转换为 JavaScript 向后兼容版本的代码。
- 转换语法
- Polyfill 目标环境中缺少的功能(通过如 core-js 的第三方
polyfill
) - 源代码转换(codemods)
- ...
其实上面解释这么多,就可以简单的概括:
babel 是一个 JavaScrip 编译器,把一段源代码编译成另外一段源代码,供浏览器识别。
babel是一个独立的工具(跟 postcss 一样),不需要借助任何的构建工具,也是能独立的运行使用。
在使用工具之前,也是需要安装的。
# 安装
pnpm install @babel/core @babel/cli -D
# @babel/core: 7.21.8
# @babel/cli: 7.21.5
@babel/core
:babel 的核心代码@babel/cli
:用于从命令行编译文件(终端输入命令,编译文件);各种入口的脚本命令都位于babel-cli/bin
的顶级包中。
Note:Please install
@babel/cli
and@babel/core
first beforenpx babel
, otherwisenpx
will install out-of-datedbabel
6.x.注意:在使用 npx babel 之前,必须先安装 @babel/core 和 @babel/cli,否则 npx 会自动安装过时的 babel 6.x 版本
babel 案例小测
这里就不直接上代码了,直接看截图,效果杠杠的。
上面这个截图,在 src 文件夹下有个 index.js 文件,通过编译命令后,生成 dist 文件夹及下面的 index.js 文件。
编译命令:npx babel src --out-dir dist
- src: 源文件
- dist: 目标文件(生成文件)
- --out-dir:output 输出,dir 目录
小小的测试了一下,生成文件夹(dist)跟源文件夹(src)的目录结构是一样的
细心的观察就会发现,上面编译后的文件,内容是基本上没有变的(除了去掉了一些空行)。为什么呢?
上面只是说明了 babel 是具有对源文件进行编译能力,但是怎么具体编译,或者说编译规则需要手动告诉它吧。就类似于建筑工给老板盖楼房,老板不说怎么建,建筑工也不知道怎么动手呀。
插件(plugin)引出
插件的出现就是为了让我们手动告诉 babel 应该如何编译。
- let / const 转化成 var,就需要
@babel/plugin-transform-block-scoping
- 箭头函数转化为普通函数,就需要
@babel/plugin-transform-arrow-functions
- ...
插件手动安装(推荐: 开发依赖)
pnpm install @babel/plugin-transform-block-scoping @babel/plugin-transform-arrow-functions -D
那么再次执行编译命令,也把插件带上,命令如下(如果有多个插件,用逗号分割,且不能有空格):
npx babel src --out-dir dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions
再次对比,发现 let
变成 var
,箭头函数
变成普通函数
,编译成功。
但是呢,可以发现对象的解构是没有被转化的,因为也是需要对应的插件。
那么问题就来了,如果 JavaScrip 发展这么快,新的语法如雨后春笋,那么不可能每个插件都需要手动去安装吧。
那么需要怎么做呢?
预设(preset)引出
为了编译 JavaScrip 新的语法,就需要对应安装多个插件。如果不嫌麻烦,这不失为一个办法。但绝不是最优解。
最优解:预设(@babel/preset-env) 。
何为预设?就是里面已经收集各种插件,只需要安装一个预设,就能享受所有插件福利。
安装:
pnpm install @babel/preset-env -D
安装之后,重新编写编译命令:
npx babel src --out-dir dist --presets=@babel/preset-env
看看效果,是不是很完美(新的语法特性都被编译成 ES5 了)。
这就是众所周知的 babel,一个可以编译文件的工具。
babel 的底层实现流程
在上面已经了解到 babel 是一个 JavaScrip 编译器。
编译器的作用就是把一段源代码转化成另外一段源代码;针对与 babel 来说,转化后的代码供浏览器认识。
babel 也是拥有编译器的工作流程:
- 解析阶段(parse)
- 转化阶段(Transformation)
- 生成阶段(Code Generation)
下面大致画了一张简单的流程图:

第一列:就是解释器的三个阶段(每个阶段都有着具体工作);
第二列:就是每个阶段具体要发生的事件(工作)。
其实整体的流程还是比较好理解的。
下面 github 地址是一个大佬使用 JavaScrip 写的一个小型编译器,麻雀虽小,五脏俱全。
下面是截取源码的 compiler 函数(编译器)
还是可以很清晰的看出,编译器的三个阶段:parser
、transformer
、codeGenderator
。
有兴趣的话,可以进去看看,每个阶段的代码具体是怎么实现的。
babel 实战应用
针对现阶段的前端开发,都是离不开构架工具的(比如说:webpack / vite 等)。当使用了构建工具,就会生成一个打包文件(比如:build.js),然后部署到线上去,与浏览器进行交互。
但是在打包这个阶段,是没有对代码进行转化的,存在浏览器不认识的问题。
而 babel 对源代码转化生成另外一段源代码,该段源代码是能够完全被浏览器认识的。
所以说,就可以想办法,先让 babel 对源代码进行转化,然后构建工具对转化后的代码进行打包,那么打包出来的代码,就能够完全被浏览器识别了。
就类似该形式,babel 在中间横插一脚。babel 先对源码进行转化,然后 webpack 打包转化后的代码。那么现在的问题就是:babel 如何插进去?
下面都是以 webpack 为例了
webpack 在构建执行的过程中,就会对代码进行编译,大致是这样的:
- 调用 Compilation 类的 build 实例方法,然后对代码进行遍历,生成依赖性,构建 module 模块
- 构建 module 是通过调用 类 Compilation 的 buildModule 方法,形成 module 对象(key: filePath, value: sourceCode)。而在此过程中,就会存在一个步骤,loader 对 sourceCode 的转化处理。
webpack 的 loader 本质就是一个函数,调用该函数,根据 loader 规则,对代码进行转化,返回新的代码。
在上面就发现了,可以通过 loader 对 babel 进行插入,那么在构建 module 的时候,就会进行转化,生成新的代码,进行 webpack 后面的一些列操作。
所以,社区里面也推出了 babel-loader
。
(理解)webpack 中使用 babel-loader
使用之前,少不了安装。
pnpm install babel-loader -D
先来简单的写写 webpack.config.js
文件的配置内容(这里就不多加深究了)
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "build.js",
path: path.resolve(__dirname, "build"),
},
mode: "development",
devtool: "source-map",
};
就简单的几个配置,入口
,出口
,模式
,source-map
四个配置。
mode 采用 development,因为 production 会压缩代码,不好进行观察。
devtool 采用 source-map,因为 development 的 devtool 默认值为 eval,那么打包文件就会存在 source-map 文件干扰;直接配置 source-map,就直接生成 source-map 文件,排除打包文件中的多余代码干扰。
如果对 source-map 不是很理解,可以看看我的另外一篇:
准备工作完成了,那么就来添加对 babel-loader 的配置。
// webpack.config.js
module.exports = {
...
module: {
rules: [
{
test: /.js$/,
use: [{ loader: "babel-loader"}],
},
],
},
}
其实跟上面的 babel 案例小测 一样,没有添加插件,没有添加预设,就简单的代码解析然后生成,其内容保持不变。
那么就需要引出插件(plugin),引出预设(preset),其配置如下
module.exports = {
...
module: {
rules: [
{
test: /.js$/,
use: [
{
loader: "babel-loader",
options: {
// plugins: [
// "@babel/plugin-transform-arrow-functions",
// "@babel/plugin-transform-block-scoping",
// ],
presets: ["@babel/preset-env"],
},
},
],
},
],
},
}
效果很 nice。
(理解)babel.config.js
当把所有 babel 内容都写到 webpack.config.js
中就显得比较的臃肿,那么这时候也可以把 babel 里面的配置项抽取出来,放到一个叫 babel.config.js 的文件中,当使用 babel 解析 的时候,就会自动来读取该文件。
// babel.config.js
module.exports = {
// plugins: [
// "@babel/plugin-transform-arrow-functions",
// "@babel/plugin-transform-block-scoping",
// ],
presets: ["@babel/preset-env"],
}
然后 webpack.config.js 只需要简单的使用 babel-loader 即可
{
test: /.js$/,
use: [{loader: "babel-loader"}],
}
(理解)babel 适配浏览器
在使用 babel-loader 之后,可以对代码中的新语法转化成比较旧的语法,适应浏览器。但是,可以继续深入的想想,有些语法真的需要转化吗?
为什么这么说呢?虽然 JavaScrip 语法在发展,但是浏览器也在发展呀。那么造成的情况就是新的浏览器是可以识别一些 JavaScrip 新的语法。既然可以识别,那转化就没有必要了嘛;因为转化过程还会造成一定的耗时,造成性能浪费。
版本较新的浏览器支持新的语法(不需要转换),版本较低的浏览器是不支持新的语法(需要转化),但是又不能指定用户使用什么浏览器,安装浏览器的什么版本。所以代码针对浏览器肯定不能单独适配,只能范围适配,兼容大多数的用户。那么怎么指定范围性的浏览器呢?
浏览器的市场占用率:统计了市场上各个浏览器,各个版本的使用率。
查看各个浏览器及版本的市场占用率:caniuse.com/usage-table
那么在处理代码兼容问题的时候,就不用去适配很少使用的浏览器,直接兼容使用率较高的浏览器。那么对于 babel-loader 应该怎么配置呢?
module.exports = {
module: {
rules: [
{
test: /.js$/,
use: [
{
loader: "babel-loader",
options: {
// presets 可能存在多个预设,是一个数组
// 针对每个预设,需要接受 options 配置,也需要写成一个数组,如果没有配置,就不需要写成数组
presets: [["@babel/preset-env", {
targets: '> 5%' // 配置浏览器使用率大于 5% 的
}]],
},
},
],
},
],
},
}
配置 5%
,稍微把使用率提高点,那么浏览器的版本就新点,为了可以看到实际的效果
可以发现,ES6 的语法没有被转化,那么实际就说明,有些浏览器是支持 ES6 的语法了,是不需要转换的。
那么这样就完美了吗?当然不是。
前端中的三剑客(HTML、CSS、JavaScrip)除了 HTML 很少考虑兼容性外,CSS 和 JavaScrip 考虑兼容性就太常见了。那么这时候的想法是兼容性肯定是一致的,肯定不是各兼容各的(就比如上面配置 babel 预设,就只兼容了 JavaScrip)。那么这时候就需要一个统一的文件来配置浏览器兼容性问题,然后无论是CSS 处理兼容性,还是 JavaScrip 处理兼容性都去读取这个统一配置文件,那么这时候两边都保持了一致。
那么如何配置这个统一文件呢?就需要借助一个工具 browserslist
既然它跟 babel 一样,都是工具,那么也是需要进行安装的。
pnpm install browserslist -D
可以简单的运行一个命令:看看浏览器占有率大于5%的有哪些
npx browserslist "> 5%"
安装了 browserslist,接下来就是该怎么使用了,有两种方式:
- 在 package.json 中配置一个属性
browserslist
{
"browserslist": [
"> 1%",
"last 2 version",
"not dead"
]
}
- 创建一个 .browserslistrc 文件
> 1%
last 2 version
not dead
当配置好后,无论是 postcss(css 处理兼容性的工具) 还是 babel(JavaScrip 处理兼容性的工具),都会去读取该文件或者配置,进行浏览器的兼容性处理。
browserslist 的配置命令具体怎么编写就不多说了,网上很多,github 上也有。
对上面三个配置的解释:
> 1%
: 筛选出浏览器市场占有率大于 1% 的浏览器;
last 2 version
: 筛选出每个浏览器的最后 2 个版本;
not dead
: 筛选出没有死掉的浏览器;dead 表示 24 个月内没有官方支持或更新的浏览器
还有一些其他的写法,就自己去学习啦!!!
(理解)polyfill
在上面已经了解到 babel 对 JavaScrip 代码进行转换(比如说:ES6 转化为 ES5)。但是呢,就是还会存在一种情况,ES6 或及以上新增的语法,比如说:
- promise
- fetch
- 数组新增的方法 api,字符串新增的方法 api
- ...
这些都是新增的语法,新的概念,如何转化?
再讲的通俗点,新增的语法都是函数(promise, fetch, includes),在转化的过程中,一个简单的函数调用,以前的浏览器能认识吗?肯定可以呀;需要转化吗?肯定不需要呀。
既然不转换这些代码,那么函数(promise,fetch)的实现体在哪?找不到函数的实现体,肯定就会报错,那么该如何解决呢?
那么这时候 polyfill
就出现了,就是为了解决类似问题。
不知道为什么会使用 polyfill 这个单词,意思挺不相近的。就简单理解为一个补丁
。使用了polyfill 就类似于打上了一个补丁,该补丁里面就包含了 promise,fetch 等函数体的实现。
怎么使用呢?
在 babel 7.4.0 之前,可以使用 @babel/polyfill
的包,但是该包现在已经不推荐使用了。
babel7.4.0 之后,可以通过单独引入core-js
和 regenerator-runtime
来完成polyfill的使用。
安装:
pnpm install core-js regenerator-runtime -D
使用(配置预设的 options,为什么是双数组,上面已经提及到了):
// babel.config.js
module.exports = {
presets: [
[
"@babel/preset-env",
{
corejs: 3.3, // 执行 core-js 的版本(可能每个版本实现的函数体有点差异吧)
useBuiltIns: "usage",
},
],
],
};
corejs
: 指定 core-js 使用的版本(可能每个版本的实现体有所不一样吧)useBuiltIns
: 指定如何形式使用 polyfill,有三个属性值。
useBuiltIns 三个属性值:
- false:就是不使用 polyfill(既然设置为false,就没有必要配置了)
- usage:自动检测所需要的 polyfill,简单理解成 按需引入。(使用了 promise 函数,就引入 promise 函数)
打包出了一大堆,里面针对 Promise 函数的实现。
- entry:当使用的第三方库里面存在新的语法函数,使用
usage
是检测不到的。那么就只有采用一种极端的手法,在入口处全部导入。
那么这时候,打包的代码量的体积就更加的庞大了。
小知识点:
既然在入口文件处已经导入了所有的 polyfill 代码,那么还需要设置
useBuiltIns: entry
吗?或则说,还需要配置 polyfill 吗? 因为 entry 就表示从入口文件导入。但是呢,都知道 webpack 也是从入口文件开始解析。简单的来说,都不需要你说,webpack 也知道解析。还是需要配置的。因为虽然从入口文件导入了,是会进行解析。但是只有你配置了 useBuiltIns:entry,才会去读取 browserslist,polyfill 也是会存在浏览器使用范围的。
useBuiltIns 值的选择:
- 性能达到最高,选择 usage;
- 严谨性做的最好,选择 entry;
(理解)其他 babel 预设
- @babel/preset-react
- @babel/preset-typescript
@babel/preset-react
在编写 react 代码时,react 使用的语法是 jsx,jsx 是可以直接使用 babel 来转换的。
针对 react jsx 语法的转化也是需要安装很多插件的,所以也是需要采取预设的方式。
pnpm install @babel/preset-react -D
// babel.config.js
module.exports = {
presets: [
["@babel/preset-env"],
["@babel/preset-react"], // 新增 @babel/preset-react 预设
],
};
这里就直接案例截图:
案例运行成功。
案例后的体会:
代码是需要多敲的。 在这里自己犯了一个错误,想了很久(但是错误的原因,就是自己无知认为的理所当然)。在平时的 React 开发过程中,React 17是不需要显示导入
import React from 'react'
,采用了全新的 JSX 编译(react/jsx-runtime)。但是在上面的案例中,安装的是 React 18,就想当然得认为也是不需要导入 React 的,结果报错找了好久。
所以,代码需要多敲,多多理解。(全新的JSX编译,是 create-react-app 做的事,自己写案例又没有做,肯定报错卅 )
@babel/preset-typescript
TypeScript 最终都会被编译成 JavaScript,这是没有疑问的,因为浏览器不认识 TypeScript。
但是把 ts 转化为 js 有两种方式:
- ts-loader:利用 tsc 转化 ts 代码
- babel-loader:利用 babel 转化 ts 代码
那么采用哪一种呢?
测试 ts-loader
pnpm install ts-loader -D
// webpack.config.js
module: {
rules: [
{
test: /.ts$/,
use: [{ loader: "ts-loader" }],
},
],
},
编写 ts 代码,引入到 webpack 的入口文件,执行打包命令。
效果正常。
但是如果在 index.ts 中,调用 foo(123)
,这时候类型是不匹配的,执行打包命令是不成功的。自己可以动手试一下,就是会提示类型错误 TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
// package.json
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"build": "webpack",
"watch": "tsc --noEmit --watch"
},
先来了解 tsc 的两个指令。
- noEmit:不生成编译文件
- watch:实时检测 ts 的语法
在编写 ts 代码时,开启 watch 模式,帮你找出错误问题。
测试 babel-loader
这里就不多做解释插件和预设之间的关系了,直接安装预设(@babel/preset-typescript)
pnpm install @babel/preset-typescript -D
// webpack.config.js
module: {
rules: [
{
test: /.ts$/,
use: [{ loader: "ts-loader" }],
},
],
},
// babel.config.js
module.exports = {
presets: [
["@babel/preset-env"],
"@babel/preset-react",
["@babel/preset-typescript"],
],
};
执行打包命令,发现效果正常。
babel-loader 有个很大的好处就是支持 polyfill,这是 ts-loader 不具备的。
但是如果 ts 代码中,存在错误的语法,babel-loader 也会打包成功,这是跟 ts-loader 最大的区别,也是选择 ts-loader 和 babel-loader 的关键因素。
所以问题来了,针对 ts 编译,该选择哪个 loader ?ts-loader 还是 babel-loader?
两种情况:
- 如果你想构建输出文件内容与源文件内容大致相同(也就是没有 polyfill 等),则使用 tsc,也就是 ts-loader。
- 你需要构建的输出文件可能存在多种结果(就是兼容浏览器,使用 polyfill,语法降级等),则使用 babel 进行编译,使用 tsc 进行检查(也就是说使用 babel,还使用 typescript 提供的工具 tsc)
(认识)Stage-X 的 preset
上面的一些列操作都是基本 babel 7.x
的版本,在 babel 6.x
配置预设是另外一种形式(state-x),这里了解即可。
Stage-X 表示什么意思呢?x 就是所谓的未知数,在初高中做数学题时,常用的变量。在这里 x 的值只有5种:0,1,2,3,4
。
小知识点的扩展:
一个新的语法出现到 ECSMScript 版本的几个阶段:
- stage-0:针对新的语法,产生念想。(客户想出一个新的需求)
- stage-1:提案,说成自己的想法,希望问题得到解决。(给研发说出需求,看研发能否实现)
- stage-2:写出初稿(基本功能实现,影响范围有多广)(研发初步设计,预估,看对以前的功能有多大影响)
- stage-3:候补阶段,功能基本实现,只是简单的修复 bug 文件,得到规范人员的认可等(需求基本开发完成)
- stage-4:完成阶段,提案将包含在 ECMAScript 的下一个修订版中(下个迭代,发版,上线)
配置是这样写的:
// babel.config.js
module.exports = {
presets: ['stage-0']
}
这里只是了解,如果遇到老的项目,看到了,有点印象就行了。这里测试就不演示了。
总结
工作繁忙,零零碎碎,花了接近三四天,终于把 babel 的知识点记录下来了。从 babel 认识到实战,基本上都手动敲打了一遍,基本上案例正确性可以得到保障,只不过文字描述可能存在误差,如有发现,多多指教,共同进步。
babel 笔记录到此结束!!!
转载自:https://juejin.cn/post/7235655870621057079