原文链接
1. Modern mode 是什么?
使用 Babel 我们能够利用 ES2015 中最新的语言特性,但这也意味着我们必须通过转换和 添加 polyfille 来支持旧浏览器。这些转换后的代码通常比原生 ES2015+ 代码更冗长,并且解析和运行较慢。鉴于当今大多数现代浏览器对原生 ES2015+ 有着不错的支持,而我们不得不将数据量更大和效率底下的代码发送给浏览器,因为我们必须支持那些旧的浏览器。
Vue CLI 提供了一个 Modern mode 来帮助您解决这个问题。用以下命令进行生产时:
vue-cli-service build --modern
Vue CLI 构建两个版本的 js 包:一个面向支持现代浏览器的原生 ES2015+ 包,以及一个针对其他旧浏览器的包。
但最酷的部分是没有特殊的部署要求。生成的 HTML 文件中自动适配。 这个方式采用了Phillip Walton 文章中讨论的技术方案:
-
在支持原生 ES2015+ 的浏览器中,js 会通过 <script type="module"> 加载,并且可以使用 <link rel="modulepreload"> 预加载。
-
在不支持的浏览器中使用 <script nomodule> 来加载编译版本,并且这会被支持 ES 模块的浏览器所忽略。
-
Safari 10 中有一个小问题这里已经解决,可以自动加载。
对比 Hello World 应用(vue 初始化的 Demo)使用这种模式打包出来的文件,通过现代模式输出的包(以后简称现代包)已经小了 16%。在生产中,现代包通常会显著的提升 parse 速度和加载性能。
2. Modern mode 实现方式
在浏览器环境语法特性检测还没有一个特别好的解决方案,随着一些新的 JavaScript 语法的出现,单凭特性检测来检查新语法的支持程度很是棘手。尽管如此对于 ES2015+ 的基本语法特性检测我们还是有办法的。解决之道便是 <script type="module">。
大部分开发者认为 <script type="module"> 是用来加载 ES 模块的,但是这里使用是 <script type="module"> 的特性——加载浏览器可以处理的、使用 ES2015+ 语法的 JavaScript 文件。
换句话说,每个支持 <script type="module"> 的浏览器都支持你所熟知的大部分 ES2015+ 语法,例如:
-
支持 <script type="module"> 的浏览器也支持 async 和 await 函数。
-
支持 <script type="module"> 的浏览器也支持 Class 类。
-
支持 <script type="module"> 的浏览器也支持 arrow functions。
-
支持 <script type="module"> 的浏览器也支持 fetch 、Promises、Map、Set 等更多 ES2015+ 语法。
因此,唯一需要做的就是为不支持 <script type="module"> 的浏览器提供一个降级方案。对于支持 <script type="module"> 的浏览器会忽略 <script nomodule></script> 方式引入的脚本,如下代码:
// 支持的浏览器 会加载 app.js, 不支持的浏览器因为 type 值不是 text/javascript
所以脚本并不会被加载。
<script type="module" src="app.js"></script>
// 支持的浏览器 会忽略配置 `nomodule` 属性的脚本加载,不支持的浏览器会正常加载。
<script src="app-legacy.js" nomodule></script>
下面看一下 vue 打包出来的代码:
/ ...
<link as="style" href="/css/app.6166f93b.css" rel="preload" />
<link as="script" href="/js/app.4e3e948a.js" rel="modulepreload" />
<link as="script" href="/js/chunk-vendors.fcf87964.js" rel="modulepreload" />
<link href="/css/app.6166f93b.css" rel="stylesheet" />
// ...
<script type="module" src="/js/chunk-vendors.fcf87964.js"></script>
<script type="module" src="/js/app.4e3e948a.js"></script>
<script>
!(function () {
var e = document,
t = e.createElement("script");
if (!("noModule" in t) && "onbeforeload" in t) {
var n = !1;
e.addEventListener(
"beforeload",
function (e) {
if (e.target === t) n = !0;
else if (!e.target.hasAttribute("nomodule") || !n) return;
e.preventDefault();
},
!0
),
(t.type = "module"),
(t.src = "."),
e.head.appendChild(t),
t.remove();
}
})();
</script>
<script src="/js/chunk-vendors-legacy.ea74b83d.js" nomodule></script>
<script src="/js/app-legacy.854b5bc1.js" nomodule></script>
之前说到现代浏览器中都可以通过 <script type="module"> 来实现 ES2015+ 的特性检测针对性的加载脚本,但是 Safari 10 除外,这里的一段脚本是修复 safari 10 上 nomdoule 的表现不同的:
<script>
!(function () {
var e = document,
t = e.createElement("script");
if (!("noModule" in t) && "onbeforeload" in t) {
var n = !1;
e.addEventListener(
"beforeload",
function (e) {
if (e.target === t) n = !0;
else if (!e.target.hasAttribute("nomodule") || !n) return;
e.preventDefault();
},
!0
),
(t.type = "module"),
(t.src = "."),
e.head.appendChild(t),
t.remove();
}
})();
</script>
3. webpack 相关配置
如果你对 vue-cli 3 是如何实现这块的感兴趣可以查看源码。
为了生成不同环境的 js 文件,你需要 2 个 babel-loader targets 配置。
// resolve targets
let targets;
if (process.env.VUE_CLI_BABEL_TARGET_NODE) {
// running tests in Node.js
targets = { node: "current" };
} else if (
process.env.VUE_CLI_BUILD_TARGET === "wc" ||
process.env.VUE_CLI_BUILD_TARGET === "wc-async"
) {
// targeting browsers that at least support ES2015 classes
// https://github.com/babel/babel/blob/master/packages/babel-preset-env/data/plugins.json#L52-L61
targets = {
browsers: [
"Chrome >= 49",
"Firefox >= 45",
"Safari >= 10",
"Edge >= 13",
"iOS >= 10",
"Electron >= 0.36",
],
};
} else if (process.env.VUE_CLI_MODERN_BUILD) {
// targeting browsers that support <script type="module">
targets = { esmodules: true };
} else {
targets = rawTargets;
}
ES2015+ 浏览器支持目标只需配置 babel-loader 参数 targets = { esmodules: true } 即可。
当然,如果你使用 vue-cli 脚手架构架项目,它已经默认内置了该配置项。
4. preload
preload 作为一个新的 web 标准,旨在提高性能,为 web 开发人员提供更细粒度的加载控制。preload 使开发者能够自定义资源的加载逻辑,且无需忍受基于脚本的资源加载器带来的性能损失。
preload 还有许多其他好处。使用 as 来指定将要预加载的内容的类型,将使得浏览器能够:
-
更精确地优化资源加载优先级。
-
匹配未来的加载需求,在适当的情况下,重复利用同一资源。
-
为资源应用正确的内容安全策略。
-
为资源设置正确的 Accept 请求头。
在 modulepreload 诞生前,还没有一种很好的声明式预加载模块的方法。Chrome 从 64 版本后 开始 “实验性的支持这个特征”。<link rel="modulepreload"> 是 <link rel="preload"> 的特定模块版本,解决了后者的一些问题。
总结:
启用该模式会自动构建两个版本的 js 包,针对支持现代浏览器的原生 ES2015+ 包,和针对其他旧浏览器的包,生成的 HTML 会通过 <script type="module"> 和 <script nomodule> 进行自动降级,不需要任何特殊部署配置。原生 ES2015 包几乎不需要任何 polyfill 和编译,代码尺寸更小,现代浏览器 parse 和运行也更快。
ECMAScript modules in browsers
Deploying ES2015+ Code in Production Today