分类
前端工具链

babel@6 升级到 babel@7,兼容性代码膨胀的原因

最近尝试把厂里项目的依赖从 babel@6 升级到 babel@7,发现打包之后体积大了很多。于是打开 webpack-bundle-analyzer,果然大部分代码都是 corejs 引入的,项目本身的逻辑只占少部分。

从报告来看,虽然目标浏览器的版本均高于 Promise 的启动版本(比如 Chrome 32),但 es.promise 仍然会被打包进来。于是以 es.promise 为突破口开始分析,找到答案:因为 JavaScript 引擎 V8 直至 v6.6 版本时,在 Promise 实现方面都存在严重 bug,所以 babel@7 保守地选择 Chrome 66/67 作为临界点。

想来其它体积多半也是这么加上来的,就不再一个一个排查了。 ​​​

这就很难处理。不升级吧,新特性兼容不了;升级吧,包体积变大,公司上层又未必同意。

下一步试试 esbuild 吧,或者回头手动打包一套 polyfill。目前设想的方案是:

  1. 用 babel 之类的工具提取出所有特性
  2. 根据 caniuse 生成必须的特性列表
  3. 像上面说的 Promise,因为 bug 所以必须兼容,我们就不考虑了,可以反过来加一条 eslint 规则规避
  4. 最终生成新的 polyfill 打包进来
分类
js

正确使用 @babel/preset-env 与 @babel/plugin-transform-runtime

这几天又在折腾项目脚手架,看到之前的配置里用到 transform-runtime,于是就想研究下。找了半天,发现现有的文章、讨论都没有准确、合理的说明,所以写篇博客介绍一下。

定位

@babel/preset-env

@babel/preset-env 是一大堆插件的集合,包含了当前浏览器环境下,所有语言特性的插件,可以根据 browserslist 的结果,选择合适的插件将新语言特性转译成旧浏览器可以支持的表达方式。

官方推荐使用预制件(preset),可以大大节省配置时间,提高开发效率。

@babel/plugin-transform-runtime

babel 转译时,往往需要用到一些工具函数,比如 _extend。转译以文件为单位,意味着每个文件里都可能有重复的 babel 工具函数。现代化项目常有大量的文件,就可能产生重复和浪费。

@babel/plugin-transform-runtime 可以帮我们把直接写入文件里工具函数变成对 core-js 的引用,可以减少转译后的文件大小。

使用场景

从上面的定位可以看出,@babel/plugin-transform-runtime@babel/preset-env 并不重合,两者的功能没有交集。

对于任何需要考虑兼容性的项目,我们都应该使用 babel 进行转译,并使用 @babel/preset-env 降低配置的复杂度,大部分通用的配置如下:

module.exports = {
  'presets': [
    [
      '@babel/preset-env',
      {
        'corejs': 3, // 使用 core-js@3 版本,core-js@2 从 ES2017 之后就没再更新了,不推荐使用
        'useBuiltIns': 'usage', // 只转译用到的新语言元素
        'bugfix': true, // v7.9 之后引入的新选项,尽量减小转译后的代码体积,v8 之后会变成默认选项
      },
    ],
  ],
};

小型项目可以到此为止,中大型项目、文件数量比较多的,可以引入 @babel/plugin-transform-transform。此时我们需要先安装两个依赖——注意,这俩依赖都要装:

# 用来开发、debug 和编译
npm i @babel/plugin-transform-runtime -D

# 用来在生产环境中使用
npm i @babel/runtime
# 如果要使用特定的 core-js 版本,也可以安装特定的 runtime
npm install --save @babel/runtime-corejs2
npm install --save @babel/runtime-corejs3

接下来,修改上面的 babel.config.js,加入插件配置:

module.exports = {
  // 重复上面的配置
  "plugins": [
    // 其它插件
    [
      "@babel/plugin-transform-runtime",
      // 一般情况下我们使用默认配置即可
    ],
  ],
};

完成。

库项目

库类项目也推荐使用 @babel/plugin-transform-runtime,因为库项目通常会面临另一个问题。如果我们直接导入 core-js 作 polyfill 的话,像 PromiseSetMap 这样的全局对象就会被覆盖。对于一般的应用而言,问题不大;但如果是库,你无法预期其它开发者会在什么情况下使用你的库,很可能他的目标平台都支持这些新语法元素,不希望转译污染。

此时,使用 @babel/plugin-transform-runtime 可以让 babel 在转译时使用沙箱包装 polyfill,避免污染全局。

参考文档

分类
前端工具链

使用 Webpack 动态打包文件夹

各式各样功能强大的小语言是我厂机器编程的特色,为了让更多的同学提前感受到 DSL 的威力,我们开发了 demo 应用:https://demo.openresty.com.cn/

Demo 里面,需要添加一些范例代码,方便用户直接体验。这些代码,可以通过后端 API 获取,也可以直接编译到前端代码里。目前范例很少,直接打包到一起应该是最简单的做法。

但是如何打包是个问题。经过一些研究,我觉得这样做好:

  1. 负责小语言的同事直接把范例写在项目仓库里,Edgelang 就用 .edge 扩展名,Navlang 就用 .nav 扩展名
  2. 前端在 webpack 里实现一个 requireAll 的功能,把所有代码当成纯文本用 raw-loader 加载进来
  3. 这样添加了代码后,重新 build 一下就好。范例文件可以用 lazy-load 的方式加载

Webpack 提供了一个方法叫 require.context,可以深度遍历一个目录,返回一个 context module,用这个 API 我们就可以动态的加载这个目录下的文件——具体的讲解和参数说明大家看下文档吧,这个部分中文资料不太多,我不太确定译名。我们可以在范例代码的目录下放一个 index.js,负责加载所有代码范例:

function requireAll(r) {
  r.keys().forEach(r);
}

const Languages = [
  'edgelang',
  'navlang',
];

let context = require.context('./edgelang', true, /\.(edge|css)$/);
requireAll(context);
context = require.context('./navlang', true, /\.(nav|css)$/);
requireAll(context);

export default Languages;

然后我们改一下 webpack.config.js 就可以了。

module.exports = {
  module: {
    rules: [
      {
        test: /\.(nav|edge|fan)$/,
        use: 'raw-loader',
      },
    ],
  },
};