一、Babel 是什么

Babel 是一个 JavaScript 编译器

  • 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
  • babel 如果本身没有任何插件,则基本就是源代码不发生变化,要想进行相应的转化,就必须有相应的插件带动它。
  • babel 可以帮助我们做什么:
    • 语法转换
    • 源代码转换
    • Polyfill 实现目标缓解缺少的功能等;

二、为什么需要 babel

事实上,在开发中很少直接去接触 babel,但是 babel 对于前端开发来说,目前是不可缺少的一部分:

  • 开发中,我们想要使用 ES6+的语法,想要使用 TypeScript,开发 React 项目,它们都是离不开 Babel
  • 学习 Babel 对于我们理解代码从编写到线上的转变过程直观重要
  • 了解真相,才能获得真知的自由

三、Babel 的命令行使用

babel 本身可以作为一个独立的工具(和 postcss 一样),不用和 webpack 等构建工具配置,也可以单独使用

如果我们希望在命令行尝试使用 babel,需要安装如下库:

  • @babel/core:babel 的核心代码,必须安装;
  • @babel/cli:可以让我们在命令行使用 babel
  • npm install @babel/cli @babel/core

使用 babel 来处理我们的源代码:

  • src:是源文件的目录
  • --out-dir:指定要输出的文件夹 dist
  • npx babel src --out-dir dist

源代码文件:

1
2
3
4
5
6
const message = "Hello Babel";

const fun = (msg) => {
console.log(msg);
};
fun(message);

npx babel src --out-dir dist后:

1
2
3
4
5
6
const message = "Hello Babel";

const fun = (msg) => {
console.log(msg);
};
fun(message);

可见,几乎没有变化,因为 babel/core 只是核心库,需要借助插件来进行编译转换。

四、插件的使用

比如我们需要转换箭头函数,那么我们就可以使用箭头函数转换相关的插件:

1
2
npm install @babel/plugin-transform-arrow-functions -D
npx babel src --out-dir dist --plugins=@babel/plugin-transform-arrow-functions
1
2
3
4
5
6
7
const message = "Hello Babel";

const fun = function (msg) {
console.log(msg);
};

fun(message);

可见箭头函数已经转换为普通函数

但是会发现 const 并没有转成 var

这是因为 plugin-transform-arrow-functions 并没有提供这样的功能

我们需要使用 plugin-transform-block-scoping 来完成这样的功能;

1
2
3
4
npm install @babel/plugin-transform-block-scoping -D

npx babel src --out-dir dist --plugins=@babel/plugin-transform-block-scoping
,@babel/plugin-transform-arrow-functions
1
2
3
4
5
6
7
var message = "Hello Babel";

var fun = (msg) => {
console.log(msg);
};

fun(message);

可以,都已经转为 ES5 的写法。

五、Babel 的预设 preset

如果要转换的内容过多,一个个设置是比较麻烦的,我们可以使用预设(preset)

后面我们再具体来讲预设代表的含义

  • 安装@babel/preset-env 预设
    • npm install @babel/preset-env -D
  • 执行如下命令:
    • npx babel src --out-dir dist --presets=@babel/preset-env
1
2
3
4
5
6
7
8
9
"use strict";

var message = "Hello Babel";

var fun = function fun(msg) {
console.log(msg);
};

fun(message);

六、Babel 的底层原理

babel 是如何做到将我们的一段代码(ES6、TypeScript、React)转成另外一段代码(ES5)的呢?

从一种源代码(原生语言)转换成另一种源代码(目标语言),这是什么的工作呢?

  • 就是编译器,事实上我们可以将 babel 看成就是一个编译器
  • Babel 编译器的作用就是将我们的源代码,转换成浏览器可以直接识别的另外一段源代码
  • Babel 也拥有编译器的工作流程:
    • 解析阶段(Parsing)
    • 转换阶段(Transformation)
    • 生成阶段(Code Generation)

编译器执行原理

七、babel-loader

实际开发中,我们通常会在构建工具中通过配置 babel 来对其进行使用的,比如在 webpack 中。

那么我们就需要去安装相关的依赖:

如果之前已经安装了@babel/core,那么这里不需要再次安装

npm install babel-loader @babel/core

我们可以设置一个规则,在加载 js 文件时,使用我们的 babel:

1
2
3
4
5
6
7
8
9
10
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
},
},
],
},

npm run build 后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/******/ (function () {
// webpackBootstrap
var __webpack_exports__ = {};
/*!*********************!*\
!*** ./src/main.js ***!
\*********************/
const message = "Hello Babel";

const fun = (msg) => {
console.log(msg);
};

fun(message);
/******/
})();

ES6 语法并没有转换,因为我们没用使用插件

指定使用的插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-transform-block-scoping',
'@babel/plugin-transform-arrow-functions',
],
},
},
},
],
},

npm run build 后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/******/ (function () {
// webpackBootstrap
var __webpack_exports__ = {};
/*!*********************!*\
!*** ./src/main.js ***!
\*********************/
var message = "Hello Babel";

var fun = function (msg) {
console.log(msg);
};

fun(message);
/******/
})();

babel-preset

如果我们一个个去安装使用插件,那么需要手动来管理大量的 babel 插件,我们可以直接给 webpack 提供一个 preset

webpack 会根据我们的预设来加载对应的插件列表,并且将其传递给 babel。

比如常见的预设有三个:

  • env
  • react
  • TypeScript

安装 preset-env:

npm install @babel/preset-env

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/******/ (function () {
// webpackBootstrap
var __webpack_exports__ = {};
/*!*********************!*\
!*** ./src/main.js ***!
\*********************/
var message = "Hello Babel";

var fun = function fun(msg) {
console.log(msg);
};

fun(message);
/******/
})();

设置目标浏览器 browserslist

我们最终打包的 JavaScript 代码,是需要跑在目标浏览器上的,那么如何告知 babel 我们的目标浏览器呢?

  • browserslist 工具
  • ptarget 属性

说明一点:babel 是转换源代码且运行在浏览器上,那么就要知道浏览器支持的语法来进行源代码的转换

之前项目中已经使用了 browserslist 工具,可以对比一下不同的配置,打包的区别:

chrome 支持 ES6 语法,所以 babel 就不会进行转换了。

设置目标浏览器 targets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
// plugins: [
// '@babel/plugin-transform-block-scoping',
// '@babel/plugin-transform-arrow-functions',
// ],
presets: ['@babel/preset-env',{targets:"last 2 version"}],
},
},
},
],
},

那么,如果两个同时配置了,哪一个会生效呢?

  • 配置的 targets 属性会覆盖 browserslist
  • 但是在开发中,更推荐通过 browserslist 来配置,因为类似于 postcss 工具,也会使用 browserslist,进行统一浏览器的适配

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 的下一个修订版中

Babel 的 Stage-X 设置

在 babel7 之前(比如 babel6 中),我们会经常看到这种设置方式:

1
2
3
module.exports = {
presets: ["stage-0"],
};

它表达的含义是使用对应的 babel-preset-stage-x 预设

但是从 babel7 开始,已经不建议使用了,建议使用 preset-env 来设置;

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:

1
2
3
module.exports = {
presets: ["@babel/preset-env"],
};

八、polyfill

Polyfill 是什么

  • 翻译:一种用于衣物、床具等的聚酯填充材料, 使这些物品更加温暖舒适
  • 理解:更像是应该填充物(垫片),一个补丁,可以帮助我们更好的使用 JavaScript

为什么时候会用到 polyfill

  • 比如我们使用了一些语法特性(例如:Promise, Generator, Symbol 等以及实例方法例如 Array.prototype.includes 等)
  • 但是某些浏览器压根不认识这些特性,必然会报错
  • 我们可以使用 polyfill 来填充或者说打一个补丁,那么就会包含该特性了

使用 polyfill

babel7.4.0 之前,可以使用 @babel/polyfill 的包,但是该包现在已经不推荐使用了

babel7.4.0 之后,可以通过单独引入 core-js 和 regenerator-runtime 来完成 polyfill 的使用

npm install core-js regenerator-runtime --save

全局引入 polyfill 和第三方包自己集成的可能会有冲突,所以 babel 打包时需要忽略第三方包

1
2
3
4
5
6
7
8
{
test: /\.m?jsx?$/,
//忽略第三方包使用babel打包编译
exclude:/node_modules/,
use: {
loader: 'babel-loader',
},
},

配置 babel.config.js

需要在 babel.config.js 文件中进行配置,给 preset-env 配置一些属性:

  • useBuiltIns:设置以什么样的方式来使用 polyfill
  • corejs:设置 corejs 的版本,目前使用较多的是 3.x 的版本
  • 另外 corejs 可以设置是否对提议阶段的特性进行支持,设置 proposals 属性为 true 即可

useBuiltIns 属性设置

第一个值:false

  • 打包后的文件不使用 polyfill 来进行适配
  • 并且这个时候是不需要设置 corejs 属性的

第二个值:usage

  • 会根据源代码中出现的语言特性,自动检测所需要的 polyfill;
  • 这样可以确保最终包里的 polyfill 数量的最小化,打包的包相对会小一些
  • 可以设置 corejs 属性来确定使用的 corejs 的版本

第三个值:entry

  • 如果我们依赖的某一个库本身使用了某些 polyfill 的特性,但是因为我们使用的是 usage,所以之后用户浏览器可能会报错
  • 所以,如果你担心出现这种情况,可以使用 entry,并且需要在入口文件中添加
  • import 'core-js/stable'
  • import 'regenerator-runtime/runtime'
  • 这样做会根据 browserslist 目标导入所有的 polyfill,但是对应的包也会变大

babel.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
presets: [
[
"@babel/preset-env",
{
//false
//usage
//entry
useBuiltIns: "entry",
corejs: 3,
},
],
],
};

入口文件 main.js

1
2
3
4
5
6
7
8
9
10
import "core-js/stable";
import "regenerator-runtime/runtime";
const message = "Hello Babel";

const fun = (msg) => {
console.log(msg);
};
fun(message);

let promise = new Promise((resolve, reject) => {});

打包后的文件有 15000 多行,就不展示了。

Plugin-transform-runtime

在前面我们使用的 polyfill,默认情况是添加的所有特性都是全局的

如果我们正在编写一个工具库,这个工具库需要使用 polyfill

别人在使用我们工具时,工具库通过 polyfill 添加的特性,可能会污染它们的代码

所以,当编写工具时,babel 更推荐我们使用一个插件:

@babel/plugin-transform-runtime

来完成 polyfill 的功能

npm install @babel/plugin-transform-runtime -D

使用 plugins 来配置 babel.config.js:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
presets: [["@babel/preset-env"]],
plugins: [
[
"@babel/plugin-transform-runtime",
{
corejs: 3,
},
],
],
};

注意:因为使用了 corejs3,所以我们需要安装对应的库:

npm i @babel/runtime-corejs3

九、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 来配置:

npm install @babel/preset-react -D

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: 3,
},
],
["@babel/preset-react"],
],
};

十、TypeScript 的编译

我们会使用 TypeScript 来开发,那么 TypeScript 代码是需要转换成 JavaScript 代码。

可以通过 TypeScript 的 compiler 来转换成 JavaScript:

npm install typescript -g

之后我们可以运行 npx tsc 来编译自己的 ts 代码:

tsc index.ts

使用 ts-loader

如果希望在 webpack 中使用 TypeScript,那么我们可以使用 ts-loader 来处理 ts 文件:

npm install ts-loader -D

配置 ts-loader:

1
2
3
4
5
{
test: /\.ts$/,
exclude: /node_modules/,
use: ['ts-loader'],
},

之后,我们通过 npm run build 打包,会报错,因为需要一个 tsconfig.json 文件

tsc –init 生成一个 tsconfig.js 文件

但是 ts-loader 会依赖本地的 typescript,所以本地也需要安装下

npm install typescript -D

npm run build

bundle.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/******/ (function () {
// webpackBootstrap
/******/ "use strict";
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it uses a non-standard name for the exports (exports).
!(function () {
var exports = __webpack_exports__;
/*!**********************!*\
!*** ./src/index.ts ***!
\**********************/

Object.defineProperty(exports, "__esModule", { value: true });
var message = "Hello TypeScript";
var foo = function (msg) {
console.log(msg);
};
foo(message);
})();
/******/
})();

使用 babel-loader

除了可以使用 TypeScript Compiler 来编译 TypeScript 之外(ts-loader 也是基于 tsc 的),我们也可以使用 Babel

  • Babel 是有对 TypeScript 进行支持
  • 我们可以使用插件:
    • @babel/tranform-typescript
  • 但是更推荐直接使用 preset:
    • @babel/preset-typescript
  • 来安装@babel/preset-typescript:
    • npm install @babel/preset-typescript -D
1
2
3
4
5
{
test: /\.ts$/,
exclude: /node_modules/,
use: ['babel-loader'],
},
1
2
3
4
5
6
7
8
9
10
11
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: 3,
},
],
['@babel/preset-react'],
['@babel/preset-typescript']
],

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 在编译的过程中,不会对类型错误进行检测
    • 那么在开发中,我们如何可以同时保证两个情况都没有问题呢?

事实上 TypeScript 官方文档有对其进行说明:

  • 也就是说我们使用 Babel 来完成代码的转换,使用 tsc 来进行类型的检查。
  • 但是,如何可以使用 tsc 来进行类型的检查呢?
  • 在这里,在 scripts 中添加了两个脚本,用于类型检查
1
2
3
4
5
6
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"type-check": "tsc --noEmit",
"type-check-watch": "npm run type-check -- --watch"
},
  • 执行 npm run type-check 可以对 ts 代码的类型进行检测

检测完后就会停止

  • 执行 npm run type-check-watch 可以实时的检测类型错误

会持续监控代码变化,不会停止,需要手动停止。

十一、ESLint

ESLint 是一个静态代码分析工具(Static program analysis,在没有任何程序执行的情况下,对代码进行分析)

ESLint 可以帮助我们在项目中建立统一的团队代码规范,保持正确、统一的代码风格,提高代码的可读性、可维护性

并且 ESLint 的规则是可配置的,我们可以自定义属于自己的规则

早期还有一些其他的工具,比如 JSLint、JSHint、JSCS 等,目前使用最多的是 ESLint。

使用 ESLint

使用脚手架的话一般都会配置 ESLint

如果从零开始进行搭建的话需要

  • 手动安装
    • npm install eslint -D
  • 新建配置文件,没有配置文件无法进行代码规范检测
    • npx eslint --init
  • 选择想要使用的 ESLint 配配置:

  • 执行检测命令
    • npx eslint ./src/main.js
1
2
3
4
5
var message = "Hello TypeScript";
var foo = function (msg) {
console.log(msg);
};
foo("qwe");

会报警告

ESLint 文件解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module.exports = {
env: {
browser: true,
commonjs: true,
es2021: true,
//我使用了node的全局变量 不配置这个会报错
node: true,
},
extends: [
"eslint:recommended",
"plugin:vue/essential",
"plugin:@typescript-eslint/recommended",
],
parserOptions: {
ecmaVersion: 13,
parser: "@typescript-eslint/parser",
},
plugins: ["vue", "@typescript-eslint"],
rules: {
//我使用了require导入包 不配置这个会报错
"@typescript-eslint/no-var-requires": 0,
},
};

env:

  • 运行的环境,比如是浏览器,并且我们使用 es2021(对应的 ecmaVersion 是 12)的语法

extends:

  • 可以扩展当前的配置,让其继承自其他的配置信息,可以跟字符串或者数组(多个)

parserOptions:

  • 这里可以指定 ESMAScript 的版本、sourceType 的类型

parser:

  • 默认情况下是 espree(也是一个 JS Parser,用于 ESLint),但是因为我们需要编译 TypeScript,所以需要指定对应的解释器

plugins:

  • 指定我们用到的插件

rules:

  • 自定义的一些规则

eslint-loader

可以安装 loader,在编译打包 js 文件时自动校验规范

npm i eslint-loader

1
2
3
4
5
{
test: /\.m?jsx?$/,
exclude: /node_modules/,
use: ['babel-loader', 'eslint-loader'],
},

这样在编译打包 js 文件时先回进行 eslint 检测,不符合规范就会报错。

注意:2021.12.15 我安装的 eslint 是 v8 版本的,这个版本貌似更新了一些东西,可以回退到 v7 版本,兼容性貌似更好一些

十二、加载 Vue 文件

编写 vue 代码

npm i vue

src -> index.js:

1
2
3
4
import Vue from "vue";
import App from "./App.vue";

new Vue({ render: (h) => h(App) }).$mount("app");

src -> App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<h2 class="title">{{ msg }}</h2>
</div>
</template>

<script>
export default {
data: () => {
msg: 'Hello Vue In Webpack';
},
};
</script>

<style scope lang="less">
.title {
color: aqua;
}
</style>

安装依赖

npm install vue-loader -D

npm install vue-loader -D 这个用于解析 template 模板

配置 webpack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 加载vue必须使用这个插件
const VueLoaderPlugin = require('vue-loader/lib/plugin');
{
test: /\.vue$/,
use: ['vue-loader'],
},
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'less-loader',
],
},
plugins: [
new VueLoaderPlugin(),
],

注意:

  • 必须使用 VueLoaderPlugin 这个插件
  • App.vue 文件中使用了 less,所以需要对 less 进行配置