babel 笔记录

241 阅读16分钟

学习初衷

在学习之前,我对 babel 的第一印象: 熟悉而又陌生

为什么熟悉?

因为在开发过程中,我了解:

  1. TypeScript 转化为 JavaScript,需要 babel 处理
  2. React 的 jsx 转化为 JavaScript,需要 babel 处理
  3. 为了兼容浏览器,es6 转化为 es5,需要 babel 处理
  4. ...

那么为什么又陌生呢?

因为它就像一个黑盒,虽然知道它的存在,但是呢,具体怎么使用,内部的执行流程大致是怎么样的,这些都是未知的。

为了打开这个黑盒,跟随 coderwhy 老师深入学习了一下,加深自己对 babel 的理解,记录下此篇。

学习路线

  1. 为什么需要 babel ?
  2. babel 介绍及使用
  3. babel的底层实现流程
  4. 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 before npx babel, otherwise npx will install out-of-dated babel 6.x.

注意:在使用 npx babel 之前,必须先安装 @babel/core 和 @babel/cli,否则 npx 会自动安装过时的 babel 6.x 版本

babel 案例小测

这里就不直接上代码了,直接看截图,效果杠杠的。

babel_01.png

上面这个截图,在 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

babel_02.png

再次对比,发现 let 变成 var箭头函数变成普通函数,编译成功。

但是呢,可以发现对象的解构是没有被转化的,因为也是需要对应的插件。

那么问题就来了,如果 JavaScrip 发展这么快,新的语法如雨后春笋,那么不可能每个插件都需要手动去安装吧。

那么需要怎么做呢?

预设(preset)引出

为了编译 JavaScrip 新的语法,就需要对应安装多个插件。如果不嫌麻烦,这不失为一个办法。但绝不是最优解。

最优解:预设(@babel/preset-env)

何为预设?就是里面已经收集各种插件,只需要安装一个预设,就能享受所有插件福利。

安装:

pnpm install @babel/preset-env -D

安装之后,重新编写编译命令:

npx babel src --out-dir dist --presets=@babel/preset-env

babel_03.png

看看效果,是不是很完美(新的语法特性都被编译成 ES5 了)。

这就是众所周知的 babel,一个可以编译文件的工具。

babel 的底层实现流程

在上面已经了解到 babel 是一个 JavaScrip 编译器。

编译器的作用就是把一段源代码转化成另外一段源代码;针对与 babel 来说,转化后的代码供浏览器认识。

babel 也是拥有编译器的工作流程:

  • 解析阶段(parse)
  • 转化阶段(Transformation)
  • 生成阶段(Code Generation)

下面大致画了一张简单的流程图:

babel_06.png

第一列:就是解释器的三个阶段(每个阶段都有着具体工作);

第二列:就是每个阶段具体要发生的事件(工作)。

其实整体的流程还是比较好理解的。


下面 github 地址是一个大佬使用 JavaScrip 写的一个小型编译器,麻雀虽小,五脏俱全。

the-super-tiny-compiler

下面是截取源码的 compiler 函数(编译器)

babel_04.png

还是可以很清晰的看出,编译器的三个阶段:parsertransformercodeGenderator

有兴趣的话,可以进去看看,每个阶段的代码具体是怎么实现的。

babel 实战应用

针对现阶段的前端开发,都是离不开构架工具的(比如说:webpack / vite 等)。当使用了构建工具,就会生成一个打包文件(比如:build.js),然后部署到线上去,与浏览器进行交互。

但是在打包这个阶段,是没有对代码进行转化的,存在浏览器不认识的问题。

而 babel 对源代码转化生成另外一段源代码,该段源代码是能够完全被浏览器认识的。

所以说,就可以想办法,先让 babel 对源代码进行转化,然后构建工具对转化后的代码进行打包,那么打包出来的代码,就能够完全被浏览器识别了。

babel_05.png

就类似该形式,babel 在中间横插一脚。babel 先对源码进行转化,然后 webpack 打包转化后的代码。那么现在的问题就是:babel 如何插进去?

下面都是以 webpack 为例了

webpack 在构建执行的过程中,就会对代码进行编译,大致是这样的:

  1. 调用 Compilation 类的 build 实例方法,然后对代码进行遍历,生成依赖性,构建 module 模块
  2. 构建 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 不是很理解,可以看看我的另外一篇:

一文加深你对 webpack 的 Source Map 理解

准备工作完成了,那么就来添加对 babel-loader 的配置。

// webpack.config.jsmodule.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"],
            },
          },
        ],
      },
    ],
  },
}

babel_07.png

效果很 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%,稍微把使用率提高点,那么浏览器的版本就新点,为了可以看到实际的效果

babel_08.png

可以发现,ES6 的语法没有被转化,那么实际就说明,有些浏览器是支持 ES6 的语法了,是不需要转换的。

那么这样就完美了吗?当然不是。

前端中的三剑客(HTML、CSS、JavaScrip)除了 HTML 很少考虑兼容性外,CSS 和 JavaScrip 考虑兼容性就太常见了。那么这时候的想法是兼容性肯定是一致的,肯定不是各兼容各的(就比如上面配置 babel 预设,就只兼容了 JavaScrip)。那么这时候就需要一个统一的文件来配置浏览器兼容性问题,然后无论是CSS 处理兼容性,还是 JavaScrip 处理兼容性都去读取这个统一配置文件,那么这时候两边都保持了一致。

那么如何配置这个统一文件呢?就需要借助一个工具 browserslist

babel_09.png

既然它跟 babel 一样,都是工具,那么也是需要进行安装的。

pnpm install browserslist -D

可以简单的运行一个命令:看看浏览器占有率大于5%的有哪些

npx browserslist "> 5%"

babel_10.png

安装了 browserslist,接下来就是该怎么使用了,有两种方式:

  1. 在 package.json 中配置一个属性 browserslist
{
  "browserslist": [
    "> 1%",
    "last 2 version",
    "not dead"
  ]
}
  1. 创建一个 .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-jsregenerator-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 函数)

babel_11.png

打包出了一大堆,里面针对 Promise 函数的实现。

  • entry:当使用的第三方库里面存在新的语法函数,使用 usage 是检测不到的。那么就只有采用一种极端的手法,在入口处全部导入。

babel_12.png

那么这时候,打包的代码量的体积就更加的庞大了。

小知识点:

既然在入口文件处已经导入了所有的 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 预设
  ],
};

这里就直接案例截图:

babel_13.png

案例运行成功。

案例后的体会:

代码是需要多敲的。 在这里自己犯了一个错误,想了很久(但是错误的原因,就是自己无知认为的理所当然)。在平时的 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 的入口文件,执行打包命令。

babel_14.png

效果正常。

但是如果在 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?

typescript 官网的选择

babel_15.png

两种情况:

  1. 如果你想构建输出文件内容与源文件内容大致相同(也就是没有 polyfill 等),则使用 tsc,也就是 ts-loader。
  2. 你需要构建的输出文件可能存在多种结果(就是兼容浏览器,使用 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 笔记录到此结束!!!