致我们学前端的小时光—corejs与env、runtime的不解之缘

13,934 阅读9分钟

前言

文章具有翻译向,具体的可以看文末的链接。

随着ES6的正式发布,以及ES2016、ES2017...每年的稳定更新,还有新提案的不断出现,使得JavaScript越来越成熟。但是对于一些新的语法和API,老版本的浏览器无法全面兼容。babel的出现,解决了在老版本浏览器使用新语法的问题,它现在不仅仅是一个ES6 to ES5单纯的语法转换工具,更是一个规范和生态,帮我们去更高效的处理JavaScript。

@babel/preset-env登场

@babel/preset-env是作为babel-preset-es2015的替代品出现的,主要的作用是用来转换那些已经被正式纳入TC39中的语法。所以它无法对那些还在提案中的语法进行处理,对于处在stage中的语法,需要安装对应的plugin进行处理。

除了语法转换,@babel/preset-env另一个重要的功能是对polyfill的处理。新加入标准库的,可能是一些语法特性,比如箭头函数等,还有可能是一些新的API,比如promise、set、inclues等。

对于语法,babel可以通过生成静态语法树,去做一些转换,生成对应的ES5的代码。

但是对于新的API,需要浏览器去原生支持,或者使用大量的代码去进行API的模拟。@babel/polyfill就是API的垫片,通过引入这些垫片,使得低版本的浏览器能模拟实现那些新的API。

而今天的主角core-js就是@babel/polyfill的核心依赖,它现在已经发布了3.0的版本,而且@babel/preset-env在7.4.0的版本已经支持这个最新的版本。大版本的升级,会带来一些破坏性,但是相应的也会带来很多优势。

core-js 被遗忘的包

core-js是什么

  • 它是JavaScript标准库的polyfill
  • 它尽可能的进行模块化,让你能选择你需要的功能
  • 它可以不污染全局空间
  • 它和babel高度集成,可以对core-js的引入进行最大程度的优化

core-js升级的动机

  • core-js中的破坏性变更只能在主版本的升级中进行
  • core-js@2.0的版本已经在一年半之前冻结,所有的新特性只会添加到3.0的分支中

core-js@3的重要改变

  • 对于ECMAScript中已经稳定的功能,core-js已经几乎完全支持,并在core-js@3中引入了一些新的功能
  • 对于一些已经加入到ES2016-ES2019中的提案,现在已经被标记为稳定功能
  • 增加了proposals配置项,对处在提案阶段的api提供支持,但是因为提案阶段并不稳定,在正式加入标准之前,可能会有大的改动,需要谨慎使用;对于一些改变巨大的提案,也进行了对应的更新
  • 增加了对一些web标准的支持,比如URL 和 URLSearchParams
  • 删除了一些过时的特性

monorepos 包的拆分

core-js@2一个最常见的问题就是包的体积太大(~2M),并且有很多重复的文件被引用。基于这个原因,core-js@3对包进行拆分,三个核心的包分别是

  • core-js:定义全局的polyfill(~500k, 40k minified and gzipped)
  • core-js-pure:提供不污染全局环境的polyfill,等价于core-js@2/library(~440k)
  • core-js-compat:包含了core-js模块和API必要的数据,通过browserslist来生成所需要的core-js模块的列表

在以前的版本中,已进入ECMAScript标准的特性用es6.的前缀来表示,提案阶段的特性用es7.的前缀来表示,选择这个前缀的原因是在2014年的时候ES6以后的所有特性都考虑使用ES7来进行命名。

在cores-js@3的版本中,所以规范中的特性都使用es.这个前缀,而提案中的特性使用esnext.这个前缀。

几乎所有的CommonJS的入口文件都已经发生改变。在core-js@3中,包含了更多的模块入口。这使得对于目标浏览器的按需支持更加的具有灵活性,同时可以带来文件大小方面的优化。

在core-js@2中,@babel/preset-evn在插件内部有一个data-table,维护了不同浏览器对于特定API的支持,通过这个data-table来实现不同targets按需加载所需要的core-js模块。由于这个compat-table存在一些固有的问题,作者重新维护了一个包,即core-js-compat,用来提供不用目标引擎所需要的core-js的模块信息。

const {
  list,              // array of required modules
  targets,           // object with targets for each module
} = require('core-js-compat')({
  targets: '> 2.5%', // browserslist query
  filter: 'es.',     // optional filter - string-prefix, regexp or list of modules
});

console.log(targets);

/* =>
{
  'es.symbol.description': { ios: '12.0-12.1' },
  'es.array.reverse': { ios: '12.0-12.1' },
  'es.string.replace': { firefox: '63', ios: '12.0-12.1' },
  'es.string.trim': { ios: '12.0-12.1' },
  'es.promise': { firefox: '63' },
  'es.promise.finally': { firefox: '63' },
  'es.array-buffer.slice': { ios: '12.0-12.1' },
  'es.typed-array.int8-array': { ios: '12.0-12.1' },
  'es.typed-array.uint8-array': { ios: '12.0-12.1' },
  'es.typed-array.uint8-clamped-array': { ios: '12.0-12.1' },
  'es.typed-array.int16-array': { ios: '12.0-12.1' },
  'es.typed-array.uint16-array': { ios: '12.0-12.1' },
  'es.typed-array.int32-array': { ios: '12.0-12.1' },
  'es.typed-array.uint32-array': { ios: '12.0-12.1' },
  'es.typed-array.float32-array': { ios: '12.0-12.1' },
  'es.typed-array.float64-array': { ios: '12.0-12.1' },
  'es.typed-array.from': { ios: '12.0-12.1' },
  'es.typed-array.of': { ios: '12.0-12.1' }
}
*/

对于core-js@3新的入口文件,下面有一些简单的例子

// polyfill all `core-js` features:
import "core-js";
// polyfill only stable `core-js` features - ES and web standards:
import "core-js/stable";
// polyfill only stable ES features:
import "core-js/es";

// if you want to polyfill `Set`:
// all `Set`-related features, with ES proposals:
import "core-js/features/set";
// stable required for `Set` ES features and features from web standards
// (DOM collections iterator in this case):
import "core-js/stable/set";
// only stable ES features required for `Set`:
import "core-js/es/set";
// the same without global namespace pollution:
import Set from "core-js-pure/features/set";
import Set from "core-js-pure/stable/set";
import Set from "core-js-pure/es/set";

// if you want to polyfill just required methods:
import "core-js/features/set/intersection";
import "core-js/stable/queue-microtask";
import "core-js/es/array/from";

// polyfill reflect metadata proposal:
import "core-js/proposals/reflect-metadata";
// polyfill all stage 2+ proposals:
import "core-js/stage/2";

core-js@3 与 babel

如上面提到的,core-js与babel是高度集成的,babel的集成给core-js的按需加载提供了可能。在babel7.4.0的版本中已经支持core-js@3的版本。

@babel/prest-env

在升级到7.4.0以上的版本以后,既支持core-js@2,也支持core-js@3。所以增加了corejs的配置,来控制所需的版本,默认是core-js@2并且会有文字输出提示升级到3的版本。

@babel/prest-env可以通过配置useBuiltIns来根据targets加载@babel/polyfill。

@babel/polyfill的改动

@babel/polyfill是一个简单的包,包含core-js和regenerator-runtime这两个包。当core-js升级到3.0的版本后,将放弃使用@babel/polyfill,因为它只包含core-js 2.0的版本。

所以在@babel/prest-env升级到7.4.0并且使用core-js@3,需要做如下的替换工作

// 安装core-js@3.0 和 regenerator-runtime
yarn add core-js@3
yarn add regenerator-runtime


// babel.config.js
presets: [
  ["@babel/preset-env", {
    useBuiltIns: "entry", // or "usage"
    corejs: 3,
  }]
]


// 入口文件index.js
// before
import "@babel/polyfill";

// after
import "core-js/stable";
import "regenerator-runtime/runtime";

@babel/runtime

当使用core-js@3的时候,@babel/transform-runtime会从core-js-pure这个包里去加载对应的polyfill代码,core-js-pure里面的代码不会污染全局变量,适合第三方库的开发。

在@babel/transform-runtime的最新版本中,已经支持core-js@3,需作如下操作。

yarn remove @babel/runtime-corejs2
yarn add @babel/runtime-corejs3

//babel.config.js
plugins: [
  ["@babel/transform-runtime", {
    corejs: 3,
  }]
]

改变一

在之前的版本中,@babel/runtime最大的问题就是无法模拟实例上的方法,比如数组的includes方法就无法被polyfill。

但是在core-js@3的版本中,所有的实例方法都可以被polyfill了。

array.includes(something)

↓ ↓ ↓ ↓ ↓ ↓

import _includesInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/includes";
_includesInstanceProperty(array).call(array, something);

改变二

core-js@3支持对ECMAScript提案的API进行模拟。

@babel/plugin-transform-runtime的默认配置中,是不会注入对提案的polyfill代码。如果想要支持提案中的API,只需要增加和@babel/preset-env类似的配置项。

plugins: [
  ["@babel/transform-runtime", {
    corejs: { version: 3, proposals: true },
  }]
]
new Set([1, 2, 3, 2, 1]);
string.matchAll(/something/g);

↓ ↓ ↓ ↓ ↓ ↓

// without proposals flag
import _Set from "@babel/runtime-corejs3/core-js-stable/set";
new _Set([1, 2, 3, 2, 1]);
string.matchAll(/something/g);


// with proposals: true
import _Set from "@babel/runtime-corejs3/core-js/set";
import _matchAllInstanceProperty from "@babel/runtime-corejs3/core-js/instance/match-all";
new _Set([1, 2, 3, 2, 1]);
_matchAllInstanceProperty(string).call(string, /something/g);

展望未来

对老版本浏览器的支持

core-js支持尽量多的浏览器和平台,甚至是IE8-和一些老版本的Firefox浏览器。但是支持如此多的低版本浏览器,必然会造成polyfill文件变大,增大包的体积。

最大的问题主要来自于一些只支持ES3的浏览器,比如IE8-。大多数ES的新特性,都是基于ES5的语法去实现的,这就导致为了使低版本的浏览器能够支持这些新的特性,需要用大量的填充代码去抹平ES5与ES3的差异。

虽然在某些地区IE8还是非常流行,但是为了语言的发展和进步,应该允许某些浏览器退出历史的舞台。core-js@3已经放弃支持IE6,在下个大版本中,core-js@将不再支持IE8,只支持那些基于ES5语法的浏览器。

ECMAScript 模块

core-js的模块都是基于CommonJS规范的。随着ECMAScript模块的发布和普及,core-js应该提供一个ECMAScript模块规范的版本以供选择。

更好的优化polyfill的加载问题

在使用@babel/preset-env的useBuiltIns:usage这个配置项是,还是会存在一些问题。比如当项目的文件无法进行静态分析时,需要提供一种方案来进行polyfill的加载。另一个问题是useBuiltIns:usage可能会在一个文件头注入数十个core-js的导入语句。当项目中有几百上千个文件的时候,这些注入的语法会占据数量客观的体积。我们需要一个机制来收集所有需要用到的模块,并进行去重操作,最后统一注入到项目里。

对于那些需要支持低版本浏览器的开发人员来说,为了支持IE11这种浏览器,polyfill文件的大小会急剧膨胀。一种解决方案是使用type = module / nomodules属性,生成两个不同的包,一个用来支持现代浏览器,一个用来支持低版本的浏览器,但这并不是一个完美的解决方案;另一种解决方式是提供一个polyfill的服务,根据请求中的UA来判断浏览器的型号,返回这个浏览器需要的polyfill文件,类似的服务有polyfill.io。但是polyfill.io的返回并不准确,可用性不是很高。

others

  • 增加对web标准的支持,比如fetch
  • @babel/runtime提供对目标环境的支持,类似@babel/preset-env中targets字段

链接

core-js

core-js@3, babel and a look into the future

@babel/prest-env 7.4.0 Released: core-js 3, static private methods and partial application