ziyi2 / ziyi2.github.io

www.ziyi2.cn
53 stars 5 forks source link

VUE CLI3构建库时对于产生polyfill的问题分析 #1

Open ziyi2 opened 5 years ago

ziyi2 commented 5 years ago

VUE CLI3构建库时对于产生polyfill的问题分析

发现问题

在使用VUE CLI3/开发/构建目标/库功能时,为了防止构建库引入一些额外的依赖(例如vuelodash),使用了Webpack的exterenalwebpack-node-externals从输出的 bundle 中排除依赖,但是打包出来的文件同时排除了一些polyfill,例如core-js/modules/es6.array.some等。那么VUE-CLI3的构件库为什么会带有这些polyfill?打包的库中带有这些polyfill该如何决策?

polyfill

In web development, a polyfill is code that implements a feature on web browsers that do not support the feature. Most often, it refers to a JavaScript library that implements an HTML5 web standard, either an established standard (supported by some browsers) on older browsers, or a proposed standard (not supported by any browsers) on existing browsers. Formally, "a polyfill is a shim for a browser API".

Polyfills allow web developers to use an API regardless of whether or not it is supported by a browser, and usually with minimal overhead. Typically they first check if a browser supports an API, and use it if available, otherwise using their own implementation. Polyfills themselves use other, more supported features, and thus different polyfills may be needed for different browsers. The term is also used as a verb: polyfilling is providing a polyfill for a feature.

在web开发中,polyfill是用于实现一些浏览器还没有支持的新特性的代码,通常它是实现HTML5 web标准的一个JavaScript库,polyfill是浏览器API的垫片。

Polyfills允许web开发人员用最小的开销使用一些浏览器的API,它们会先检测浏览器是否支持API,如果支持直接调用,否则就自己实现一个类似的API去做兼容。

现代浏览器还没有支持的一些标准API,可以用polyfill做兼容性处理。

core-js

Modular standard library for JavaScript. Includes polyfills for ECMAScript 5, ECMAScript 6: promises, symbols, collections, iterators, typed arrays, ECMAScript 7+ proposals, setImmediate, etc. Some additional features such as dictionaries or extended partial application. You can require only needed features or use it without global namespace pollution.

core-js 是用于 JavaScript 的组合式标准化库。它包含 ECMAScript 5, ECMAScript 6的 promises, symbols, collections, iterators, typed arrays, ECMAScript 7+的提案,setImmediate等等的 polyfills 实现,你可以按需引入你想要的特性,但是需要注意它没有实现generator

@babel/polyfill(老版本是babel-polyfill)

Babel includes a polyfill that includes a custom regenerator runtime and core-js.

This will emulate a full ES2015+ environment(no < Stage 4 proposals) and is intended to be used in an application rather than a library/tool. This polyfill is automatically loaded when using babel-node.

This means you can use new built-ins like Promise or WeakMap, static methods like Array.from or Object.assign, instance methods like Array.prototype.includes, and generator functions (provided you use the regenerator plugin). The polyfill adds to the global scope as well as native prototypes like String in order to do this.

@babel/polyfill不仅包含了core-js ,还包含了regenerator runtime。从core-js的介绍可以发现它没有实现generatorasync函数,因此需要额外的polyfill进行处理,Babel在这里引入了facebook的regenerator runtime,这个库主要功能就是compiled generator and async functions

有了以上两个polyfill,@babel/polyfill就可以模拟一个完整的ES2015+环境,它的定位不是用于library/tool,而是在应用程序中使用。需要注意当使用babel-node的时候,会自动加入@babel/polyfill,因此可以解释该命令用于代替node从而可以使node环境模拟完整的ES2015+环境。

以下是@babel/polyfill源码

// Cover all standardized ES6 APIs.
import "core-js/es6";

// Standard now
import "core-js/fn/array/includes";
import "core-js/fn/array/flat-map";
import "core-js/fn/string/pad-start";
import "core-js/fn/string/pad-end";
import "core-js/fn/string/trim-start";
import "core-js/fn/string/trim-end";
import "core-js/fn/symbol/async-iterator";
import "core-js/fn/object/get-own-property-descriptors";
import "core-js/fn/object/values";
import "core-js/fn/object/entries";
import "core-js/fn/promise/finally";

// Ensure that we polyfill ES6 compat for anything web-related, if it exists.
import "core-js/web";

import "regenerator-runtime/runtime";

if (global._babelPolyfill && typeof console !== "undefined" && console.warn) {
  console.warn(
    "@babel/polyfill is loaded more than once on this page. This is probably not desirable/intended " +
      "and may have consequences if different versions of the polyfills are applied sequentially. " +
      "If you do need to load the polyfill more than once, use @babel/polyfill/noConflict " +
      "instead to bypass the warning.",
  );
}

global._babelPolyfill = true;

The polyfill is provided as a convenience but you should use it with @babel/preset-env and the useBuiltIns option so that it doesn't include the whole polyfill which isn't always needed. Otherwise, we would recommend you import the individual polyfills manually.

@babel/polyfill相对于core-js按需引入,会大大增加应用体积。如果只是使用简单的几个特性可以按需引入core-js,如果是开发较大的应用,而且会频繁使用新特性并考虑兼容,那应该考虑使用@babel/polyfill。当然官方文档给出了另外一种按需引入的方案,使用babel插件集@babel/preset-env

@babel/preset-nev

@babel/preset-env is a smart preset that allows you to use the latest JavaScript without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s). This both makes your life easier and JavaScript bundles smaller!

@babel/preset-env智能插件集使用最新的Javascript特性并且不需要考虑目标环境需要哪些polyfills,它会使得JavaScript构建的体积最小化。

@babel/preset-env would not be possible if not for a number of awesome open-source projects, like browserslist, compat-table, and electron-to-chromium.

We leverage these data sources to maintain mappings of which version of our supported target environments gained support of a JavaScript syntax or browser feature, as well as a mapping of those syntaxes and features to Babel transform plugins and core-js polyfills.

@babel/preset-env利用了一些开源项目(browserslistcompat-tableelectron-to-chromium)的数据来维护目标环境(这里并不单单指浏览器)和babel转换插件以及core-js的polyfill之间的映射。

@babel/preset-env不支持提案系列babel插件(stage-x)。

由于利用了browserslist开源项目的数据源,因此@babel/preset-env集成了browserslist功能,可以使用.browserslistrc配置项目特定的目标环境。

如何在webpack中使用@babel/preset-env配置polyfill:

@babel/preset-env中重点关注useBuiltIns(This option configures how @babel/preset-env handles polyfills.):

其中试验性的选项usage 会智能识别项目中使用的polyfill,并且智能的加载这些polyfill,这可以减小项目构建的体积。举个栗子:

// 输入 a.js
var a = new Promise();
// 输入 b.js
var b = new Map();

// 输出 a.js 如果环境不支持
import "core-js/modules/es6.promise";
var a = new Promise();
// 输出 b.js 如果环境不支持
import "core-js/modules/es6.map";
var b = new Map();

// 输出 a.js 如果环境支持
var a = new Promise();
// 输出 b.js 如果环境支持
var b = new Map();

VUE CLI3中使用polyfill

VUE CLI3/开发/浏览器兼容性/Polyfill中提到了VUE CLI3对polyfill的处理。VUE CLI3封装了一个babel插件集@vue/babel-preset-app(集成了@babel/preset-envStage 3 or Below以及Vue JSX support),通过 @babel/preset-envbrowserslist 配置来决定项目需要的 polyfill。

默认情况下,@babel/preset-env会把 useBuiltIns: 'usage' 传递给 @babel/preset-env,这样它会根据源代码中出现的语言特性自动检测需要的 polyfill。这确保了最终包里 polyfill 数量的最小化。然而,这也意味着如果其中一个依赖需要特殊的 polyfill,默认情况下 Babel 无法将其检测出来

当使用 Vue CLI 来构建一个库或是 Web Component 时,推荐给 @vue/babel-preset-env 传入 useBuiltIns: false 选项。这能够确保库或是组件不包含不必要的 polyfills。通常来说,打包 polyfills 应当是最终使用你的库的应用的责任。

结论

由于默认@babel/preset-env配置了 useBuiltIns: 'usage' ,如果在库开发模式中引入了浏览器不兼容的API,那么使用VUE CLI3进行构建库时会产生特殊的Polyfill,这里有三种方式可以进行处理:

1、去除库源码中的Polyfill(推荐):去除源码中对于环境不兼容的API,采用兼容的API进行处理, 同时在@vue/babel-preset-env 传入 useBuiltIns: false,打包构建的体积确保最小化。

2、将core-js加入库的依赖列表:如果库已经变得非常大,去除Polyfill需要极大的代价,那么推荐使用Webpack的exterenalwebpack-node-externals从输出的 bundle 中排除依赖core-js,并将其加入库的依赖列表,这样可以防止二次打包。例如在业务层面如果需要做额外的Polyfill处理,那么业务层面和库可以引用同一份Polyfill,从而可以减少打包体积。但是正如VUE官方说明的那样,需要对库的应用进行负责,毕竟库中引入了core-js

3、将core-js打包加入库:如果库已经变得非常大,去除Polyfill需要极大的代价,不进行Webpack的排除依赖处理,直接在库中打包polyfills,这样会使得库体积增大,但是使用场景不会受到限制。

参考链接

ziyi2 commented 5 years ago

4

shalldie commented 4 years ago

有个地方没想明白,,,

为什么使用了 useBuiltIns: false 依然会有 polyfill 引入呢?这个是从哪里来的。

ziyi2 commented 4 years ago

有个地方没想明白,,,

为什么使用了 useBuiltIns: false 依然会有 polyfill 引入呢?这个是从哪里来的。

问题解决了么?

shalldie commented 4 years ago

@ziyi2 没有解决这个问题。 猜测可能是 vue-cli-service 做了什么操作,但是短时间内不打算继续深究了,,

在我的使用中,即使设置了 useBuiltIns: false ,对于新的api,依然会进行polyfill