再学babel配置

babel 每次学习都有新的理解,哪怕是其配置都与我们前端生态中的各种概念息息相关。近期再次复习babel知识从而更好的编写js类库,本文是学习过程所做的记录。

babel 生态里的一些npm模块

  • babel/core 核心转义功能
  • babel/cli 命令行工具,可以通过babel命令来转换代码,或者转换文件,输出转义后的文件。如 babel src --out-dir lib --watch
  • babel/node 是用他来执行es6+代码,以node方式执行。适合于去执行你写的nodejs代码。

那么,我们使用 webpack 来打包js代码时,是使用的什么呢?答案是 babel-loader。

babel-lodaer 是适配 webpack 的一个模块,其内部就是调起 babel/core 来实现代码转译。

理解转译其实有2种类型的转译

记住:babel自身唯一能做的事情叫:代码转译。
至于新增api那些,那是需要引入polyfill垫片的事情(下文会讲)。

babel本身所做的事情,一般都叫他代码转译。那么所谓的转译都有哪些种类呢?如下:

1、单纯的语法解析和转换。例如箭头函数转成function
2、需要一点辅助的语法解析和转换,例如 class语法,给你转成prototype原型语法

解释下第二点:我们知道,一种新的语法意味着新的一些关键字和符号等,那么对于一些特殊符号如箭头函数,那么他恰好有es5对应的 function 关键字,所以babel只需简单换一下字符。

然而有些却不是简单的换一下,例如 class 语法,babel给你转成 es5 之后,他必须改造成函数和prototype的写法,在这种情况下,babel编译后的代码中会加入一些辅助函数(也就是下文说的babel/runtime所干的事儿)以协助来用es5的方式实现 class。

举个栗子:
image.png

看到了吗,babel生成的原型式写法中,需要在构造函数中对调用者的实例化方式进行检测,这些都封装成了 helper函数 _classCallCheck。

除此之外,还有更复杂的场景 babel转译之后甚至需要依赖polyfill垫片:例如 async 转换。 babel 会把 async 语法代码转成 generator 辅助函数,而 generator 辅助函数的功能需要依赖 regenerator-runtime 这个polyfill垫片。如下图,babel给你把async语法转换后,多了很多辅助函数;甚至其中有一个 regeneratorRuntime 的函数是没有找到定义的,而这个函数其实就是依赖全局需要引入 regenerator-runtime 这个 polyfill 才行。

image.png

理解什么是 corejs 和 babel/runtime

首先,我们要理解什么是 polyfill 什么是辅助函数。

像上文所说的2种babel转译,其中转换之后所出现的那些辅助函数,叫做运行时所需要的helper,他们其实就是 runtime。 而这些运行时函数,有一个单独的 npm 包去实现他们,叫做 babel/runtime。

而像那些 es6 新增的 API,例如 Promise、Map、WeakMap。数据新方法 flat、includes 等等 api,这些东西不属于 babel语法编译的范畴,是属于一些新的api。这些叫做 垫片,英文名叫 polyfill。在社区里,polyfill垫片通过另外2个npm包来实现:一个叫做 corejs(用来实现除了generator之外的垫片,现在要使用他的版本3),另一个叫做 regenerator-runtime(用来实现generator垫片)。

实战:配置 babel 的最佳实践

其实最佳实践的前提就是正确的理解上文中两个概念。从而正确的使用 babel/runtime 和 corejs3.

如果比喻成做饭,babel/runtime 和 corejs3 就是我们的食材。有了食材,我们就要用一个配套的刀具来加工他们。

  • babel/runtime配套的刀具是:babel/plugin-transform-runtime
  • corejs3的配套刀具是 preset/env 预设。

我们接下来所讲的实践,其背景是在 webpack + babel 下的实践。因为,毕竟我们通常不会单据来用 babel 操作一个 js 文件。

webpack 打包一个网站应用js时

这种场景的要求是:

  • 按需转译语法和辅助函数。例如如果我的目标平台支持了箭头函数,那么babel请不要给我转换成es5那种语法。
  • 全局polyfill垫片即可,不怕污染,因为我期望面向我产品所有用户使用的浏览器
  • 尽可能的少打包垫片。即按需polyfill。因为我们期望尽可能减少不必要的体积。比如我面向的用户浏览器大于IE10,那么IE9的polyfill不要打;同时,如果我代码中没有使用promise,那么promise的垫片也不要给我打
  • helper那些辅助函数,不要每个js模块中都写一份(因为babel本身肯定是针对每个js编译的,所以默认每个js肯定都会出现辅助函数)

解决方法:

1、 为了能按需转译语法。在最新babel7之后,我们只需使用preset/env预设就很简单了。

// babel.config.js
module.exports = {
  "presets": [
    [
      "@babel/env",
      {
        "targets": "last 50 Chrome versions",
        "useBuiltIns": false, // polyfill配置先关掉,后面再讲
      }
    ]
  ]
}

我们可以通过修改 targets ,来决定babel编译哪些语法。例如你把chrome版本号调到最新1个版本,那么箭头函数必然是不会转换的。

2、 3、按需加载polyfill

按需加载polyfill有2种方式,一种是把 useBuiltIns配置成 entry,另一种是配置成 usage.

使用上的区别是:

  • entry:需要你手工在你的webpack 入口js里,引入一下corejs和regenerator-runtime这俩polyfill。babel编译后,会自动在入口js里,把你那2行换成面向目标targets按需引用的corejs模块。
  • usage:不需要你手工引入。babel会自动把 corejs库的模块放到你的每个js模块里。放置的原则就是:不仅面向目标targets来按需引入,而且还按照你代码中是否使用来引入。假如你的a.js里用了promise,那么他会把corejs中promise模块引入。

如下是usage的方式:

module.exports = {
  "presets": [
    [
      "@babel/env",
      {
        "targets": "last 50 Chrome versions",
        "useBuiltIns": 'usage',
        "corejs": 3 // 写死就好!当前阶段就该用3
      }
    ]
  ]
}

如下是entry方式

//babel.config.js
module.exports = {
  "presets": [
    [
      "@babel/env",
      {
        "targets": "last 50 Chrome versions",
        "useBuiltIns": 'entry',
        "corejs": 3
      }
    ]
  ]
}

entry模式时,需要在代码里手工引入一下。

// index.js
// 手工引入
import "core-js/stable";
import "regenerator-runtime/runtime";

既然usage这么好,我们何苦要用entry呢? 所以,就用usage模式吧!

4、问题来了:怎么解决辅助函数的冗余问题。

babel/runtime是干啥的,要杂用?

上文的配置,我们解决了polyfill的问题。下一步,我们要解决辅助函数在每个js里都有冗余的问题。因为现在的配置下,如果你有 a.js 和 b.js,那么两个js中都会被babel放入那些 helper 辅助函数。当webpack打包完成一个 bundle。学过webpack原理的应该指到:最终的bundle里也会有 a.js 和 b.js模块,每个模块中必然也存在重复的辅助函数。

怎么解决? 那就用 babel/plugin-transform-runtime 插件。
这个插件的工作就是:在babel编译每个js时,把里面的辅助函数给换成 对 babel/runtime 的 require 引用。

当每个js里的辅助函数,都变成 babel/runtime的引用。那么webpack打包后的bundle,必然这些辅助函数就变成一个公共模块了,解决了冗余问题。

配置方式:

module.exports = {
  "presets": [
    [
      "@babel/env",
      {
        "targets": "last 50 Chrome versions",
        "useBuiltIns": 'usage',
        "corejs": 3
      }
    ]
  ],
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "helpers": true,  // 这就是抽离helper 的配置
      "corejs": false, // 先设置false,后面再讲
      "regenerator": false // 先设置false,后面再讲
    }]
  ]
}

上面把 helper配置为true,就可以实现helper函数抽到 babel/runtime。

至此,网站打包讲解配置完成!开发网站时就用上述配置即可。

webpack 打包一个 jssdk 时

开发jssdk跟开发网站对bundle.js有不同的要求。

  • 我们期望我们的sdk可以支持很多target环境,因此需要polyfill垫片。但我们不知道jssdk的运行环境是否有垫片。这里有2个方法,方法1:通过文档告诉开发者你需要加哪些垫片;方法2:我们自己polyfill,但是不能污染全局
  • jssdk如果自己进行polyfill。那当然也希望按需polyfill,减少体积
  • 辅助函数同样需要减少冗余,跟上文网站相同。

可以显而易见,对于开发jssdk,我们第一要做的,就是先把全局polyfill给他关掉。比如关掉那个 usage:

module.exports = {
  "presets": [
    [
      "@babel/env",
      {
        "targets": "last 50 Chrome versions",
        "useBuiltIns": false
      }
    ]
  ]
}

幸运的是,jssdk的另外几个要求,靠 babel/plugin-transform-runtime 都可以搞定。如下配置即可:

module.exports = {
  "presets": [
    [
      "@babel/env",
      {
        "targets": "last 50 Chrome versions",
        "useBuiltIns": false
      }
    ]
  ],
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "helpers": true,  // 这就是抽离helper 的配置
      "corejs": 3, // 这是局部polyfill的配置
      "regenerator": true // 这是局部polyfill generator的配置
    }]
  ]
}

我们只需把该插件的配置全开。把 preset/env 的polyfill配置关掉,就是一个适配jssdk开发的配置了。

阅读 182

推荐阅读
青檬前端
用户专栏

一个轻量级的前端技术分享专栏

1293 人关注
24 篇文章
专栏主页