Vue 学习笔记(二十三):Vue CLI —— 浏览器兼容、HTML 和静态资源

Vue CLI:
浏览器兼容——暂时不用考虑,先做初步了解
HTML 和静态资源

浏览器兼容性

browserslist

package.json 文件里的 browserslist 字段,或一个单独的 .browserslistrc 文件,指定了项目的目标浏览器的范围。这个值会被 @babel/preset-env 和 Autoprefixer 用来确定需要转译的 JavaScript 特性和需要添加的 CSS 浏览器前缀。

Polyfill

什么是Polyfill?

Polyfill 是一块代码(通常是 Web 上的 JavaScript),用来为旧浏览器提供它没有原生支持的较新的功能。
比如说 polyfill 可以让 IE7 使用 Silverlight 插件来模拟 HTML Canvas 元素的功能,或模拟 CSS 实现 rem 单位的支持,或 text-shadow,或其他任何你想要的功能。

具体来说:
Polyfill 是一个js库,主要抚平不同浏览器之间对js实现的差异。比如,html5的storage(session,local), 不同浏览器,不同版本,有些支持,有些不支持。Polyfill,帮你把这些差异化抹平,不支持的变得支持了(典型做法是在IE浏览器中增加 window.XMLHttpRequest ,内部实现使用 ActiveXObject。)

提到Polyfill,不得不提shim, polyfillshim 的一种。
shim 是将不同 api封装成一种,比如 jQuery的 $.ajax 封装了 XMLHttpRequest和 IE用ActiveXObject方式创建xhr对象。它将一个新的API引入到一个旧的环境中,而且仅靠旧环境中已有的手段实现。

直接引入项目,最简单的就是CDN如下。但是更推荐的方式请继续往下看。

1
<script src="//cdn.polyfill.io/v1/polyfill.min.js" async defer></script>

参考概念发明者解读:https://remysharp.com/2010/10/08/what-is-a-polyfill
翻译: https://blog.csdn.net/C_ZhangSir/article/details/102490761

Polyfill源于什么?为什么要创造Polyfill这个术语?
这个词是在2009年,我编写《介绍HTML5》一书时创造的。当时我坐在一个咖啡店内,突然间想到,我需要一个能够很好地概括“使用JavaScript或Flash或其他的一些手段来支持一些浏览器不原生支持的API”这种行为的词汇。

Shim(垫片)于我而言,意味着一个可以帮助你修复一些功能的代码块,但是这种技术通常会拥有自己的API。我想要的是一种可以随时使用,而且对代码中的其他部分影响不大甚至没有影响的一种技术。记得老版本的shim.gif吗?它需要你真正地插入一张图片来修复空的td元素,我想要的是一种能够自动帮我做这些事情的技术。

我明白我所追求的是不是渐进式增强,因为我正在努力做的baseline需要使用JavaScript和最新技术。所以现有的术语无法满足我的要求。

我同样明白我所追求的并不是一种优雅的退化,因为离开了原生功能和原生的JavaScript(此处假设你的polyfill用的是JavaScript),polyfill也不会正常工作。

所以我想要的是一个简单易懂的词,仅凭字面意思就能大致了解这个词的真正含义。Polyfill这个词恰巧合适,满足了我的所有要求。Poly意味着可以使用多种技术解决这些问题,而并不仅仅局限于使用JavaScript,而fill指”fill”浏览器的漏洞。Polyfill一词同样不局限于”老版本的浏览器”,因为我们也需要”polyfill”新的浏览器。

对我来说,Polyfilla(美国的一种抹墙粉)是一种糨糊(原文为paste),可以被抹到墙上来覆盖墙上的裂缝和窟窿。polyfill一词就像是把糨糊抹到浏览器的窟窿里一样,polyfill给我一种可视化修复浏览器的感觉,我非常喜欢。一旦墙变平整了,便可以随心所欲地喷漆或者绘画———正如浏览器的bug都修复了,可以随心所欲地编写代码一样。

我曾得到一些反馈意见,让我”换个词”。但当时的社区非常需要这个词汇。就像我们需要Ajax、HTML5、Web2.0这些词汇一样,我们需要一些词汇来表达我们的想法。无论这些词是否完美,它们都被证明是有用的,开发人员和设计师们可以理解这些概念.

我从没有故意宣传我发明的术语,我只是在很少的几个关键的地方随口提了一下.然而在Polyfill一词被发明几个月后,保罗·艾里什在展示上直接引用了Polyfill这一术语,此时这个术语得到了大量的曝光.(同时Modernizr 的HTML5 shims和polyfill page在推广方面也起到了很大的作用.)

useBuiltIns: ‘usage’

默认的 Vue CLI 项目会使用 @vue/babel-preset-app,它通过 @babel/preset-envbrowserslist 配置来决定项目需要的 polyfill。

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

如果有依赖需要 polyfill,你有几种选择:

  1. 如果该依赖基于一个目标环境不支持的 ES 版本撰写: 将其添加到 vue.config.js 中的 transpileDependencies 选项。这会为该依赖同时开启语法转换和根据使用情况检测 polyfill。

  2. 如果该依赖交付了 ES5 代码并显式地列出了需要的 polyfill: 你可以使用 @vue/babel-preset-apppolyfills 选项预包含所需要的 polyfill。注意 es.promise 将被默认包含,因为现在的库依赖 Promise 是非常普遍的。

    回调函数真正的问题在于他剥夺了我们使用 return 和 throw 这些关键字的能力。而 Promise 很好地解决了这一切。Promise,就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理。ES6 原生提供了 Promise 对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // babel.config.js
    module.exports = {
    presets: [
    ['@vue/app', {
    polyfills: [
    'es.promise',
    'es.symbol'
    ]
    }]
    ]
    }

我们推荐以这种方式添加 polyfill 而不是在源代码中直接导入它们,因为如果这里列出的 polyfill 在 browserslist 的目标中不需要,则它会被自动排除。

  1. 如果该依赖交付 ES5 代码,但使用了 ES6+ 特性且没有显式地列出需要的 polyfill (例如 Vuetify):请使用 useBuiltIns: 'entry' 然后在入口文件添加 import 'core-js/stable'; import 'regenerator-runtime/runtime';。这会根据 browserslist 目标导入所有 polyfill,这样你就不用再担心依赖的 polyfill 问题了,但是因为包含了一些没有用到的 polyfill 所以最终的包大小可能会增加。

构建库或是 Web Component 时的 Polyfills

Polyfills when Building as Library or Web Components

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

现代模式 Modern Mode

Babel 让我们可以兼顾所有最新的 ES2015+ 语言特性,但也意味着需要交付转译和 polyfill 后的包以支持旧浏览器。这些转译后的包通常都比原生的 ES2015+ 代码更冗长,运行更慢。现如今绝大多数现代浏览器都已经支持了原生的 ES2015,所以因为要支持更老的浏览器而为它们交付笨重的代码是一种浪费。

Vue CLI 提供了一个“现代模式”帮你解决这个问题。为生产环境构建:

1
vue-cli-service build --modern

Vue CLI 会产生两个应用的版本:一个现代版的包,面向支持 ES modules 的现代浏览器,另一个旧版的包,面向不支持的旧浏览器。

其生成的 HTML 文件会自动使用 Phillip Walton 如何在生产环境中部署ES2015+中讨论到的技术:

  • 现代版的包会通过 <script type="module"> 在被支持的浏览器中加载;还会使用 <link rel="modulepreload"> 进行预加载。

  • 旧版的包会通过 <script nomodule> 加载,并会被支持 ES modules 的浏览器忽略。

  • 一个针对 Safari 10 中 <script nomodule> 的修复会被自动注入。

对于一个 Hello World 应用来说,现代版的包已经小了 16%。在生产环境下,现代版的包通常都会表现出显著的解析速度和运算速度,从而改善应用的加载性能。

<script type="module"> 需要配合始终开启的 CORS 进行加载。这意味着你的服务器必须返回诸如 Access-Control-Allow-Origin: * 的有效的 CORS 头。如果你想要通过认证来获取脚本,可使将 crossorigin 选项设置为 use-credentials

同时,现代浏览器使用一段内联脚本来避免 Safari 10 重复加载脚本包,所以如果你在使用一套严格的 CSP,你需要这样显性地允许内联脚本:

1
Content-Security-Policy: script-src 'self' 'sha256-4RS22DYeB7U14dra4KcQYxmwt5HkOInieXK1NUMBmQI='

HTML 和静态资源

HTML

Index 文件

public/index.html 文件是一个会被 html-webpack-plugin 处理的模板。在构建过程中,资源链接会被自动注入。另外,Vue CLI 也会自动注入 resource hint (preload/prefetch、manifest 和图标链接 (当用到 PWA 插件时) 以及构建过程中处理的 JavaScript 和 CSS 文件的资源链接。

插值

因为 index 文件被用作模板,所以可以使用 lodash template 语法插入内容:

  • <%= VALUE %> 用来做不转义插值;
  • <%- VALUE %> 用来做 HTML 转义插值;
  • <% expression %> 用来描述 JavaScript 流程控制。

除了html-webpack-plugin 暴露的默认值之外,所有客户端环境变量也可以直接使用。例如,BASE_URL 的用法:

1
<link rel="icon" href="<%= BASE_URL %>favicon.ico">

Preload

<link rel="preload"> 是一种 resource hint,用来指定页面加载后很快会被用到的资源,所以在页面加载的过程中,我们希望在浏览器开始主体渲染之前尽早 preload。

Resource Hint是一系列相关标准,来告诉浏览器哪些源(origin)下的资源我们的Web App想要获取,哪些资源在之后的操作或浏览时需要被使用,以便让浏览器能够进行一些预先连接或预先加载等操作。

默认情况下,Vue CLI 应用会为所有初始化渲染需要的文件自动生成 preload 提示。

这些提示会被 @vue/preload-webpack-plugin 注入,并且可以通过 chainWebpackconfig.plugin('preload') 进行修改和删除。

Prefetch

<link rel="prefetch"> 是一种 resource hint,用来告诉浏览器在页面加载完成后,利用空闲时间提前获取用户未来可能会访问的内容。

默认情况下,一个 Vue CLI 应用会为所有作为 async chunk 生成的 JavaScript 文件 (通过动态 import() 按需 code splitting 的产物) 自动生成 prefetch 提示。

这些提示会被 @vue/preload-webpack-plugin 注入,并且可以通过 chainWebpackconfig.plugin('prefetch') 进行修改和删除。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vue.config.js
module.exports = {
chainWebpack: config => {
// 移除 prefetch 插件
config.plugins.delete('prefetch')
// 或者
// 修改它的选项:
config.plugin('prefetch').tap(options => {
options[0].fileBlacklist = options[0].fileBlacklist || []
options[0].fileBlacklist.push(/myasyncRoute(.)+?\.js$/)
return options
})
}
}

当 prefetch 插件被禁用时,可以通过 webpack 的内联注释手动选定要提前获取的代码区块:

1
import(/* webpackPrefetch: true */ './someAsyncComponent.vue')

webpack 的运行时会在父级区块被加载之后注入 prefetch 链接。

Prefetch 链接将会消耗带宽。如果你的应用很大且有很多 async chunk,对于移动端,可能需要关掉 prefetch 链接并手动选择要提前获取的代码区块。

不生成 index

当基于已有的后端使用 Vue CLI 时,你可能不需要生成 index.html,这样生成的资源可以用于一个服务端渲染的页面。编辑 vue.config.js 增加:

1
2
3
4
5
6
7
8
9
10
11
// vue.config.js
module.exports = {
// 去掉文件名中的 hash
filenameHashing: false,
// 删除 HTML 相关的 webpack 插件
chainWebpack: config => {
config.plugins.delete('html')
config.plugins.delete('preload')
config.plugins.delete('prefetch')
}
}

然而这个做法不是很推荐:

  • 硬编码的文件名不利于实现高效率的缓存控制。
  • 硬编码的文件名也无法很好的进行 code-splitting (代码分段),因为无法用变化的文件名生成额外的 JavaScript 文件。
  • 硬编码的文件名无法在现代模式下工作。

应该考虑换用 indexPath 选项将生成的 HTML 用作一个服务端框架的视图模板。

构建一个多页应用

Vue CLI 支持使用 vue.config.js 中的 pages 选项构建一个多页面的应用。构建好的应用将会在不同的入口之间高效共享通用的 chunk 以获得最佳的加载性能。

处理静态资源

通过两种方式:

  • 在 JavaScript 被导入或在 template/CSS 中通过相对路径被引用。这类引用会被 webpack 处理。

  • 放置在 public 目录下或通过绝对路径被引用。这类资源将会直接被拷贝,而不会经过 webpack 的处理。

从相对路径导入

当你在 JavaScript、CSS 或 *.vue 文件中使用相对路径 (必须以 . 开头) 引用一个静态资源时,该资源将会被包含进入 webpack 的依赖图中。在其编译过程中,所有诸如 <img src="...">background: url(...) 和 CSS @import 的资源 URL 都会被解析为一个模块依赖

例如,url(./image.png) 会被翻译为 require('./image.png'),而:

1
<img src="./image.png">

将会被编译到:

1
h('img', { attrs: { src: require('./image.png') }})

在其内部,我们通过 file-loader 用版本哈希值和正确的公共基础路径来决定最终的文件路径,再用 url-loader 将小于 4kb 的资源内联,以减少 HTTP 请求的数量。

你可以通过 chainWebpack 调整内联文件的大小限制。例如,限制设置为 10kb:

1
2
3
4
5
6
7
8
9
10
// vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('images')
.use('url-loader')
.loader('url-loader')
.tap(options => Object.assign(options, { limit: 10240 }))
}
}

URL 转换规则

  • 绝对路径 (例 /images/foo.png),将会被保留不变。

  • . 开头,会作为一个相对模块请求被解释且基于你的文件系统中的目录结构进行解析。

  • URL 以 ~ 开头,其后的任何内容都会作为一个模块请求被解析。这意味着你甚至可以引用 Node 模块中的资源:

    1
    <img src="~some-npm-package/foo.png">
  • 如果 URL 以 @ 开头,它也会作为一个模块请求被解析。它的用处在于 Vue CLI 默认会设置一个指向 <projectRoot>/src 的别名 @。(仅作用于模版中)

public 文件夹

任何放置在 public 文件夹的静态资源都会被简单的复制,而不经过 webpack。需要通过绝对路径来引用它们。

推荐将资源作为模块依赖图的一部分导入,这样它们会通过 webpack 的处理并获得如下好处:

  • 脚本和样式表会被压缩且打包在一起,从而避免额外的网络请求。
  • 文件丢失会直接在编译时报错,而不是到了用户端才产生 404 错误。
  • 最终生成的文件名包含了内容哈希,因此你不必担心浏览器会缓存它们的老版本。

public 目录提供的是一个应急手段,当你通过绝对路径引用它时,留意应用将会部署到哪里。如果你的应用没有部署在域名的根部,那么你需要为你的 URL 配置 publicPath 前缀:

  • public/index.html 或其它通过 html-webpack-plugin 用作模板的 HTML 文件中,需要通过 <%= BASE_URL %> 设置链接前缀:

    1
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  • 在模板中,首先需要向你的组件传入基础 URL:

    1
    2
    3
    4
    5
    data () {
    return {
    publicPath: process.env.BASE_URL
    }
    }

    然后:

    1
    <img :src="`${publicPath}my-image.png`">

何时使用 public 文件夹

  • 需要在构建输出中指定一个文件的名字。
  • 有上千个图片,需要动态引用它们的路径。
  • 有些库可能和 webpack 不兼容,这时你除了将其用一个独立的 <script> 标签引入没有别的选择。