1 为什么需要babel?
事实上,在开发中我们很少直接去接触babel,但是babel对于前端开发来说,目前是不可缺少的一部分。原因如下:
- 开发中,我们想要使用ES6+的语法,想要使用TypeScript,开发React项目,它们都是离不开Babel的
- 学习Babel对于我们理解代码从编写到线上的转变过程直观重要
- 了解真相,你才能获得真知的自由
那么,Babel到底是什么呢?
Babel
是一个工具链,主要用于旧浏览器或者缓解中将ECMAScript 2015+代码转换为向后兼容版本的JavaScript。包括:语法转换、源代码转换、Polyfill实现目标缓解缺少的功能等。
2 Babel命令行使用
babel本身可以作为一个独立的工具(和postcss一样),不和webpack等构建工具配置来单独使用。如果我们希望在命令行尝试使用babel,需要安装如下库:
yarn add @babel/cli @babel/core -D
- @babel/core:babel的核心代码,必须安装
- @babel/cli:可以让我们在命令行使用babe
使用babel来处理我们的源代码:
npx babel src --out-dir dist
src:源文件的目录。
--out-dir:指定要输出的文件夹dist。
2.1 插件的使用
比如我们需要转换箭头函数,那么我们就可以使用箭头函数转换相关的插件
yarn add @babel/plugin-transform-arrow-functions -D
npx babel src --out-dir dist --plugins=@babel/plugin-transform-arrow-functions
const转成 var:
yarn add @babel/plugin-transform-block-scoping -D
npx babel src --out-dir dist --plugins=@babel/plugin-transform-block-scoping
,@babel/plugin-transform-arrow-functions
2.3 Babel的预设preset
但是如果要转换的内容过多,一个个设置是比较麻烦的,我们可以使用预设(preset),后面我们再具体来讲预设代表的含义。
yarn add @babel/preset-env -D
npx babel src --out-dir dist --presets=@babel/preset-env
3 Babel的底层原理
babel是如何做到将我们的一段代码(ES6、TypeScript、React)转成另外一段代码(ES5)的呢?
从一种源代码(原生语言)转换成另一种源代码(目标语言),这是什么的工作呢?
就是编译器,事实上我们可以将babel看成就是一个编译器。Babel编译器的作用就是将我们的源代码,转换成浏览器可以直接识别的另外一段源代码。
3.1 babel编译器执行原理
Babel的执行阶段:
当然,这只是一个简化版的编译器工具流程,在每个阶段又会有自己具体的工作:
以下面代码为例:
const name = "coderwhy";
const foo = (name) => console.log(name);
foo(name);
首先,babel通过此法分析生成tokens数组
,生成结构如下:
[
{
"type": "Keyword",
"value": "const"
},
{
"type": "Identifier",
"value": "foo"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "Punctuator",
"value": "("
},
{
"type": "Identifier",
"value": "name"
},
{
"type": "Punctuator",
"value": ")"
},
{
"type": "Punctuator",
"value": "=>"
},
{
"type": "Identifier",
"value": "console"
},
{
"type": "Punctuator",
"value": "."
},
{
"type": "Identifier",
"value": "log"
},
{
"type": "Punctuator",
"value": "("
},
{
"type": "Identifier",
"value": "name"
},
{
"type": "Punctuator",
"value": ")"
},
{
"type": "Punctuator",
"value": ";"
},
{
"type": "Identifier",
"value": "foo"
},
{
"type": "Punctuator",
"value": "("
},
{
"type": "String",
"value": "\"coderwhy\""
},
{
"type": "Punctuator",
"value": ")"
},
{
"type": "Punctuator",
"value": ";"
}
]
其次,babel通过语法分析(parsing)生成AST(抽象语法树):
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "foo"
},
"init": {
"type": "ArrowFunctionExpression",
"id": null,
"params": [
{
"type": "Identifier",
"name": "name"
}
],
"body": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"computed": false,
"object": {
"type": "Identifier",
"name": "console"
},
"property": {
"type": "Identifier",
"name": "log"
}
},
"arguments": [
{
"type": "Identifier",
"name": "name"
}
]
},
"generator": false,
"expression": true,
"async": false
}
}
],
"kind": "const"
},
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "foo"
},
"arguments": [
{
"type": "Literal",
"value": "coderwhy",
"raw": "\"coderwhy\""
}
]
}
}
],
"sourceType": "script"
}
接着,babel遍历AST后进行访问操作,并通过相应的插件转成新的AST:
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "foo"
},
"init": {
"type": "FunctionExpression",
"id": {
"type": "Identifier",
"name": "foo"
},
"params": [
{
"type": "Identifier",
"name": "name"
}
],
"body": {
"type": "BlockStatement",
"body": [
{
"type": "ReturnStatement",
"argument": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"computed": false,
"object": {
"type": "Identifier",
"name": "console"
},
"property": {
"type": "Identifier",
"name": "log"
}
},
"arguments": [
{
"type": "Identifier",
"name": "name"
}
]
}
}
]
},
"generator": false,
"expression": false,
"async": false
}
}
],
"kind": "var"
},
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "foo"
},
"arguments": [
{
"type": "Literal",
"value": "coderwhy",
"raw": "\"coderwhy\""
}
]
}
}
],
"sourceType": "script"
}
最后生成目标代码啦:
var name = "coderwhy";
var foo = function foo(name) {
return console.log(name);
};
foo(name);
4. webpack使用babel
4.1 babel-loader
在实际开发中,我们通常会在构建工具中通过配置babel来对其进行使用的,比如在webpack中。
那么我们就需要去安装相关的依赖:
- 如果之前已经安装了@babel/core,那么这里不需要再次安装;
yarn add babel-loader @babel/core -D
我们可以设置一个规则,在加载js文件时,使用我们的babel:
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader'
}
}
]
}
}
4.2 指定使用的插件
我们必须指定使用的插件才会生效:
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-transform-arrow-functions',
'@babel/plugin-transform-block-scoping'
]
}
}
}
]
}
}
4.3 babel-preset
如果我们一个个去安装使用插件,那么需要手动来管理大量的babel插件,我们可以直接给webpack提供一个preset,webpack会根据我们的预设来加载对应的插件列表,并且将其传递给babel。
安装preset-env:
yarn add @babel/preset-env -D
配置:
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env'
]
}
}
}
]
}
}
4.4 设置目标浏览器 browserslist
我们最终打包的JavaScript代码,是需要跑在目标浏览器上的,那么如何告知babel我们的目标浏览器呢?
- browserslist工具,之前在
postcss
已经配置过了。 - target属性
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: 'chrome 87'
}]
]
}
}
}
]
}
}
target的优先级高于browserslist,也就是说,当设置target后,browserlist的配置会失效。但我们开发中推荐在browserlist里配置,这样能和postcss同步。
4.5 Stage-X的preset
要了解Stage-X,我们需要先了解一下TC39的组织:
- TC39是指技术委员会(Technical Committee)第 39 号;
- 它是 ECMA 的一部分,ECMA 是 “ECMAScript” 规范下的 JavaScript 语言标准化的机构;
- ECMAScript 规范定义了 JavaScript 如何一步一步的进化、发展;
TC39 遵循的原则是:分阶段加入不同的语言特性,新流程涉及四个不同的 Stage:
- Stage 0:strawman(稻草人),任何尚未提交作为正式提案的讨论、想法变更或者补充都被认为是第 0 阶段的"稻草人";
- Stage 1:proposal(提议),提案已经被正式化,并期望解决此问题,还需要观察与其他提案的相互影响;
- Stage 2:draft(草稿),Stage 2 的提案应提供规范初稿、草稿。此时,语言的实现者开始观察 runtime 的具体实现是否合理;
- Stage 3:candidate(候补),Stage 3 提案是建议的候选提案。在这个高级阶段,规范的编辑人员和评审人员必须在最终规范上签字。Stage 3 的提案不会有太大的改变,在对外发布之前只是修正一些问题;
- Stage 4:finished(完成),进入 Stage 4 的提案将包含在 ECMAScript 的下一个修订版中;
4.1.1 Babel的Stage-X设置
在babel7之前(比如babel6中),我们会经常看到这种设置方式:
- 它表达的含义是使用对应的 babel-preset-stage-x 预设
- 但是从babel7开始,已经不建议使用了,建议使用preset-env来设置;
5 Babel的配置文件
像之前一样,我们可以将babel的配置信息放到一个独立的文件中,babel给我们提供了两种配置文件的编写:
- babel.config.json(或者.js,.cjs,.mjs)文件;
- .babelrc.json(或者.babelrc,.js,.cjs,.mjs)文件;
它们两个有什么区别呢?目前很多的项目都采用了多包管理的方式(babel本身、element-plus、umi等);
- .babelrc.json:早期使用较多的配置方式,但是对于配置Monorepos项目是比较麻烦的;
- babel.config.json(babel7):可以直接作用于Monorepos项目的子包,更加推荐;
在项目根目录新建babel.config.js文件
:
module.exports = {
presets: [
['@babel/preset-env']
]
}
然后移除webpack.config.js
的babel配置:
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
}
}
]
}
}
6 认识polyfill
Polyfill是什么呢?
- 翻译:一种用于衣物、床具等的聚酯填充材料, 使这些物品更加温暖舒适;
- 理解:更像是应该填充物(垫片),一个补丁,可以帮助我们更好的使用JavaScript;
为什么时候会用到polyfill呢?
- 比如我们使用了一些语法特性(例如:Promise, Generator, Symbol等以及实例方法例如
Array.prototype.includes等) - 但是某些浏览器压根不认识这些特性,必然会报错;
- 我们可以使用polyfill来填充或者说打一个补丁,那么就会包含该特性了;
6.1 如何使用polyfill?
babel7.4.0之前,可以使用 @babel/polyfill的包,但是该包现在已经不推荐使用了。
babel7.4.0之后,可以通过单独引入core-js和regenerator-runtime来完成polyfill的使用:
yarn add core-js regenerator-runtime --save
在webpack.configf.js
中排除node_modules的文件。因为很多第三方包内部已经实现了polyfill了,会引起冲突。
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
}
}
]
}
}
配置babel.config.js
我们需要在babel.config.js文件中进行配置,给preset-env配置一些属性:
- useBuiltIns:设置以什么样的方式来使用polyfill;
- corejs:设置corejs的版本,目前使用较多的是3.x的版本,比如我使用的是3.8.x的版本;
- 另外corejs可以设置是否对提议阶段的特性进行支持;设置 proposals属性为true即可;
useBuiltIns属性设置
useBuiltIns属性有三个常见的值
- 第一个值:false
- 打包后的文件不使用polyfill来进行适配;
- 并且这个时候是不需要设置corejs属性的;
- 第二个值:usage
- 会根据源代码中出现的语言特性,自动检测所需要的polyfill;
- 这样可以确保最终包里的polyfill数量的最小化,打包的包相对会小一些;
- 可以设置corejs属性来确定使用的corejs的版本;
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3 // 默认值2版本,我们安装的是3
}]
]
}
- 第三个值:entry
- 如果我们依赖的某一个库本身使用了某些polyfill的特性,但是因为我们使用的是usage,所以之后用户浏览器可能会报错;
- 所以,如果你担心出现这种情况,可以使用 entry;
- 并且需要在入口文件中添加 `import 'core-js/stable'; import 'regenerator-runtime/runtime';
- 这样做会根据 browserslist 目标导入所有的polyfill,但是对应的包也会变大;
useBuiltIns值解读:
babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'entry',
corejs: 3
}]
]
}
main.js
import 'core-js/stable';
import 'regenerator-runtime/runtime'
new Promise (resolve => {
resolve(2)
})
6.2 认识Plugin-transform-runtime(了解)
在前面我们使用的polyfill,默认情况是添加的所有特性都是全局的
- 如果我们正在编写一个工具库,这个工具库需要使用polyfill;
- 别人在使用我们工具时,工具库通过polyfill添加的特性,可能会污染它们的代码;
- 所以,当编写工具时,babel更推荐我们使用一个插件: @babel/plugin-transform-runtime来完成polyfill的功能
安装
yarn add install @babel/plugin-transform-runtime -D
使用plugins来配置babel.config.js:
注意:因为我们使用了corejs3,所以我们需要安装对应的库:
module.exports = {
presets: [
['@babel/preset-env', {
// useBuiltIns: 'entry',
// corejs: 3
}]
],
plugins: [
['@babel/plugin-transform-runtime', {
corejs: 3
}]
]
}
7 React的jsx支持
在我们编写react代码时,react使用的语法是jsx,jsx是可以直接使用babel来转换的。
对react jsx代码进行处理需要如下的插件:
- @babel/plugin-syntax-jsx
- @babel/plugin-transform-react-jsx
- @babel/plugin-transform-react-display-name
但是开发中,我们并不需要一个个去安装这些插件,我们依然可以使用preset来配置:
yarn add @babel/preset-react -D
babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'entry',
corejs: 3
}],
['@babel/preset-react']
],
}
8 TypeScript
在项目开发中,我们会使用TypeScript来开发,那么TypeScript代码是需要转换成JavaScript代码。
8.1 TypeScript的命令行使用
可以通过TypeScript的compiler来转换成JavaScript:
npm install typescript -g
另外TypeScript的编译配置信息我们通常会编写一个tsconfig.json文件:
tsc --init
之后我们可以运行 npx tsc来编译自己的ts代码
tsc ./index.ts
8.2 使用ts-loader
如果我们希望在webpack中使用TypeScript,那么我们可以使用ts-loader来处理ts文件:
yarn add ts-loader -D
配置ts-loader:
webpack.config.js
module.exports = {
module: {
rules: [
test: /\.ts$/,
exclude: /node_modules/,
use: 'ts-loader'
}
]
}
}
8.3 使用babel-loader
除了可以使用TypeScript Compiler来编译TypeScript之外,我们也可以使用Babel:
- Babel是有对TypeScript进行支持;
- 我们可以使用插件: @babel/tranform-typescript;
- 但是更推荐直接使用preset:@babel/preset-typescript;
我们来安装@babel/preset-typescript:
yarn add @babel/preset-typescript -D
webpack.config.js
module.exports = {
entry: './src/main.ts', // ts入口文件
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
}
}
babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3
}],
['@babel/preset-typescript']
],
}
8.4 ts-loader和babel-loader选择
那么我们在开发中应该选择ts-loader还是babel-loader呢?
使用ts-loader(TypeScript Compiler)
- 来直接编译TypeScript,那么只能将ts转换成js;
- 如果我们还希望在这个过程中添加对应的polyfill,那么ts-loader是无能为力的;
- 我们需要借助于babel来完成polyfill的填充功能;
使用babel-loader(Babel)
- 来直接编译TypeScript,也可以将ts转换成js,并且可以实现polyfill的功能;
- 但是babel-loader在编译的过程中,不会对类型错误进行检测;
那么在开发中,我们如何可以同时保证两个情况都没有问题呢?
8.5 编译TypeScript最佳实践
事实上TypeScript官方文档有对其进行说明:
也就是说我们使用Babel来完成代码的转换,使用tsc来进行类型的检查。
但是,如何可以使用tsc来进行类型的检查呢?
- 在这里,我在scripts中添加了两个脚本,用于类型检查;
- 我们执行 npm run type-check可以对ts代码的类型进行检测;
- 我们执行 npm run type-check-watch可以实时的检测类型错误;
package.json
{
"scripts": {
"build": "webpack --config wk.config.js",
"type-check": "tsc --noEmit",
"type-check-watch": "tsc --noEmit --watch"
}
}
*注意:必须有tsconfig.json
文件
9 es-lint
ESLint是一个静态代码分析工具(Static program analysis,在没有任何程序执行的情况下,对代码进行分析),ESLint可以帮助我们在项目中建立统一的团队代码规范,保持正确、统一的代码风格,提高代码的可读性、可维护性;并且ESLint的规则是可配置的,我们可以自定义属于自己的规则;早期还有一些其他的工具,比如JSLint、JSHint、JSCS等,目前使用最多的是ESLint。
9.1 使用ESLint
安装
yarn add eslint -D
创建ESLint的配置文件:
npx eslint --init
执行检测命令:
npx eslint ./src/main.js
9.2 ESLint的配置文件解析
默认创建的环境如下:
- env:运行的环境,比如是浏览器,并且我们会使用es2021(对应的ecmaVersion是12)的语法;
- extends:可以扩展当前的配置,让其继承自其他的配置信息,可以跟字符串或者数组(多个);
- parserOptions:这里可以指定ESMAScript的版本、sourceType的类型
- parser:默认情况下是espree(也是一个JS Parser,用于ESLint),但是因为我们需要编译TypeScript,所以需要指定对应的解释器;
- plugins:指定我们用到的插件;
- rules:自定义的一些规则;
module.exports = {
env: {
browser: true,
commonjs: true,
es2021: true,
},
extends: ['plugin:vue/essential', 'airbnb-base'],
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
},
plugins: ['vue', '@typescript-eslint'],
rules: {
// 0 => off
// 1 => warn
// 2 => error
'no-unused-vars': 0,
quotes: ['warn', 'single'],
'no-console': 0,
'import/no-extraneous-dependencies': 0,
},
};
9.3 eslint-loader
安装
yarn add eslint-loader -D
配置
webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: ['babel-loader', 'eslint-loader'],
}
],
},
};
配置完成后,执行yarn build
的时候如果有eslint错误就会抛出,阻止打包。
9.4 vscode插件eslint
但是如果每次校验时,都需要执行一次npm run eslint就有点麻烦了,所以我们可以使用一个VSCode的插件:ESLint
9.5 VSCode的Prettier插件
ESLint会帮助我们提示错误(或者警告),但是不会帮助我们自动修复,在开发中我们希望文件在保存时,可以自动修复这些问题;我们可以选择使用另外一个工具:prettier
10 加载Vue文件
安装vue
yarn add vue
安装相关依赖
yarn add vue-loader vue-template-compiler -D
配置
webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
importLoaders: 2
}
},
"postcss-loader",
"less-loader"
]
},
{
test: /\.vue$/,
use: "vue-loader"
}
],
},
plugins: [
new VueLoaderPlugin()
],
};