Babel 是一个工具集,主要用于将 ES6
版本的 JavaScript
代码转为 ES5
等向后兼容的 JS 代码,从而可以运行在低版本浏览器或其它环境中。
因此,你完全可以在工作中使用 ES6
编写程序,最后使用 Babel 将代码转为 ES5
的代码,这样就不用担心所在环境是否支持了。下面是一个示例:
转换前,代码里使用 ES6
箭头函数
const fn = num => num;
转换后
var fn = function fn(num) {
return num;
};
上述就是 Babel 主要做的事情
前言
使用之前,我们先搭建一个简单的 webpack 环境,用于执行编译我们的代码(当然也可以使用 @babel/cli
,在这里使用较为常见的 webpack)
项目准备
mkdir babel-demo
cd babel-demo
npm init -y
npm i webpack webpack-cli -D
- webpack: webpack 核心库
- webpack-cli: webpack 命令行工具库
在 `package.json 添加一条 build 脚本
"scripts": {
"build": "webpack --mode development --devtool false"
},
--mode development
: webpack 核心库
--devtool false
: 禁止生成 sourceMap,以便我们查阅编译后的代码,不开启的话源码则会被放在 eval
函数内
添加入口文件
在 根目录下创建 src 文件夹,在 src 在 创建 index.js 文件,添加以下代码
src/index.js
是 webpack 的默认入口文件的路径
export default class Animal {
constructor(name) {
this.name = name;
}
say() {
console.log('My name is:', this.name);
}
}
上述是一个 ES6
的 class,大家肯定都知道 class 只是构造函数的一个语法糖。虽然 class 在大部分主流浏览器都已经完全支持,但是也依然存在着一些”钉子户“,所以我们的此次的目标就是把它打包成构造函数。
执行 npm run build
,先看看 webpack 如果不配置任何东西,打包的结果是怎样的。
可以看到根目录下生成了一个 dist 目录,其中 main.js 是我们的打包后的文件。
文件内容主要分为两部分,上面主要是 webpack 的运行时代码,最底部是我们的源码。同时可以看到,webpack 原封不动的把我们的源码搬了过来,什么都没做,这不是我们想要的。当然这不也是 webpack 的问题,它本身的定位就是个模块打包器。
引入 babel
npm i @babel/core @babel/preset-env babel-loader -D
@babel/core
: babel 核心库
@babel/preset-env
: babel 官方内置预设,它提供了 ES6
转换 ES5
的语法转换规则,后续会在 babel 配置文件里使用到它
babel-loader
: webpack loader 插件,配置 webpack 解析规则的时候,可以指定使用 babel 去编译 js 文件,可以理解为 webpack 和 babel 的连接桥梁
配置 webpack
由于需要使用到 webpack loader,为了方便配置,我们在根目录下创建一个 webpack 配置文件 webpack.config.js
关于更多 webpack 配置细节可以查看 这篇文章
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'] // 启用转换
}
}
]
}
]
}
};
也可以把 此处的 options 配置在外部文件中,可以命名为 babel.config.js
、 .babelrc
、.babelrc.js
或 babel.config.json
等,babel 会去自动去寻找这些路径,详情见 babel 官网
重新执行 npm run build
,观察 dist/main.js 文件,翻到最底部,可以看到 class 已经被成功转换成了 构造函数
了。
然而细心的朋友可能看到除了编译后构造函数上面,多了几行函数代码,这个问题我们后面会说到。
这就是一个最简单的 Babel 使用过程,我们把用 ES6 编写 class 类转换成了 ES5 的构造函数。
再次尝试下添加个 Promise API 到 入口文件中
export default class Animal {
constructor(name) {
this.name = name
}
say() {
console.log('My name is:', this.name)
}
}
+ export const p1 = () => {
+ return new Promise(resolve => {
+ resolve(1)
+ })
+ }
重新执行 build,会发现 dist/main.js
中 Promise
经过编译后还是 Promise,为什么
Babel 没有对 ES6 的 Promise
进行转换?
babel 默认只转换新的 JavaScript 语法,比如箭头函数、扩展运算(spread)。 不转换新的 API,例如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转译。则需要为当前环境提供一个垫片(polyfill)。
垫片的概念理解为一个桌子 4 条腿,当其中一条腿短了一截时,就垫个东西过去,使得 4 条腿长度一样从而保持桌子平衡:当其他浏览器不支持新的 API 的时候,那我们就预先把这些 API 给加上。
polyfill
不处理新的 API 肯定是不行的,如果项目运行在 ie 11 以下或者其他浏览器的低版本环境中,则会直接报错。
polyfill 一般主要有 2 种,一个是 @babel/polyfill,一个是 @babel/runtime,下面来看这 2 种的区别。
@babel/polyfill
目前最常用的配合 Babel 一起使用的 polyfill 是 @babel/polyfill
(如果你使用的 babel 版本低于 7 则应该使用 babel-polyfill
) ,通过改写全局 prototype 的方式实现,会加载整个 polyfill,针对编译的代码中新的 API 进行处理,并且在代码中插入一些帮助函数,比较适合单独运行的项目。
# 因为这是一个 polyfill (需要在你的源代码之前运行),我们需要让它成为一个 dependencies devDependencies 。
npm i @babel/polyfill -S
在任意项目的入口文件顶部引入 polyfill 文件
+ import '@babel/polyfill'
export default class Animal {
constructor(name) {
this.name = name
}
say() {
console.log('My name is:', this.name)
}
}
export const p1 = () => {
return new Promise(resolve => {
resolve(1)
})
}
配置 browserslist
babel 会根据 package.json 中的 browserslist 设定的浏览器范围进行编译,当然也可以将此字段独立成一个文件,这里在 package.json 中配置。
{
"browserslist": {
">0.5%",
"not dead",
"not op_mini all"
}
}
根据你需要兼容的环境,合理设置 browserslist,babel 会根据提供的属性按需引入 @babel/polyfill
下不同的工具库。
- 你需要兼容的浏览器版本越少,打包出来的依赖就更少,
- 反之则越多
- 不提供 browserslist 配置会把所有的 polyfill 全部打包!
更多关于 browserslist 的配置可见 https://github.com/browserslist/browserslist
重新执行 build 命令,可以看到 Promise 依然是 Promise,但是上面引入了出来许多的 polyfill 代码,现在新的 API 也可以安全的在低版本的浏览器中运行了。
@babel/polyfill
整合了 2 个核心库: core-js
和 regenerator-runtime
,我们也可以手动安装这 2 个库并添加到入口文件中。
因此,在不设置 browserslist 的前提下,你可以手动的引入一些你需要兼容的 API 到入口文件的顶部。
例如:某些移动端浏览器不支持 Promise
的 final
回调,你可以你的应用的入口文件顶部导入相关的 API 方法。
import 'core-js/fn/promise/finally.js';
// ...other
@babel/runtime
上面有说到,babel 编译 class 语法后,会生成一些辅助函数代码内联在编译后的文件中,如果每个文件里都使用了 class 类语法,那会导致每个转换后的文件上部都会注入这些相同的函数声明。这会导致我们用构建工具打包出来的包非常大。
一个思路就是,我们把这些函数声明都放在一个 npm 包里,需要使用的时候直接从这个包里引入到我们的文件里。这样即使上千个文件,也会从相同的包里引用这些函数。通过 webpac k 这一类的构建工具打包的时候,我们只会把使用到的 npm 包里的函数引入一次,这样就做到了复用,减少了体积。
Babel 为了解决上述问题,提供了单独的包 @babel/runtime
,把所有语法转换会用到的辅助函数都集成在一起。
npm install --save @babel/runtime
注:在安装 @babel/preset-env
的时候,其实已经自动安装了 @babel/runtime
,不过在项目开发的时候,建议再单独 npm install
一遍 @babel/runtime
。
打包后用到的三个函数都可以从 @babel/runtime
库中找到,我们可以直接引入。
手动替换辅助函数
var _classCallCheck = require('@babel/runtime/helpers/classCallCheck');
var _defineProperties = require('@babel/runtime/helpers/defineProperty');
var _createClass = require('@babel/runtime/helpers/createClass');
var Animal = /*#__PURE__*/ (function () {
function Animal(name) {
_classCallCheck(this, Animal);
this.name = name;
}
_createClass(Animal, [
{
key: 'say',
value: function say() {
console.log('My name is:', this.name);
}
}
]);
return Animal;
})();
这样就解决了代码复用和最终文件体积大的问题。不过,这么多辅助函数要一个个记住并手动引入,平常人是做不到的,我也做不到。这个时候,Babel 插件 @babel/plugin-transform-runtime
就来帮我们解决这个问题。
@babel/plugin-transform-runtime
有三大作用,其中之一就是自动移除语法转换后内联的辅助函数,自动使用 @babel/runtime/helpers
里的辅助函数来替代。这样就减少了我们手动引入的麻烦。
现在我们还要安装 Babel 插件 @babel/plugin-transform-runtime
来自动替换辅助函数
npm i @babel/plugin-transform-runtime -D
配置 babel 插件
{
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-runtime'] // 添加插件
}
重新执行 build 命令,可以看到,它生成的代码比我们完全手动引 @babel/runtime
里的辅助函数更加优雅。实际前端开发的时候,我们除了安装 @babel/runtime
这个包外,一定会安装 @babel/plugin-transform-runtime
这个 Babel 插件包的。
如何选择
那么,上面讲的 API 转换有什么用,明明通过 polyfill 补齐 API 的方式也可以使代码在浏览器正常运行?
其实,API 转换主要是给开发 JS 库或 npm 包等的人用的,项目工程一般仍然使用 polyfill 补齐 API
可以想象,如果开发 JS 库的人使用 polyfill 补齐 API,我们前端工程也使用 polyfill 补齐 API,但 JS 库的 polyfill 版本或内容与我们前端工程的不一致,那么我们引入该 JS 库后很可能会导致我们的前端工程出问题。所以,开发 JS 库或 npm 包等的人会用到 API 转换功能。
总结
如果你是一个项目工程开发者并且有兼容低版本浏览器的需求,那么建议使用 @babel/polyfill
补齐 API
如果你是一个组件/库的开发者,则一定要使用 @babel/runtime
配合 @babel/plugin-transform-runtime
的转换 API