前端代码打包之后的生成的静态资源就要发布到静态服务器上,这时候就要做对这些静态资源做一些运维配置,其中,gzip和设置缓存是必不可少的。这两项是最直接影响到网站性能和用户体验的
gzip
服务器对文件进行gzip 压缩后,再进行传输,浏览器收到资源后再解压的过程。
优缺点
- 对于 js、text、json、css 这种纯文本进行压缩,效果特别好,不用改变代码即可提升网站响应速度;
- 压缩过程是需要花费 CPU 资源的,对大文件(图片、音乐等)进行压缩,不仅不能提升网站响应速度,还会增加服务器压力,让网站有明显的卡顿感。
使用
安装依赖:npm i --save-dev compression-webpack-plugin@5.0.1
在vue.config.js顶部引入依赖
const CompressionWebpackPlugin=require('compression-webpack-plugin')
在vue.config.js module.exports configureWebpack里面新增,直接放在代码压缩下边即可
// 代码压缩
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
// gzip压缩
const CompressionWebpackPlugin = require('compression-webpack-plugin')
// 是否为生产环境
const isProduction = process.env.NODE_ENV !== 'development'
// 本地环境是否需要使用cdn
const devNeedCdn = true
// cdn链接
const cdn = {
// cdn:模块名称和模块作用域命名(对应window里面挂载的变量名称)
externals: {
vue: 'Vue',
vuex: 'Vuex',
'vue-router': 'VueRouter'
},
// cdn的css链接
css: [],
// cdn的js链接
js: [
'https://unpkg.com/vue@next',
'https://cdn.staticfile.org/vuex/3.1.0/vuex.min.js',
'https://cdn.staticfile.org/vue-router/3.0.3/vue-router.min.js'
]
}
module.exports = {
productionSourceMap: false,
chainWebpack: config => {
// ============压缩图片 start============
config.module
.rule('images')
.use('image-webpack-loader')
.loader('image-webpack-loader')
.options({ bypassOnDebug: true })
.end()
// ============压缩图片 end============
// ============注入cdn start============
config.plugin('html').tap(args => {
// 生产环境或本地需要cdn时,才注入cdn
if (isProduction || devNeedCdn) args[0].cdn = cdn
return args
})
// ============注入cdn start============
},
configureWebpack: config => {
// 用cdn方式引入,则构建时要忽略相关资源
if (isProduction || devNeedCdn) config.externals = cdn.externals
// 生产环境相关配置
if (isProduction) {
// 代码压缩
config.plugins.push(
new UglifyJsPlugin({
uglifyOptions: {
//生产环境自动删除console
compress: {
warnings: false, // 若打包错误,则注释这行
drop_debugger: true,
drop_console: true,
pure_funcs: ['console.log']
}
},
sourceMap: false,
parallel: true
})
)
// gzip压缩
const productionGzipExtensions = ['html', 'js', 'css']
config.plugins.push(
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'.(' + productionGzipExtensions.join('|') + ')$'
),
threshold: 10240, // 只有大小大于该值的资源会被处理 10240
minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
deleteOriginalAssets: false // 删除原文件
})
)
// 公共代码抽离
config.optimization = {
splitChunks: {
cacheGroups: {
vendor: {
chunks: 'all',
test: /node_modules/,
name: 'vendor',
minChunks: 1,
maxInitialRequests: 5,
minSize: 0,
priority: 100
},
common: {
chunks: 'all',
test: /[/]src[/]js[/]/,
name: 'common',
minChunks: 2,
maxInitialRequests: 5,
minSize: 0,
priority: 60
},
styles: {
name: 'styles',
test: /.(sa|sc|c)ss$/,
chunks: 'all',
enforce: true
},
runtimeChunk: {
name: 'manifest'
}
}
}
}
}
}
}
配置nginx
server{
listen 8080
server_name localhost
gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary on;
gzip_disable "MSIE [1-6].";
location /appShare {
client_max_body_size 10m;
root /home/test/webIndex/appShare;
try_filtes $uri $uri/ /yourProduct/index.html;
index index.htm index.html;
}
}
查看是否已压缩
打开控制台,响应头中有 Content-Encoding: gzip ,表示已经开启
缓存
优缺点
- 减少了不必要的数据传输,节省带宽
- 减少服务器的负担,提升网站性能
- 加快了客户端加载网页的速度
- 用户体验友好
- 资源如果有更改但是客户端不及时更新会造成用户获取信息滞后,如果老版本有bug的话,情况会更加糟糕
强缓存
简单粗暴,如果资源没过期,就取缓存,如果过期了,则请求服务器。 当设置了强缓存,且没有过期时,就会从本地读取数据(内存中或硬盘中)
在chrome浏览器中的控制台Network中size栏通常会有三种状态
- from memory cache: 资源在内存当中
- from disk cache:资源在硬盘中(一般是样式)
- 资源本身的大小(如:1.5k)
强缓存又分为Expires 和 Cache-Control, Expires几乎已经不用,主要讲解Cache-Control
Cache-Control
有以下几个属性
- private: 仅浏览器可以缓存
- public: 浏览器和代理服务器都可以缓存(对于private和public,前端可以认为一样,不用深究)
- max-age=xxx 过期时间(重要)
- no-cache 不进行强缓存(重要)
- no-store 不强缓存,也不协商缓存,基本不用,缓存越多才越好呢
协商缓存
协商缓存利用Last-Modified , If-Modified-Since 和 ETag , If-None-Match来实现
触发条件:
- Cache-Control 的值为 no-cache (不强缓存)
- 或者 max-age 过期了 (强缓存,但总有过期的时候)
ETag和 Last-Modified 是在每个请求的响应头部中(response headers)
- ETag:每个文件有一个,改动文件了就变了,可以看似md5
- Last-Modified:文件的修改时间
在下次请求时
- 如果名字变了,就在请求头(request headers)携带If-None-Match,
- 修改时间变了,,就在请求头(request headers)携带携带If-modified-since
疑问?
你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?
- 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
- 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
- 某些服务器不能精确的得到文件的最后修改时间。
vue项目如何做缓存
index.html文件采用协商缓存,理由就是要用户每次请求index.html不拿浏览器缓存,直接请求服务器,这样就保证资源更新了,用户能马上访问到新资源,如果服务端返回304,这时候再拿浏览器的缓存的index.html
hash、chunkhash和contenthash三者的区别
- hash
如果都使用hash的话,所有文件的hash都是一样的,而且每次修改任何一个文件,所有文件名的hash至都将改变。
所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。
output:{
path:path.resolve(__dirname,'./dist'),
publicPath: '/dist/',
filename: '[name]-[hash].js'
}
2. chunkhash
使只有被修改了的文件的文件名hash值修改
output:{
path:path.resolve(__dirname,'./dist'),
publicPath: '/dist/',
filename: '[name]-[chunkhash].js'
}
如果我一个js文件里面引入了css文件。这时要是我修改了js,但没修改css,
能否让css能够继续利用缓存呢?答案是可以!
首先,我们使用Extract-text-webpack-plugin插件将css文件从js中分离出来。
{
test: /.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: {
loader:"css-loader",
options:{
minimize: true //css压缩
}
}
})
},
然后设置css的plugin
new ExtractTextPlugin({
filename: 'css/[name]-[chunkhash].css',
}),
3. contenthash
对css使用了chunkhash之后,我们测试会发现,如果修改了js直接,css文件名的hash值确实没变,
但这时要是我们修改css文件的话,我们就会发现css文件名的hash值居然没变化,
这样就导致我们的非覆盖发布css文件失效了。所以这里需要注意就是css文件必须使用contenthash。
将上面的css插件配置改为如下:
new ExtractTextPlugin({
filename: 'css/[name]-[contenthash].css',
}),
懒加载
图片懒加载
图片的加载是由src属性引发的,当对src属性赋值时,浏览器就会发送一个http请求图片资源。根据这个原理,我们可以使用HTML5的data-src属性来存储图片的路径,在需要加载图片的时候,将data-src中图片的路径赋值给src,这样就实现了图片的按需加载,即懒加载。
当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可, 以下使用IntersectionObserver来实现
IntersectionObserver两个参数
- callback是当被监听元素的可见性变化时,触发的回调函数
- options是一个配置参数,可选,有默认的属性值
<div class="container">
<img style="height:1080px;" data-src="./png1.png" />
<img style="height:1080px;" data-src="./png2.png" />
<img style="height:1080px;" data-src="./png3.png" />
<img style="height:1080px;" data-src="./png4.png" />
<img style="height:1080px;" data-src="./png5.png" />
<img style="height:1080px;" data-src="./png6.png" />
</div>
<script>
const imgs = document.querySelectorAll("img");
const observe = new IntersectionObserver((observes) => {
observes.forEach((ele) => {
//如果isIntersecting为true,则说明元素进入可视化区域
if (ele.isIntersecting) {
//do something...
ele.target.src = ele.target.dataset.src;
observe.unobserve(ele.target); //取消监听
}
});
});
//创建observe后需要给定一个目标元素进行观察:
imgs.forEach((el) => {
observe.observe(el);
});
</script>
路由懒加载
其实就是将引用方式写成函数调用的形式
import Vue from 'vue'
import Router from 'vue-router'
// import HelloWorld from '@/components/HelloWorld'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: ()=>import("@/components/HelloWorld")
// component:HelloWorld
}
]
})
回流和重绘
回流:当元素尺寸、结构或者属性发生变化时,浏览器会重新元素的过程
重绘:元素的样式发生变化,同时不会影响其在文档流中的位置时
如何避免回流和重绘
- 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
- 避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后一次性添加到文档中
- 将元素先设置display:none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘
- 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写
浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。
防抖和节流
webpack优化
- loader优化:减少文件搜索范围
module.exports = {
module: {
rules: [
{
// js 文件才使用 babel
test: /.js$/,
loader: 'babel-loader',
// 只在 src 文件夹下查找
include: [resolve('src')],
// 不会去查找的路径
exclude: /node_modules/,
options: {
cacheDirectory: true, // 开启babel编译缓存,下次只需要编译更改过的代码文件即可
cacheCompression: false, // 缓存文件不要压缩,压缩需要时间
}
]
}
}
- appyPack: 开启多线程
受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,HappyPack 可以将 Loader 的同步执行转换为并行的
module: {
loaders: [
{
test: /.js$/,
include: [resolve('src')],
exclude: /node_modules/,
// id 后面的内容对应下面
loader: 'happypack/loader?id=happybabel'
}
]
},
plugins: [
new HappyPack({
id: 'happybabel',
loaders: ['babel-loader?cacheDirectory'],
// 开启 4 个线程
threads: 4
})
]
- DllPlugin: 将特定的类库提前打包然后引入,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案
// 单独配置在一个文件中
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 想统一打包的类库
vendor: ['vue']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
library: '[name]-[hash]'
},
plugins: [
new webpack.DllPlugin({
// name 必须和 output.library 一致
name: '[name]-[hash]',
// 该属性需要与 DllReferencePlugin 中一致
context: __dirname,
path: path.join(__dirname, 'dist', '[name]-manifest.json')
}),
]
}
plugins: [
new webpack.DllReferencePlugin({
manifest: require('./dist/vendor-manifest.json')
})
]
DllPlugin和DllReferencePlugin参考文档
- Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现