最近特别关注 vue-cli 3 的更新情况, 有很多特别棒的新功能和特性, 比如基于 UI 界面的项目管理器(参数配置, 数据查看, 插件安装一体的界面工具), 可配置的输出构建类型(App, 库, 组件, 异步组件), 构建模式 Modern mode 等等. 下面我们重点关注下 Modern mode 是什么, 如何实现的.
目录
Modern mode 是什么?
Modern mode 实现方式
Modern mode 是什么?
使用 Babel 我们能够利用 ES2015 中最新的语言特性, 但这也意味着我们必须通过转换和 添加 polyfille 来支持旧浏览器. 这些转换后的代码通常比原生 ES2015+ 代码更冗长, 并且解析和运行较慢. 鉴于当今大多数现代浏览器对原生 ES2015+ 有着不错的支持, 而我们不得不将数据量更大和效率底下的代码发送给浏览器, 因为我们必须支持那些旧的浏览器.
Vue CLI 提供了一个 Modern mode 来帮助您解决这个问题. 用以下命令进行生产时:
vue-cli-service build --modern
Vue CLI 构建两个版本的 js 包: 一个面向支持现代浏览器的原生 ES2015+ 包, 以及一个针对其他旧浏览器的包.
但最酷的部分是没有特殊的部署要求. 生成的 html 文件中自动适配. 这个方式采用了 Phillip Walton 文章中讨论的技术方案 https://philipwalton.com/articles/deploying-es2015-code-in-production-today/ :
在支持原生 ES2015+ 的浏览器中, js 会通过
<script type="module">
加载, 并且可以使用
<link rel="modulepreload">
预加载.
在不支持的浏览器中使用 <script nomodule> 来加载编译版本, 并且这会被支持 ES 模块的浏览器所忽略.
Safari 10 中有一个小问题这里已经解决, 可以自动加载.
对比 Hello World 应用 (vue 初始化的 Demo) 使用这种模式打包出来的文件, 通过现代模式输出的包 (以后简称现代包) 已经小了 16%. 在生产中, 现代包通常会显著的提升 parse 速度和加载性能.
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>
webpack 相关配置
如果你对 vue-cli 3 是如何实现这块的感兴趣可以查看源码 https://github.com/vuejs/vue-cli/commit/204d8f07deb5362804bbffc672d206072ab364f5 .
为了生成不同环境的 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 }
即可.
示例代码块地址 https://github.com/vuejs/vue-cli/blob/3b2cc6bd305263f17a5d1d52d4bb03bf9025e9c3/packages/@vue/babel-preset-app/index.js#L71
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 和运行也更快.
拓展阅读:
ES6 Modules in Chrome M61+ https://medium.com/dev-channel/es6-modules-in-chrome-canary-m60-ba588dfb8ab7
ECMAScript modules in browsers https://jakearchibald.com/2017/es-modules-in-browsers/
- ES6 Modules in Depth https://ponyfoo.com/articles/es6-modules-in-depth
- Deploying ES2015+ Code in Production Today https://philipwalton.com/articles/deploying-es2015-code-in-production-today/
- Preloading modules https://developers.google.com/web/updates/2017/12/modulepreload
来源: https://juejin.im/entry/5b38d1836fb9a00e632548f2