个人主页:爱吃炫迈
系列专栏:前端工程化
座右铭:道阻且长,行则将至
开发中,我们想使用ES6+语法,想要使用TypeScript,开发React项目,它们都是离不开Babel的,所以,学习babel对于我们理解代码从编写到线上的转变过程十分重要的。
官方定义:
babel是一个工具链,主要用于就旧浏览器或者环境中将ECMAScript2015+代码转换为向后兼容版本的JavaScript,包括语法转换、源代码转换、polyfill实现目标环境缺少的功能等,以便能够运行在当前和旧版本的浏览器或其他环境中。
但是默认情况下babel并不会进行转换,只会从一个文件夹里输出到另外一个文件夹里,如果想进行代码转换,就需要使用相关的插件,插件是小型的 JavaScript 程序,用于指导 Babel 如何对代码进行转换。
举个栗子:
npm install @babel/plugin-transform-arrow-functions -D
npx babelsrc --out-dir dist --plugins=@babel/plugin-transform-arrow-functions
const fu = () => 1;
// converted to
const fn = function fn() {
return 1;
};
plugin-transform-arrow-functions
并没有提供这样的功能,还需要使用plugin-transform-block-scoping
来完成这样的功能npm insatll @babel/plugin-transform-block-scoping -D
npx babel src --out-dir dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions
const fu = () => 1;
// converted to
var fn = function fn() {
return 1;
};
如果需要转换的内容过多,一个个设置比较麻烦,这个时候可以使用预设(即一组预先设定的插件)。就像插件一样,你也可以根据自己所需要的插件组合创建一个自己的 preset 并将其分享出去。
@babel/preset-env
是一个智能预设,它可以将我们的高版本JavaScript代码进行转译根据内置的规则转译成为低版本的JavaScript代码。
preset-env
内部集成了绝大多数plugin
(State > 3
)的转译插件,它会根据对应的参数进行代码转译。
具体的参数配置可以看官网:https://www.babeljs.cn/docs/babel-preset-env
注意:babel-preset-env
仅仅针对语法阶段的转译,比如转译箭头函数,const/let
语法。针对一些API
或者ES6
内置模块的polyfill
,preset-env
是无法进行转译的。
通常我们在使用React
中的jsx
时,相信大家都明白实质上jsx
最终会被编译称为React.createElement()
方法。
babel-preset-react
这个预设起到的就是将jsx
进行转译的作用。
babel-preset-typescript
对于TypeScript
代码,我们有两种方式去编译TypeScript
代码成为JavaScript
代码。
tsc
命令,结合cli
命令行参数方式或者tsconfig
配置文件进行编译ts
代码。babel
,通过babel-preset-typescript
代码进行编译ts
代码。对于上面的例子而言,我们可以使用一个名称为 env
的 preset:
npm install @babel/preset-env -D
npx babel src --out-dir dist --presets=@babel/preset-env
原则如下:
Plugin 会运行在 Preset 之前。
Plugin 会从前到后顺序执行。
Preset 的顺序则从后向前。
preset 的逆向顺序主要是为了保证向后兼容,因为大多数用户的编写顺序是 ['es2015', 'stage-0']
。这样必须先执行 stage-0
才能确保 babel 不报错。因为低一级的 stage 会包含所有高级 stage 的内容
实际项目中单独使用Babel的命令行工具来实现工作流的情况不是很多,更多的是结合Webpack使用。
在webpack.config.js中添加以下loader配置:
module.exports = {
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: ["@babel/preset-env"]
}
}
]
}
]
}
}
然后安装相应的依赖:babel-loader、@babel/core、@babel/preset-env:
npm install --save-dev babel-loader @babel/core @babel/preset-env
//.babelrc
{
"presets": ["@babel/preset-env"]
}
关于webpack中我们日常使用的babel相关配置主要涉及以下三个相关插件:
babel-loader
babel-core
babel-preset-env
首先我们需要清楚在webpack中loader的本质就是一个函数,接受我们的源代码作为入参同时返回新内容。
babel-loader
的本质就是一个函数,我们匹配到对应的jsx?/tsx?
的文件交给babel-loader
:
/**
*
* @param sourceCode 源代码内容
* @param options babel-loader相关参数
* @returns 处理后的代码
*/
function babelLoader (sourceCode,options) {
// ..
return targetCode
}
关于
options
,babel-loader
支持直接通过loader
的参数形式注入,同时也在loader
函数内部通过读取.babelrc/babel.config.js/babel.config.json
等文件注入配置。
babel-loader
仅仅是识别匹配文件和接受对应参数的函数,那么babel
在编译代码过程中核心的库就是@babel/core
这个库。
babel-core
是babel
最核心的一个编译库,他可以将我们的代码进行词法分析–语法分析–语义分析过程从而生成AST
抽象语法树,从而对于“这棵树”的操作之后再通过编译称为新的代码。
babel-core
通过transform
方法将我们的代码进行编译。
关于babel-core
中的编译方法其实有很多种,比如直接接受字符串形式的transform
方法或者接受js
文件路径的transformFile
方法进行文件整体编译。
让我们来完善对应的babel-loader函数:
const core = require('@babel/core')
/**
*
* @param sourceCode 源代码内容
* @param options babel-loader相关参数
* @returns 处理后的代码
*/
function babelLoader (sourceCode,options) {
// 通过transform方法编译传入的源代码
core.transform(sourceCode)
return targetCode
}
这里我们在babel-loader
中调用了babel-core
这个库进行了代码的编译作用。
上边我们说到babel-loader
本质是一个函数,它在内部通过babel/core
这个核心包进行JavaScript
代码的转译。
但是针对代码的转译我们需要告诉babel
以什么样的规则进行转化,比如我需要告诉babel
:“嘿,babel。将我的这段代码转化称为ECMAScript 5版本的内容!”。
此时babel-preset-env
在这里充当的就是这个作用:告诉babel
我需要以为什么样的规则进行代码转译。
const core = require('@babel/core');
/**
*
* @param sourceCode 源代码内容
* @param options babel-loader相关参数
* @returns 处理后的代码
*/
function babelLoader(sourceCode, options) {
// 通过transform方法编译传入的源代码
core.transform(sourceCode, {
presets: ['babel-preset-env'],
plugins: [...]
});
return targetCode;
}
babel本身可以作为一个独立的工具,不和webpack等构建工具配置来单独使用。
npm install @babel/cli @babel/core
npx babel src --out-dir dist
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hsDuwTIQ-1690128623705)(…/AppData/Roaming/Typora/typora-user-images/image-20230723183636666.png)]
babel是如何将我们的一段代码(ES6、TypeScript、React)转换成另一端代码(ES5)的呢?从一种源代码(原生语言)转换成另一种源代码(目标语言)是怎么工作的呢?这其实是编译器的工作。
事实上我们可以将babel看成是一个编译器,Babel编译器的作用就是将我们的源代码,转换成浏览器可以直接识别的另一段源代码。
Babel也拥有编译器的工作流程:
这个阶段将我们的
js
代码(字符串)进行词法分析生成一系列tokens
,之后再进行语法分析将tokens
组合称为一颗AST
抽象语法树。(比如babel-parser
它的作用就是这一步)
这个阶段
babel
通过对于这棵树的遍历,从而对于旧的AST
进行增删改查,将新的js
语法节点转化称为浏览器兼容的语法节点。(babel/traverse
就是在这一步进行遍历这棵树)
这个阶段
babel
会将新的AST
转化同样进行深度遍历从而生成新的代码。(@babel/generator
)
现在市面上有大量的浏览器:比如chrome、edge、Safari等等,它们的市场占率是多少?我们要不要兼容它们?在开发中,浏览器的兼容性问题,我们应该如何去解决和处理?
注意:这里的兼容性是针对不同的浏览器支持的特性:比如css特性、js语法之间的兼容性。
查询市场占有率,有一个好用的网站:caniuse
Browerslist是一个在**不同的前端工具之间 **,共享目标浏览器和Node.js版本的配置:
- Autoprefixer
- Babel
- postcss-preset-env
- eslint-plugin-compat
- stylelint-no-unsupported-browser-features
- postcss-normalize
- obsolete-webpack-plugin
使用browserslist,可以在css兼容性和js兼容性下共享我们配置的兼容性条件。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FTlEOCB8-1690128623708)(…/AppData/Roaming/Typora/typora-user-images/image-20230723182035355.png)]
// .browserslistrc
> 1%
last 2 versions
not dead
Babel默认只转换新的JavaScript语法,而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign
)都不会转码。
举例来说,ES6在Array
对象上新增了Array.from
方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill
,在Array
上添加实现这个方法,为当前环境提供一个垫片。
其实可以简单总结一下,语法层面的转化preset-env
完全可以胜任。但是一些内置方法模块,仅仅通过preset-env
的语法转化是无法进行识别转化的,所以就需要一系列类似”垫片“的工具进行补充实现这部分内容的低版本代码实现。这就是所谓的polyfill
的作用
通过babelPolyfill通过往全局对象上添加属性以及直接修改内置对象的Prototype
上添加方法实现polyfill
。
比如说我们需要支持String.prototype.include
,在引入babelPolyfill
这个包之后,它会在全局String
的原型对象上添加include
方法从而支持我们的Js Api
。
我们说到这种方式本质上是往全局对象/内置对象上挂载属性,所以这种方式难免会造成全局污染。
可以通过单独引入core-js和regenerator-runtime来完成polyfilll的使用:
npm install core-js regenerator-runtime --save
// babel.config.js
module.exports = {
presets: [
["@babel/preset-env",{
corejs:3,
useBuiltIns: false
}]
]
}
在babel-preset-env
中存在两个参数:
一个是corejs
参数,所谓的core-js
就是我们上文讲到的“垫片”的实现。它会实现一系列内置方法或者Promise
等Api
。
一个useBuiltIns
参数,这个参数决定了如何在preset-env
中使用@babel/polyfill
。useBuiltIns
属性有三个常见的值:
import 'core-js/stable'
;import 'regenerator-runtime/runtime'
上边我们说到配置为
entry
时,perset-env
会基于我们的浏览器兼容列表进行全量引入polyfill
。所谓的全量引入比如说我们代码中仅仅使用了Array.from
这个方法。但是polyfill
并不仅仅会引入Array.from
,同时也会引入Promise
、Array.prototype.include
等其他并未使用到的方法。这就会造成包中引入的体积太大了。
举个栗子:我们以项目中引入Promise为例
当我们配置useBuintInts:entry时,仅仅会在入口文件全量引入一次polyfill,你可以这样理解:
// 当使用entry配置时
...
// 一系列实现polyfill的方法
global.Promise = promise
// 其他文件使用时
const a = new Promise()
当我们配置useBuintInts:usage时,preset-env只能基于各个模块去分析它们 所使用到的polyfill进而引入。
preset-env
会帮助我们智能化的在需要的地方引入,比如:
// a. js 中
import "core-js/modules/es.promise";
...
// b. js 中
import "core-js/modules/es.promise";
...
上边我们讲到@babel/polyfill
是存在污染全局变量的副作用,在实现polyfill
时Babel
还提供了另外一种方式去让我们实现这功能,那就是@babel/runtime
。
简单来讲,@babel/runtime
更像是一种按需加载的解决方案,比如哪里需要使用到Promise
,@babel/runtime
就会在他的文件顶部添加import promise from 'babel-runtime/core-js/promise'
。
同时上边我们讲到对于preset-env
的useBuintIns
配置项,我们的polyfill
是preset-env
帮我们智能引入。
而babel-runtime
则会将引入方式由智能完全交由我们自己,我们需要什么自己引入什么。
它的用法很简单,只要我们去安装npm install --save @babel/runtime
后,在需要使用对应的polyfill
的地方去单独引入就可以了。比如:
// a.js 中需要使用Promise 我们需要手动引入对应的运行时polyfill
import Promise from 'babel-runtime/core-js/promise'
const promsies = new Promise()
总而言之,babel/runtime
你可以理解称为就是一个运行时“哪里需要引哪里”的工具库。
babel-runtime存在的问题:
babel-runtime
在我们手动引入一些polyfill
的时候,它会给我们的代码中注入一些类似_extend(), classCallCheck()
之类的工具函数,这些工具函数的代码会包含在编译后的每个文件中,比如:
class Circle {}
// babel-runtime 编译Class需要借助_classCallCheck这个工具函数
function _classCallCheck(instance, Constructor) { //... }
var Circle = function Circle() { _classCallCheck(this, Circle); };
如果有多个文件都用到了 es6 的 class,则需要在每个文件中都要定义一遍 ,会造成一种浪费
所以针对上述提到的两个问题:
此时需要引入我们的主角:@babel/plugin-transform-runtime
作用:为了解决babel-runtime出现的问题
@babel/plugin-transform-runtime
插件会智能化的分析我们的项目中所使用到需要转译的js
代码,从而实现模块化从babel-runtime
中引入所需的polyfill
实现。@babel/plugin-transform-runtime
插件提供了一个helpers
参数。这个参数开启后就可以将上述提到编译阶段重复的工具函数,比如classCallCheck
、extends
等代码转化为require语句,这些工具函数就不会重复的出现在使用中的模块中了。比如这样:
// @babel/plugin-transform-runtime会将工具函数转化为require语句进行引入
// 而非runtime那样直接将工具模块代码注入到模块中
var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");
var Circle = function Circle() { _classCallCheck(this, Circle); };
n-transform-runtime`插件会智能化的分析我们的项目中所使用到需要转译的`js`代码,从而实现模块化从`babel-runtime`中引入所需的`polyfill`实现。
2. `@babel/plugin-transform-runtime`插件提供了一个`helpers`参数。这个参数开启后就可以将上述提到编译阶段重复的工具函数,比如`classCallCheck`、`extends`等代码转化为require语句,这些工具函数就不会重复的出现在使用中的模块中了。
比如这样:
```js
// @babel/plugin-transform-runtime会将工具函数转化为require语句进行引入
// 而非runtime那样直接将工具模块代码注入到模块中
var _classCallCheck = require("@babel/runtime/helpers/classCallCheck");
var Circle = function Circle() { _classCallCheck(this, Circle); };