Halo,Babel

1,943 阅读15分钟

本文基于 babel 7 做叙述,如果之前一直使用 babel 6 的同学可以先看本文关于 babel 6 升级 babel 7 的相关模块

Babel 是如何工作的呢?

Babel 的编译过程可以分为三个阶段:

  • 解析(Parsing):将代码字符串解析成抽象语法树。
  • 转换(Transformation):对抽象语法树进行转换操作。
  • 生成(Code Generation): 根据变换后的抽象语法树再生成代码字符串。

解析:@babel/parser (Babylon)

使用 @babel/core 中的 parse API 来进行词法分析(分词+语法分析),转化成 AST,为 Transform 准备。
Tip:感兴趣的同学可以阅读这篇文章深入了解下:www.alloyteam.com/2017/04/ana… 本文重在讲述 babel 在项目中的实践呀。

转换:使用者可以决定的过程

使用 @bable/core 中的 transform API 来进行转义;我们熟知的插件就是应用在这一过程,如果这一阶段不使用任何插件,Babel 将在生成阶段输出原样的代码;

  1. plugins 和 presets 如何配合工作的呢?
  • Preset 是 Babel 官方做的一些插件集,可以把它理解成一些插件集合预设,减轻繁琐的配置。每年每个 Preset 只会编译当年批准的内容;
  • plugin/preset 执行顺序:对于转义的节点,babel 先按照配置文件的 plugins 数组配置去执行,执行完,再倒序读取 presets 配置项里每一个预设,如下图:

2. 常用插件 plugins: 可以根据项目开发需要去引入对于的插件,在 Bable plugins 查询文档 中查询,对于大部分插件可以通过升级 @bebel-preset-env 来引入,而对应实验性的语法,则需要单独引入对于的 plugin 来支持,在 Babel 各个阶段插件集合 中 Experimental 对应的插件列表中去查询; 例如:

  • @babel/plugin-proposal-class-properties
  • @babel/plugin-proposal-decorators
  • @babel/plugin-proposal-export-default-from
  • babeljs.io/docs/en/bab…
  1. 常用预设 presets:
  • @babel-preset-env:根据用户的自定义的配置项,来将 ES2015+ 转化为 ES5,详细说明可以看一下@babel-preset-env 配置说明
  • @babel/preset-react:转义 react 代码,详情可以阅读:www.babeljs.cn/docs/babel-…
  • @babel-preset-typescript:转义 typescript 代码,可以阅读该文章来加深对该插件的认识喔:iamturns.com/typescript-… 通常对于 react 项目,presets 使用 @babel/preset-env + @babel/preset-react 便 ok 了,但是随着 TypeScript 的火热,babel 7 为开发者带来了 @babel/preset-typescript ,用于支持 ts 开发,从下图可以看出,这个插件使用趋势在增长势头很猛呀;

  • 配置推荐
// react + typescript 
"presets": [
    [
      "@babel/preset-env",
      {
        "targets": "cover 95%, safari >= 7", # 根据项目实际情况配置
        "modules": "cjs",
        "useBuiltIns": "usage"
      }
    ],
    "@babel/react",
    "@babel/preset-typescript", # 如果项目使用 typescript 开发需要安装这个预设
  ]

生成:

用 babel-generator 通过 AST 生成 ES5 代码; TIP:如果在 babel 的配置中,不使用任何 presets 和 plugins,那么生成的代码和原代码无异;

你需要知道的 @babel-polyfill

背景:

  1. 由于 babel 本身只负责语法转换,比如将 ES6+ 语法转化为 ES5。但是如果有些对象、方法,浏览器本身不支持,比如:
    • 全局对象:Promise、WeakMap 等。
    • 全局静态函数:Array.from、Object.assign 等。
    • 实例方法:比如 Array.prototype.includes 等。 此时,需要引入 @babel-polyfill 来模拟实现这些对象与方法。
  2. @babel-polyfill 主要包含了core-js和regenerator两部分。
    • babel-polyfill:提供了如 ES5、ES6、ES7 等规范中 中新定义的各种对象、方法的模拟实现。
    • regenerator:提供 generator 支持,如果应用代码中用到 generator、async 函数的话用到。

不足:

  1. @babel-polyfill 造成代码代码体积过大 以往项目中使用 polyfill,常规的做法有两种:
    • 在入口 js 文件中 import @babel-polyfill
    • 在webpack 的 entry 配置里写入 @babel-polyfill
  2. @babel-polyfill 的会污染全局空间和内置原型对象 像Map,Array.prototype.find这些就存在于全局空间中。

@babel-polyfill VS @babel-runtime

@babel-runtime 出现背景

@babel-polyfill 的不足,在日常业务开发中,影响不是很大的,但是如果在通用的第三方库的开发中引入 @babel-polyfill,就会带来潜在的问题:比如我们项目中自定义了一份新的 API 的实现,但是引入的第三方通用库使用了polyfill,并且实现了同样的方法,就会将我们原有的方法覆盖(至少会有存在这样的风险),为了避免这样的问题,babel 推出了 @babel-runtime 来实现 @babel-polyfill 的绝大部分的功能,它将开发者依赖的全局内置 对象等,单独抽成模块,通过模块导入的方式,避免了对全局作用域的污染。 所以,如果是开发库、工具,可以考虑使用 @babel-runtime;

如何使用 @babel-runtime 来实现 polyfill

  1. 安装 @babel/plugin-transform-runtime 和 @babel-runtime
# babel-plugin-transform-runtime 用于构建过程的代码转换
npm install --save-dev babel-plugin-transform-runtime 
npm install --save babel-runtime
  1. .babelrc plugins 中添加 @babel/plugin-transform-runtime
{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "absoluteRuntime": false,
        "corejs": false,
        "helpers": true,
        "regenerator": true,
        "useESModules": false
      }
    ]
  ]

@babel-runtime + @babel/plugin-transform-runtime 实现细节

@babel/plugin-transform-runtime 插件主要做了如下三件事情:

  1. core-js aliasing:自动导入 @babel-runtime/core-js,将全局静态方法、全局内置对象映射到对应的模块;
# js
var sym = Symbol();

var promise = new Promise();

console.log(arr[Symbol.iterator]());
# 转化后
"use strict";

var _getIterator2 = require("@babel/runtime-corejs2/core-js/get-iterator");

var _getIterator3 = _interopRequireDefault(_getIterator2);

var _promise = require("@babel/runtime-corejs2/core-js/promise");

var _promise2 = _interopRequireDefault(_promise);

var _symbol = require("@babel/runtime-corejs2/core-js/symbol");

var _symbol2 = _interopRequireDefault(_symbol);

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj };
}

var sym = (0, _symbol2.default)();

var promise = new _promise2.default();

console.log((0, _getIterator3.default)(arr));
  1. Helper aliasing:将内联的工具函数移除,改成通过 @babel-runtime/helpers 模块进行导入
# js
class Person() {}

# 转化后
"use strict";

var _classCallCheck2 = require("@babel/runtime/helpers/classCallCheck");

var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj };
}

var Person = function Person() {
  (0, _classCallCheck3.default)(this, Person);
};
  1. Regenerator aliasing:用于在项目中使用 async/generator 时,自动导入 @babel-runtime/regenerator 模块
# js
function* foo() {}

# 转化后
"use strict";

var _regenerator = require("@babel/runtime/regenerator");

var _regenerator2 = _interopRequireDefault(_regenerator);

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj };
}

var _marked = [foo].map(_regenerator2.default.mark);

function foo() {
  return _regenerator2.default.wrap(
    function foo$(_context) {
      while (1) {
        switch ((_context.prev = _context.next)) {
          case 0:
          case "end":
            return _context.stop();
        }
      }
    },
    _marked[0],
    this
  );
}

@babel-runtime + @babel/plugin-transform-runtime 缺点

由于 @babel-runtime 和 @babel/plugin-transform-runtime 实现方式导致它不会对实例方法进行扩展(例如 Array.prototype.includes()),所以在使用中,存在很大的隐患,简单的说,如果你使用的是 @babel-runtime 实现的polyfill,那么,就会出现你使用的部分原型方法得不到支持而报错,这是不能接受的,所以我推荐在项目开发中,还是使用 @babel-polyfill 比较稳妥。

@babel/preset-env preset 带来的 @babel-polyfill 按需引用

TIP: 关于 @babel/preset-env 的详细内容可以查看下一小节的内容 通过在 presets 里使用 @babel/preset-env preset,设置 useBuiltInts 为 usage 或者或者 entry 来实现按需引入; 与@babel/preset-env一起使用时(注意,仍然需要安装@ babel-polyfill。)

  1. 如果在.babelrc 中指定 useBuiltIns:'usage',则不要在webpack.config.js 和 入口 js 文件引入 @babel-polyfill。
  2. 如果在.babelrc中指定了useBuiltIns:'entry',则通过require或import将@ babel / polyfill包含在应用程序入口点的顶部。
  3. 如果未指定 useBuiltIns 键或在 .babelrc 中使用 useBuiltIns:false 显式设置,请将@ babel / polyfill直接添加到webpack.config.js 对应的配置中去。

你需要了解的 @babel/preset-env

使用趋势

从 2018-9-17 发布至今,每周的下载量都在稳步提升,推荐小伙伴们在项目中使用基于 babel 7的 @babel/preset-env 插件预设;

简单介绍

@babel-preset-env 将基于你的实际浏览器及运行环境,自动的确定 babel 插件及 polyfill,编译ES2015 及此版本以上的语言,在没有配置项的情况下,@babel-preset-env 表现的同 babel-preset-latest一样(或者可以说同babel-preset-es2015、babel-preset-es2016、 babel-preset-es2017 结合到一起表现的一致)。

优点

  1. 可以根据配置的目标环境自动采用需要的 babel 插件;
    • 简化了 babel 配置
    • 按需引入必要插件,优化了包体积;
  2. 支持 polyfill 相关配置,按需引入相关的 polyfill,优化代码体积; TIP:可以通过 www.npmjs.com/package/@ba… 来查看项目版本使用的 @babel/preset-env 版本依赖的 plugins,保证代码中使用的 js 新的语言特性得到支持,如果没有,可以通过升级 @babel/preset-env 或者安装对应的 plugin 来支持;

不足:

对于实验属性(babel-preset-latest 不支持的)需要手动安装配置相应的 plugins 或者 presets; 可以通过 babeljs.io/docs/en/plu… 来查询相关的实验属性对应的插件,来引入支持业务开发;

配置指南

浏览器配置相关

  • 每个浏览器最近的两个版本和 IE 大于等于7的版本所需的 polyfill 和代码转译。
"babel": {
  "presets": [
    [
      "env",
      {
        "targets": {
          "browsers": ["last 2 versions", "ie >= 7"]
        }
      }
    ]
  ]
},
  • 支持市场份额超过5%的浏览器。
"targets": {
  "browsers": "> 5%"
}
  • 指定浏览器版本
"targets": {
   "chrome": 56
 }

polyfill 配置: useBuiltIns(默认 false)

@babel-preset-env 默认只支持对语法的转化,需要开启useBuiltIns配置才能转化 API 和实例方法, 来为标准库中的新功能提供了 polyfill,为内置对象,静态方法,实例方法,生成器函数提供支持。 @babel-preset-env 可以实现基于特定环境引入需要的polyfill。

  • 可选值包括:"usage" | "entry" | false, 默认为 false,表示不对 polyfills 处理;
  • entry:根据target中浏览器版本的支持,将polyfills拆分引入,仅引入有浏览器不支持的polyfill;
  • usage:检测代码中ES6/7/8等的使用情况,仅仅加载代码中用到的polyfills;

Node.js 配置相关

如果你通过Babel编译你的Node.js代码,babel-preset-env 很有用,设置 "targets.node" 是 "current",支持的是当前运行版本的nodejs:

const presets = [
  [
    "@babel/env",
    {
      node: 'current',
    },
  ],];
module.exports = { presets };

spec : 启用更符合规范的转换,但速度会更慢,默认为 false

loose:是否使用 loose mode,默认为 false

modules:将 ES6 module 转换为其他模块规范,可选 "adm" | "umd" | "systemjs" | #### "commonjs" | "cjs" | false,默认为 false

debug:启用debug,默认 false

include:一个包含使用的 plugins 的数组

exclude:一个包含不使用的 plugins 的数组

babel 6 迁移 babel 7

在迁移之前建议熟悉 babel 7 具体带来的改动,这样有助于我们在项目中进行具体的优化和进行针对性的配置修改

Babel 7 带来的改动

熟悉相关重要改动,可以帮助我们更好的去配置项目,使用最新支持的特性,提高编码效率

  1. 移除了年度预设用法
    带有env 的预设已经发布了一年多,现在它会取代一下所有的用法:
  • babel-preset-es2015
  • babel-preset-es2016
  • babel-preset-es2017
  • babel-preset-latest
  1. 移除 stage 预设用法,迁移时可以参考 github.com/babel/babel… 进行升级;也可以通过 babel-upgrade 来使用命令行进行替换
  2. 移除 @babel-polyfill 中的提议:可以参考一下这里:github.com/babel/babel…
  3. 包重命名:babylon 现在重新命名为 @babel/parser (看起来是被收了)
  4. 包命名空间
    babel 6 升级 babel 7 最重要的一个改动之一就是给所有的包加了命名空间,这样做可以减少很多意外或者故意的命名问题,可以与社区的其他插件有一个清晰的区分,形成一个 babel 的命名规范;例如:babel-preset-env 改名为 @babel/preset-env;
  5. 把 TC39 提议都换成 -proposal 把那些非年度预设的 TC39 插件中的-transform都换成了-propoal,这样可以更好的区分出一个提议是否为 javascript 官方的。例如:
  • @babel/plugin-tranform-function-bind 换成 @babel/plugin-proposal-function-bind
  1. 移除包中的年份
    一些插件中还带有 -es2015等字符,现在做了统一的规范:统统去掉,例如: @babel/plugin-transform-es2015-classes 换成了 @babel/plugin-transform-classes
  2. 分离 React 和 Flow 之间的预设
  3. 解析选项修改
    现在 Babel 的配置选项会比以前的 Babel6 更加严格。像这样的一个逗号分隔列表:"presets": "es2015, es2016"在以前的版本中可以运行,今后都要改成数组才可以。但是这个对 CLI 不受影响,它依然可以使用逗号分隔的字符串。
# right
{
    "presets": ["@babel/preset-env", "@babel/preset-react"]
}

# wrong
{
   "presets": "@babel/preset-env, @babel/preset-react"
}
  1. 插件/预设的暴露
    今后所有的插件和预设向外暴露的都必须是一个函数,而不能是一个对象,这可以帮助我们进行缓存。
  2. JSX Fragment Support (<>)
render() {
  return (
    <>
      <ChildA />
      <ChildB />
    </>
  );
}

// output 👇

render() {
  return React.createElement(
    React.Fragment,
    null,
    React.createElement(ChildA, null),
    React.createElement(ChildB, null)
  );
}
  1. TypeScript Support (@babel/preset-typescript)
    使用前(带有类型声明的 react 代码块)
interface Person {
  firstName: string;
  lastName: string;
}

function greeter(person : Person) {
  return "Hello, " + person.firstName + " " + person.lastName;
}

使用后(移除类型声明)

function greeter(person) {
  return "Hello, " + person.firstName + " " + person.lastName;
}
  1. Automatic Polyfilling (experimental)
    在 babel 6 中,对于 polyfill 的支持都是采用如下方式:
import "@babel/polyfill";

但它包括整个 polyfill,如果浏览器已经支持,你可能不需要导入所有内容。这与@babel/preset-env 试图用语法解决的问题相同,所以我们在这里将它应用于polyfill。选项 useBuiltins:“entry” 通过将原始导入仅拆分为必要的导入来实现此目的。 但是我们可以通过仅导入代码库中使用的 polyfill 来做得更好。选项“useBuiltIns:”usage“是我们第一次尝试启用类似的东西(详细说明)。 关于 @babel/preset-env 详细的说明,可以参考这篇文档:@babel-preset-env 配置说明 ,理解其中每一个配置,来利用 babel 新增的 api 来结合项目自身特点进行配置,进行优化。

推荐迁移步骤:

第一步:进入到项目根目录下:执行 Babel 升级脚本,主要进行如下三种类型的修改:

  1. 将 babel-xx 替换成 @babel/xx,替换成新的插件
  2. 升级 babel-loader 到 支持 babel 7 的版本
  3. 安装废弃的 stage 相关的预设对应的 plugins ,各个 stage 对应的插件查询库
# 不安装到本地而是直接运行命令,npm 的新功能
npx babel-upgrade --write

# 或者常规方式
npm i babel-upgrade -g
babel-upgrade --write

以一个依赖 babel 6 的项目为例:通过 babel 官方提供的升级工具,轻松搞定依赖相关的问题:

  • 对 babel-polyfill 进行包命名空间的转化的同时,又对其进行了升级,对于用户来说,只需要去配置文件中修改对应的包就 ok 了,毫无其他痛点,做的很友好;
  • 安装 babel-preset-stage-1 这个废弃的 preset 依赖的 plugins 来保证原有的功能可用
    • @babel/plugin-proposal-export-default-from
    • @babel/plugin-proposal-logical-assignment-operators
    • @babel/plugin-proposal-optional-chaining
    • @babel/plugin-proposal-pipeline-operator
    • @babel/plugin-proposal-nullish-coalescing-operator
    • @babel/plugin-proposal-do-expressions
  • 把 TC39 提议都转化为 -proposal 例如该项目中使用的 babel-plugin-transform-decorators-legacy -> @babel/plugin-proposal-decorators

第二步:根据上一步 package.json 文件的修改,同步修改 babel 文件中对应的配置,运行开发环境,根据 webpack 报错逐个解决

  • 升级 babel-plugin-module-resolver:babel 升级到 babel 7 之后,babel-plugin-module-resolve V2 的版本会出现报错,需要升级到 V3;

  • @babel/plugin-proposal-pipeline-operator 插件没有添加相关配置项报错
    • 处理方案:
 ["@babel/plugin-proposal-pipeline-operator", {
    # 由于项目中并没有使用管道符,所以我这里就直接给一个 minimal 的属性值,其他项目可以根据具体需要做调整
      "proposal": "minimal"
    }]
  • ES6 modules 与 commonJS 混用导致报错

- 解决方案:@babel/preset-env 的 modules 属性设置为 commonjs/cjs

第三步:优化项目 polyfill 配置

babel 7 带来了@babel/preset-env 这个插件,它扩展了对 polyfill 的配置,让使用者可以优化垫片的体积,做到按需加载,具体细节可以参考 @babel-preset-env 配置说明

  1. 移除入口文件引入的 @babel-polyfill
  2. 设置 @babel/preset-env useBuiltIns 属性为 usage,实现按需加载

  1. 设置 @babel/preset-env 的 corejs 属性,让该插件集不仅仅支持 stable ECMAScript Features,还支持 proposal ECMAScript Features

配置 Babel 支持 TypeScript 环境开发

可以参考我这篇 JavaScript 项目迁移 TypeScript 实践分享 文章来做迁移(在余下时间我会重新整理发布到掘金)

  1. 安装 @babel/preset-typescript 进行 babel 配置
# .babelrc
# yarn add @babel/preset-typescript
"presets": [
    [
      "@babel/env",
      {
        "targets": "cover 95%, safari >= 7",
        "modules": "cjs",
        "corejs": 2,
        "useBuiltIns": "usage",
      }
    ],
    "@babel/react",
    "@babel/typescript"
  ],
  1. 创建 TypeScript 配置文件 tsconfig.json
{
  "compilerOptions": {
    "module": "esnext",
    "target": "es6",
    "allowSyntheticDefaultImports": true,
    "baseUrl": ".",
    "sourceMap": true,
    "outDir": "../client-dist",
    "allowJs": true,
    "jsx": "react",
    "noEmit": true,
    "moduleResolution": "node",
    "isolatedModules": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    // `vs code`所需要的,在开发时找到对应的路径,真实的引用是在`webpack`中配置的`alias`
    "paths": {
    }
  },
  # 这里这么设计,是因为我们项目历史原因,js 的业务代码比较多,在跑 ci tsc 做类型检查的时候,只走 tsx? 类型文件
  "include": ["src/**/*.ts", "src/**/*.tsx"],
  "exclude": [
    "node_modules"
  ]
}
  1. webpack 配置修改,支持 TypeScript 开发环境
    • babel-loader 扩展对 .ts?x 文件解析
    • resolves 对应的 extensions 增加对 ts 与 tsx 文件的支持

4. package.json 扩展 TypeScript 类型检查命令

# package.json
{
  "scripts": {
     "tsc": "tsc --noEmit"
  }
}
  1. 扩展 TypeScript 相关的 Eslint 配置,进行代码规范约束
    • parser 由 babel-eslint 替换为 @babel-eslint/parser
    • extends 属性增加对 typescript 代码约束插件
      • prettier
      • prettier/react
      • prettier/@typescript-eslint
    • plugins 属性扩展 @typescript-eslint 插件

总结

本人想通过本文和大家分享一下自己关于 Babel 在项目中使用的一些心得,希望可以给小伙伴们带来一些帮助,Babel 不是一个黑盒子,而是一个给开发者提供了各种各样选择的工具集,术业有专攻,只需要稍微花一点时间来理解它,就可以在项目中很安全地使用它,利用 Babel 团队不断努力给我们带来的新的语言特性的支持; 大家有什么疑问可以直接给我留言评论。

参考

gitissue.com/issues/5c18… juejin.cn/post/684490… juejin.cn/post/684490… segmentfault.com/q/101000000… www.jianshu.com/p/d078b5f30… jsweibo.github.io/2019/08/05/… juejin.cn/post/684490… blog.hhking.cn/2019/04/02/… github.com/sorrycc/blo… github.com/Kehao/Blog/… juejin.cn/post/684490… jsweibo.github.io/2019/08/09/… github.com/SunshowerC/… zhuanlan.zhihu.com/p/43249121 pdsuwwz.github.io/2018/09/29/… - babel 升级踩坑 juejin.cn/post/684490… juejin.cn/post/684490… github.com/lmk123/blog… segmentfault.com/a/119000001… segmentfault.com/a/119000001… www.zcfy.cc/article/bab… juejin.cn/post/684490…