Vue项目部署的最佳实践,试一下才知道多厉害_web

作者:沉末_

前言

使用​​vue​​​、​​react​​​、​​angular​​等技术开发过程中,我们都会遇到以下问题:

  1. 首屏加载慢
  2. 每一次更新都需要清除浏览器缓存才能看到效果(经常被测试吐槽)

这两个问题可以从很多方面进行优化,今天我就从前端页面部署阶段来优化一下这两个问题。PS:以下内容都基于​​vue-cli3+​​。

前端页面文件缓存方案

从​​vue-cli3​​打包说起

路由使用按需加载后,打包生成的文件,每一个路由页面都对应一个​​js​​​和​​css​​​文件,入口​​main.js​​​及其依赖则打包成了​​app.js​​​和​​app.css​​​,公共依赖都放到了​​chunk-vendors.js​​。

​vue-cli3​​​打包后的​​dist/js​​文件夹:

Vue项目部署的最佳实践,试一下才知道多厉害_web_02

可以看到,打包生成的​​js/css/img​​​等文件的文件名都带有​​hash​​​值,当源文件内容改变时,重新打包后对应的文件​​hash​​​值也会改变。举个栗子,我们修改了​​about.vue​​​中​​js​​​的内容,重新打包时​​about.js​​​的​​hash​​​值会改变,以及依赖​​about.vue​​​的文件​​app.js​​​的​​hash​​​值也会改变,而其他没有修改的文件,打包后的​​hash​​值都不会改变。

我们知道,文件名带​​hash​​是为了消除缓存带来的影响的,但是所有文件都不缓存肯定不是一个很好的解决方案。

​vue-cli3​​​打包生成的文件名带​​hash​​值的作用

为了缓存的最优体验

我们先来简单回顾下​​http​​缓存的知识(参考MDN:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching_FAQ):

  1. ​HTTP1.0​​ 是通过​​Expires​​(文件过期时间)和​​Last-Modified​​(最近修改时间)来告诉浏览器进行缓存的,这两个字段都是 ​​UTC​​ 时间(绝对时间)。​​Expires​​ 过期控制不稳定,因为浏览器端可以随意修改本地时间,导致缓存使用不精准。而且 ​​Last-Modified​​ 过期时间只能精确到秒。
  2. ​HTTP1.1​​ 通过​​Cache-Contorl​​和 ​​Etag​​(版本号)进行缓存控制。浏览器先检查 ​​Cache-Control​​,如果有,则以 ​​Cache-Control​​ 为准,忽略 ​​Expires​​。如果没有 ​​Cache-Control​​,则以 ​​Expires​​ 为准。

​Cache-Control​​​ 除了可以设置 ​​max-age​​(相对过期时间,以秒为单位)以外,还可以设置如下几种常用值:

  • ​public​​,资源允许被中间服务器缓存。浏览器请求服务器时,如果缓存时间没到,中间服务器直接返回给浏览器内容,而不必请求源服务器。
  • ​private​​,资源不允许被中间代理服务器缓存。浏览器请求服务器时,中间服务器都要把浏览器的请求透传给服务器。
  • ​no-cache​​,不管本地副本是否过期,每次访问资源,浏览器都要向服务器询问,如果文件没变化,服务器只告诉浏览器继续使用缓存(304)。
  • ​no-store​​,浏览器和中间代理服务器都不能缓存资源。每次访问资源,浏览器都必须请求服务器,并且,服务器不去检查文件是否变化,而是直接返回完整的资源。
  • ​must-revalidate​​,本地副本过期前,可以使用本地副本;本地副本一旦过期,必须去源服务器进行有效性校验。
  • ​proxy-revalidate​​,要求代理服务器针对缓存资源向源服务器进行确认。
  • ​s-maxage​​:缓存服务器对资源缓存的最大时间。

现在99%的浏览器都是​​HTTP1.1​​​及以上版本,我们配置缓存就使用​​Cache-Contorl​​​和​​Etag​​配合就好了。

那么问题来了,检查文件是否最新不是用​​etag​​​吗,为什么文件名还需要有​​hash​​值?

(1)如果文件名不带​​hash​​​值,文件版本得用​​etag​​来标记,浏览器需要先去检查下是否过期,服务器则需要检查文件是否最新。

(2)而文件名带有​​hash​​值,可以直接将文件的过期时间设置为1年,浏览器就不用检查是否过期,直接使用。

原因是,如果页面源文件有修改,生成的​​js/css​​​的​​hash​​​值就会修改,对应的请求​​js/css​​​地址也会变化,​​htpp​​​地址改了,也就不用检查是否过期。没修改的文件的​​hash​​则不变,可以使用缓存文件。

所以利用文件名带​​hash​​来做缓存,即能保证,页面有修改浏览器能请求到最新的文件,又能节省服务器的请求(检查是否过期的请求)。

实现无感知发版

只有一台服务器的情况下,我们的页面文件需要更新,通常操作是:先删掉旧文件,然后上传新文件,这段时间系统将不可用,对用户有一定的影响。

仅更新前端页面的前提下,文件名带有​​hash​​​值还可以实现用户无感知发版:系统更新时,只需要将打包之后的文件除​​index.html​​​以外的文件(​​js/css/img​​​),全部上传到服务器网站目录,未修改文件(即重名文件)直接跳过,有修改的文件由于文件的​​hash​​​值不同会被上传,上传完毕我们再将​​index.html​​​覆盖掉旧版就行。这段时间用户已请求旧版本​​index.html​​​的无影响(不会出现文件404,因为新旧版本​​js/css​​​同时存在),而新访问用户则请求的是新版​​index.html​​,访问旧页面用户刷新也会请求新版文件,并且无缓存影响,即对用户使用0影响。一段时间之后,我们只需要按文件生成时间对比一下删除旧文件即可。PS:替换前端文件不需要重启服务器。

总结: 凡是文件名带有​​hash​​​值的的文件都可以设置为“永久缓存”(一年),其他不带​​hash​​​的文件使用​​etag​​​来设置缓存,由​​Nginx​​判断是否过期。

优化打包结果

页面部署的时候,有个问题,如何区分文件名是否带有​​hash​​​值呢?正则匹配显然不是很好的办法。其实办法很简单,打包生成的文件都带有​​hash​​​值,而​​public​​​目录里面的文件不会经过打包处理。所以只需要将​​public​​​目录里面的文件除了​​index.html​​​全部放到一个​​static​​目录(注意引入路径)

Vue项目部署的最佳实践,试一下才知道多厉害_web_03

那么打包后的文件目录就会变成这样:

Vue项目部署的最佳实践,试一下才知道多厉害_java_04

​static​​​目录里面的文件和​​index.html​​​的文件名是不带​​hash​​​值的,其他的文件都是带有​​hash​​值的

补充:打包后发现一些页面文件很小,只有几K

如下图所示,虽然是按需加载,但是感觉浪费服务器请求

Vue项目部署的最佳实践,试一下才知道多厉害_运维_05

这时,我们可以配置​​webpack​​​的特殊注释(需要 ​​Webpack​​​ > 2.4),将一些按需加载的路由打包到同一个​​js​​文件

const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

这里需要注意一下,虽然每个文件单独打包都是​​1k​​​,但是​​1k+1k​​​不等于​​2k​​​,也就是说,打包到一起的体积会比原来分开的大,4个​​1k​​​的文件打包到一起,体积大约是​​10k​​​,体积达到​​10k​​​,刚好就会触发​​gzip​​​压缩,压缩之后体积在​​4k​​左右,所以并没有什么影响。

服务器配置缓存

理论知识有了,现在我们来实际操作一下:文件名带​​hash​​​的(即​​css​​​、​​js​​​、​​font​​​和​​img​​​目录下的所有文件)设置一个月缓存,浏览器可以直接使用缓存不需要请求服务器。其他的文件(​​index.html​​​和​​static​​​目录下的文件)设置为​​no-cache​​,即每次都来服务器检查是否最新。

为什么缓存时间是一个月,刚才不是说设置一年?设置为一年,当然没有任何问题。不变的文件可以一直使用,有改动的文件,会重新请求,但是有该动的旧文件已经没有用了,由于过期时间是一年,所以不会被删的,一直占用用户的硬盘,系统更新越频繁,无用旧文件越多,占用的存储也越多,这样是不好的(用户看了想打人)。所以设置一个合理的时间比较好,一个月就挺好。

废话不说,以​​Nginx​​​服务器为例,配置如下(配置文件​​nginx.conf​​​的​​http​​模块):

server {
location = /index.html {
add_header Cache-Control no-cache;
}

location ~ /static/ {
add_header Cache-Control no-cache;
}

location ~ /(js/*|css/*|img/*|font/*) {
expires 30d;
add_header Cache-Control public;
}
}

效果如下图:当我们修改​​index.html​​​内容时,会重新请求,没有修改就会​​304​​​,文件名带​​hash​​的都是直接从本地缓存读取。

Vue项目部署的最佳实践,试一下才知道多厉害_运维_06

有两点需要注意的地方:

  1. 项目里面不要用​​service-worker​​,这会影响我们的缓存设置,浏览器会优先使用​​service-worker​​缓存。​​vue-cli4​​生成的模板自带​​service-worker​

Vue项目部署的最佳实践,试一下才知道多厉害_html_07Vue项目部署的最佳实践,试一下才知道多厉害_运维_08

  1. 调试的时候记得允许缓存

Vue项目部署的最佳实践,试一下才知道多厉害_java_09

前端文件设置​​gzip​​压缩

​webpack​​​配置生成​​gzip​​压缩的文件

​webpack​​​有一个文件压缩的插件,可以将大文件压缩成​​gzip​​​的格式。使用起来也非常简单,先安装:​​npm install --save-dev compression-webpack-plugin​​,

然后修改​​webpack​​​配置(​​vue.config.js​​):

const CompressionWebpackPlugin = require("compression-webpack-plugin");
// 可加入需要的其他文件类型,比如json
// 图片不要压缩,体积会比原来还大
const productionGzipExtensions = ["js", "css"];

module.exports = {
configureWebpack: config => {
if (process.env.NODE_ENV === "production"){
return {
plugins: [
new CompressionWebpackPlugin({
// filename: '[path].gz[query]',
algorithm: "gzip",
test: new RegExp("\\.(" + productionGzipExtensions.join("|") + ")$"),
threshold: 10240, //对超过10k的数据进行压缩
minRatio: 0.6 // 压缩比例,值为0 ~ 1
})
]
};
}
}
};

打包完的​​js/css​​​文件,都会多一份对应的​​gzip​​​文件,部署的时候需要配置一下,启用​​gzip​​​,这样支持​​gzip​​​压缩的浏览器请求的就是压缩文件,不支持的浏览器请求的就是源文件,​​gzip​​压缩文件体积会小很多。

Vue项目部署的最佳实践,试一下才知道多厉害_运维_10

字体文件是否需要​​gzip​​?

网站中常见的图片的格式有​​jpg​​​(​​jpeg​​​)、​​png​​​、​​gif​​​、​​webp​​​,这些格式的图片本身已经优化了,所以不再需要​​gzip​​​。实际上对图片进行​​gzip​​压缩,不仅没有效果,反而可能使图片体积更大。那么字体文件呢,是不是和图片一样?

从阿里巴巴矢量图库生成的图标字体的​​css​​​中我们可以看出,一般常见的字体文件有:​​eot​​​、​​woff​​​、​​ttf​​​、​​svg​​​,另外​​woff2​​​是以​​base64​​的格式存储的。

@font-face {font-family: "iconfont";
src: url('iconfont.eot?t=1587624344896'), /* IE9 */
url('iconfont.woff?t=1587624344896') format('woff'),
url('data:application/x-font-woff2;charset=utf-8;base64,...') format('woff2'),
url('iconfont.ttf?t=1587624344896') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url('iconfont.svg?t=1587624344896#iconfont') format('svg'); /* iOS 4.1- */
}

查阅资料后发现:​​eot​​​ 和 ​​ttf​​​ 格式一般情况下本身不压缩,也就是说可以进行​​gzip​​​压缩。而​​woff​​​格式具有内建压缩,不需要​​gzip​​压缩。

实际测试一下,发现​​eot​​​和​​ttf​​​可以进行压缩,效果还不错,而​​woff​​​格式的,​​CompressionWebpackPlugin​​​插件根本不支持压缩,即使你写了配置了压缩​​woff​​​文件,它也不会生成​​gz​​文件。

Vue项目部署的最佳实践,试一下才知道多厉害_web_11

并且实验发现,​​svg​​​虽然是图片,但是也可以进行​​gzip​​压缩,压缩效果还不错:

Vue项目部署的最佳实践,试一下才知道多厉害_运维_12

结论:​​svg​​​、​​eot​​​ 和 ​​ttf​​​ 这三种格式的字体文件可以使用​​CompressionWebpackPlugin​​​进行压缩,并且配合​​Nginx​​​的​​gzip_types​​​配置,​​woff​​​和​​woff2​​​格式的字体文件不需要​​gzip​​。

服务器配置​​gzip​​压缩

​Nginx​​​是前端文件常用的服务器,​​Nginx​​​服务器的配置文件​​nginx.conf​​​的​​http​​模块:

server {
# 开启gzip on为开启,off为关闭
gzip on;
# 检查是否存在请求静态文件的gz结尾的文件,如果有则直接返回该gz文件内容,不存在则先压缩再返回
gzip_static on;
# 设置允许压缩的页面最小字节数,页面字节数从header头中的Content-Length中进行获取。
# 默认值是0,不管页面多大都压缩。
# 建议设置成大于10k的字节数,配合compression-webpack-plugin
gzip_min_length 10k;
# 对特定的MIME类型生效,其中'text/html’被系统强制启用
gzip_types text/javascript application/javascript text/css application/json;
# Nginx作为反向代理的时候启用,开启或者关闭后端服务器返回的结果
# 匹配的前提是后端服务器必须要返回包含"Via" header头
# off(关闭所有代理结果的数据的压缩)
# expired(启用压缩,如果header头中包括"Expires"头信息)
# no-cache(启用压缩,header头中包含"Cache-Control:no-cache")
# no-store(启用压缩,header头中包含"Cache-Control:no-store")
# private(启用压缩,header头中包含"Cache-Control:private")
# no_last_modefied(启用压缩,header头中不包含"Last-Modified")
# no_etag(启用压缩,如果header头中不包含"Etag"头信息)
# auth(启用压缩,如果header头中包含"Authorization"头信息)
# any - 无条件启用压缩
gzip_proxied any;
# 请求加个 vary头,给代理服务器用的,有的浏览器支持压缩,有的不支持,所以避免浪费不支持的也压缩
gzip_vary on;
# compression-webpack-plugin 插件一样,gzip压缩比(1~9),
# 越小压缩效果越差,但是越大处理越慢,一般取中间值
gzip_comp_level 6;
# 获取多少内存用于缓存压缩结果,‘16 8k’表示以8k*16 为单位获得。
# PS: 如果没有.gz文件,是需要Nginx实时压缩的
gzip_buffers 16 8k;
# 注:99.99%的浏览器基本上都支持gzip解压了,所以可以不用设这个值,保持系统默认即可。
gzip_http_version 1.1;
}

检查​​gzip​​是否生效

浏览器文件请求的请求头包含字段​​Accept-Encoding: gzip​​​代表浏览器支持​​gzip​​压缩文件

Vue项目部署的最佳实践,试一下才知道多厉害_java_13

文件响应头包含字段​​Content-Encoding: gzip​​代表返回的是压缩文件

Vue项目部署的最佳实践,试一下才知道多厉害_html_14

同时​​NetWork​​​一栏还可以查看到文件的实际大小和实际的请求(​​gzip​​)文件大小

Vue项目部署的最佳实践,试一下才知道多厉害_java_15

检查​​Nginx​​​是否使用了我们提供的​​gz​​文件

​Nginx​​​自带​​gzip​​​压缩功能,如果我们没提供,它会实时压缩(例如​​index.html​​​文件),这就很浪费服务器资源了。现在我们已经提供​​js​​​和​​css​​​的​​gz​​​文件,如何判断​​Nginx​​​是使用了我们提供的​​gz​​文件,而不是自己压缩的呢?

上面有一个配置项:​​gzip_static on;​​​,开启之后​​Nginx​​​会优先使用我们的​​gz​​​文件,但是还是不能确定,​​Nginx​​​有没有使用​​gz​​文件。

查看​​network​​​请求发现,每一个文件都有​​etag​​​响应头,如果​​Nginx​​​使用了已有的​​gz​​​文件,那么这个请求的​​etag​​​值不带有​​W/​​​,反之,如果是文件是​​Nginx​​​压缩的,​​etag​​​值则会带有​​W/​​。

例如​​index.html​​:

Vue项目部署的最佳实践,试一下才知道多厉害_nginx_16

拿​​chunk-vendors.js​​​做一个实验,这个文件本身是带有​​gz​​​文件的,请求的​​etag​​​如下(不带有​​W/​​):

Vue项目部署的最佳实践,试一下才知道多厉害_nginx_17

这时候我们删掉服务器上​​chunk-vendors.js​​​对应的​​gz​​文件,刷新页面,请求如下:

Vue项目部署的最佳实践,试一下才知道多厉害_nginx_18

综上,我们就可以验证,只要我们配置了​​gzip_static on;​​​,​​Nginx​​​就会优先使用了我们提供的​​gz​​文件。

附录 -​​windows​​​安装​​Nginx​​服务器

  1. 下载​​windows​​下​​Nginx​​的安装包:nginx.org/en/download…

Vue项目部署的最佳实践,试一下才知道多厉害_nginx_19

  1. 解压压缩包

Vue项目部署的最佳实践,试一下才知道多厉害_nginx_20

  1. 在​​Nginx​​的目录下使用​​cmd​​命令行,启动命令:​​start nginx​​,关闭命令:​​nginx -s stop​

备注:修改配置文件需要重载配置:​​nginx -s reload​​​。启动之后,打开​​http://localhost:80​​就能看的效果

总结

页面文件合理的设置缓存和​​gzip​​压缩是实实在在能提升用户体验的操作,而且比少写几个循环、删除几行代码优化强得多,但是需要前端和运维的密切配合,才能实现最佳方案。

​service worker​​​是用来实现离线应用的,文章中没有详细赘述。​​vue-cli4​​​生成的模板自带​​service worker​​​,或许这才是​​vue​​项目缓存的最佳实践?

最后,​​Nginx​​并不是很熟悉,有什么问题和错误,欢迎指出!