正确使用 @babel/preset-env

2022-12-22 · 2,077 chars · 11 min read

@babel/preset-env 想必大家都很熟悉了,它会自动对代码进行转换,添加 polyfill。让我们可以在一定程度上,自由使用最新的语法特性,而不必担心浏览器的兼容问题。

但最近在对一个“老旧工程”进行性能优化的时候,发现引入的 core-js 体积比较大,更严重的是,多了预期之外的 polyfill。

这个项目的 browserslist 的配置如下:

# .browserslistrc

iOS >= 11
Android >= 6

browserslist 配置加上 bundle analyzer 的图,对一下不难发现问题。iOS 11+ 和 Android 6+ 都是支持 Promise 的,但是包里面依然有 es.promise.js。这显然是存在什么问题的。

排查#

这个问题排查起来并不麻烦,调整下 browserslist 的配置,例如仅设置 iOS >= 11,就能明显的发现,core-js 只有 66KB 左右了,es.promise.js 也没有了。

这说明 babel-preset-env 和 browserslist 的运行是没有问题的,问题出在了我们的配置上。错误的配置导致了多余的 polyfill 被引入。

Broswerslist 的 Github 有一个表格记录了所有支持的浏览器名字,我把上文提到的浏览器截了出来。

Browser NameDesktopAndroidiOSOther Mobile
Android (WebView)Android
SafariSafariiOS ios_saf

可以看到我们前文配置的浏览器名是没有错的,对于 H5 页面,主要就是 Android Webview 和 iOS Safari。

Analyze Browserslist Config#

既然浏览器名称没错,就看看 broswerslist 解析完后,具体对应的版本是哪些。直接执行 npx browserslist,输出:

android 108
ios_saf 16.1
ios_saf 16.0
ios_saf 15.6
ios_saf 15.5
ios_saf 15.4
ios_saf 15.2-15.3
ios_saf 15.0-15.1
ios_saf 14.5-14.8
ios_saf 14.0-14.4
ios_saf 13.4-13.7
ios_saf 13.3
ios_saf 13.2
ios_saf 13.0-13.1
ios_saf 12.2-12.5
ios_saf 12.0-12.1
ios_saf 11.3-11.4
ios_saf 11.0-11.2

iOS 正常,但是 android 只有一个 108。为什么只有一个版本???为什么是 108???Android 最新版也才 13!

说明

你看到的可能和我不一样,数据是在不停更新的。目前 Android 最新就是 108,iOS 就是 16.1。

如果同样的配置,你看到的版本比我还低,说明你该更新了。

这样的提示应该很多人看到过:

Browserslist: caniuse-lite is outdated. Please run:
  npx browserslist@latest --update-db
  Why you should do it regularly: https://github.com/browserslist/browserslist#browsers-data-updating

Debug @babel/preset-env#

上面是 broswerslist 输出的版本信息,@babel/preset-env 也可以 debug,我们打开 debug:

[
  '@babel/preset-env',
  {
    useBuiltIns: 'usage',
    corejs: 3,
    modules: false,
    debug: true // 打开 debug
  },
]

执行构建,观察输出:

@babel/preset-env: `DEBUG` option

Using targets:
{
  "android": "36",
  "ios": "11"
}

Using modules transform: false

Using plugins:
  proposal-numeric-separator { "android":"36", "ios":"11" }
  proposal-logical-assignment-operators { "android":"36", "ios":"11" }
  proposal-nullish-coalescing-operator { "android":"36", "ios":"11" }
  proposal-optional-chaining { "android":"36", "ios":"11" }
  proposal-json-strings { "android":"36", "ios":"11" }
  proposal-optional-catch-binding { "android":"36", "ios":"11" }
  transform-parameters { "android":"36" }
  proposal-async-generator-functions { "android":"36", "ios":"11" }
  proposal-object-rest-spread { "android":"36", "ios":"11" }
  transform-dotall-regex { "android":"36", "ios":"11" }
  proposal-unicode-property-regex { "android":"36", "ios":"11" }
  transform-named-capturing-groups-regex { "android":"36", "ios":"11" }
  transform-async-to-generator { "android":"36" }
  transform-exponentiation-operator { "android":"36" }
  transform-template-literals { "android":"36", "ios":"11" }
  transform-literals { "android":"36" }
  transform-function-name { "android":"36" }
  transform-arrow-functions { "android":"36" }
  transform-block-scoped-functions { "android":"36" }
  transform-classes { "android":"36" }
  transform-object-super { "android":"36" }
  transform-shorthand-properties { "android":"36" }
  transform-duplicate-keys { "android":"36" }
  transform-computed-properties { "android":"36" }
  transform-for-of { "android":"36" }
  transform-sticky-regex { "android":"36" }
  transform-unicode-escapes { "android":"36" }
  transform-unicode-regex { "android":"36", "ios":"11" }
  transform-spread { "android":"36" }
  transform-destructuring { "android":"36" }
  transform-block-scoping { "android":"36" }
  transform-typeof-symbol { "android":"36" }
  transform-new-target { "android":"36" }
  transform-regenerator { "android":"36" }
  proposal-export-namespace-from { "android":"36", "ios":"11" }
  syntax-dynamic-import { "android":"36", "ios":"11" }
  syntax-top-level-await { "android":"36", "ios":"11" }

Using polyfills with `usage` option:

[/Users/keenwon/Code/st-xxx/src/index.js] Based on your code and targets, core-js polyfills were not added.

[/Users/keenwon/Code/st-xxx/src/polyfill.js] Based on your code and targets, core-js polyfills were not added.

[/Users/keenwon/Code/st-xxx/src/app.jsx] Based on your code and targets, core-js polyfills were not added.

[/Users/keenwon/Code/st-xxx/src/routes.js] Added following core-js polyfills:
  es.object.to-string { "android":"36" }
  es.promise { "android":"36" }

# ...后面的省略...

神奇,这里的 Android 版本变成了 36。😂

误区#

带着这些疑惑,又重新翻看了 @babel/preset-env、core-js、browserslist 的文档和一些源码,发现我之前一直存在几个很致命的误区。

误区一:Android WebView 的配置问题#

Browserslist 通过 caniuse-lite 这个包,使用 caniuse 的数据,把 targets 转为具体的浏览器版本。但是 caniuse 的数据有个大问题:从 4.4 开始,它的 android browser 仅有最新版的数据,也就是上文看到的 108。

这个问题要拆开看,首先是为什么 4.4 之后的版本有个大跳跃?这个 caniuse 已经给出了解释:

Android browser/WebView version numbers through 4.4 refer to the version of Android OS. Support listed is for the Android core; it should be noted that many hardware vendors (Samsung, HTC, etc.) use altered version of their default browser which may include more/less/buggy support.

Starting in Android 5, the web engine can be updated separately, so the latest Chromium version number is used instead.

简而言之就是 4.4 之后使用的新的 Chromium WebView,并且可以独立的更新,所以版本号有个大的跳跃。

其次,caniuse 为什么只有最新版的数据?这个问题在这个 issues 里有说明,一是数据源的问题,再者就是兼容性与桌面端差异不大。

这是我截取的部分数据(完整版在 https://github.com/Fyrd/caniuse 上看):

{
  "android": {
    "browser": "Android Browser",
    "long_name": "Android Browser / Webview",
    "abbr": "And.",
    "prefix": "webkit",
    "type": "mobile",
    "usage_global": {"2.1": 0, "2.2": 0, "2.3": 0, "3": 0, "4": 0.0643678, "4.1": 0.0257471, "4.2-4.3": 0.0772413, "4.4": 0, "4.4.3-4.4.4": 0.296092, "108": 0},
    "version_list": [
      {"version": "2.1", "global_usage": 0, "release_date": 1256515200, "era": -9, "prefix": ""},
      {"version": "2.2", "global_usage": 0, "release_date": 1274313600, "era": -8, "prefix": ""},
      {"version": "2.3", "global_usage": 0, "release_date": 1291593600, "era": -7, "prefix": ""},
      {"version": "3", "global_usage": 0, "release_date": 1298332800, "era": -6, "prefix": ""},
      {"version": "4", "global_usage": 0.0643678, "release_date": 1318896000, "era": -5, "prefix": ""},
      {"version": "4.1", "global_usage": 0.0257471, "release_date": 1341792000, "era": -4, "prefix": ""},
      {"version": "4.2-4.3", "global_usage": 0.0772413, "release_date": 1374624000, "era": -3, "prefix": ""},
      {"version": "4.4", "global_usage": 0, "release_date": 1386547200, "era": -2, "prefix": ""},
      {"version": "4.4.3-4.4.4", "global_usage": 0.296092, "release_date": 1401667200, "era": -1, "prefix": ""},
      {"version": "108", "global_usage": 0, "release_date": 1669939200, "era": 0, "prefix": ""}
    ],
    "current_version": "108"
  },
}

综上,第一个误区就是 Android WebView 的配置,我们不应该配置 Android x,即使是最新版本的 Android 13,表现和 Android 5、6 也没什么区别。

目前应该没有应用会继续兼容 Android 5 以下的版本了,所以后续我们应该在 browserslist 中使用 ChromeAndroid 或者 and_chr,直接控制 WebView 版本(而不是 Android 版本)。

Tip

除了 caniuse,mdn 文档也会展示 api 的浏览器兼容性,它的数据存放在这儿,对比 Android ChromeAndroid WebView 可以发现,从 version 37 开始,Android Chrome 和 Android WebView 基本是一致的。

误区二:@babel/preset-env 的数据源#

大多数人都习惯了通过 caniuse 查询一个特性的兼容性。但实际上,@babel/preset-env 根本没有直接使用 caniuse 的兼容性数据。

@babel/preset-env 实际上是通过 browserslist 将 targets 解析为具体的浏览器版本,然后使用 core-js-compat 的兼容性数据确定要插入哪些 polyfill(更具体的说明,可以看这里)。

这就解释了为什么前文的例子里,会有 es.promise.js 了。虽然 caniuse 写了 android 6+ 是支持 Promise 的。但 Android >= 6+ 被 babel 转换成了 android 36(最小只有 36)。在 core-js-compat 中,android 67+ 才支持 Promise。

我写了几行代码,debug 了一下:

const getTargets = require('@babel/helper-compilation-targets');

const fn = typeof getTargets === 'function' ? getTargets : getTargets.default;

console.log(fn({ browsers: ['Android >= 6'] }));
// { android: '36.0.0' }

core-js-compat 的兼容性数据可以看这里

总结#

总结一下就是:

  • @babel/preset-env 并未使用 caniuse 的兼容性数据,所以要看 core-js-compat
  • 在 browserslist 中,直接写 Android >= 6.0 是错误的,要写具体的 Chromium WebView 版本

对于海外应用,Google Play 内可以自动升级 WebView;国内应用一般跟随系统升级。所以最好的方式还是直接统计用户的 WebView 版本,选择一个合适的最小兼容版本号。

若是没有这样的统计数据,也可以考虑查一下每个 Android 版本初始使用的 WebView 版本号,比如 Android 7 对应的就是 WebView 51:

我大概翻了翻 Android 源码,把主要大版本对应的初始 WebView 捞了出来,供大家参考。理论上讲,用户的 WebView 版本应该“只大不小”,我的三星手机升级 One UI 5、Android 13,自带的版本就已经是 104.0.5112.97 了。

Android 版本初始 WebView 版本
Android 13101
Android 1291
Android 1183
Android 1074
Android 966
Android 858
Android 751
Android 644
Android 537

⚠️ 警告

以上数据仅供参考,使用时请严格测试,并时刻关注线上异常以便及时调整!

参考#

赞赏

微信