一. 前言
本文是专栏【前端工程化】webpack5+vue3+ts+代码规范构建企业级前端项目系列第四篇
,会详细讲解优化构建结果
:构建结果分析,抽离css文件,压缩css,js文件,hash合理配置,代码分割,tree-shaking清理css和js,打包生成gzip等优化配置。
本系列文章将使用最新的webpack5一步一步从零搭建一个完整的vue3+ts开发和打包环境,配置完善构建速度和构建结果的优化配置,以及配置完善的代码规范和git提交规范。完整代码已上传到webpack5-vue3-ts。
全系列文章:
- 《第一篇:基础功能配置,webpack5配置vue3+ts基础环境,实现dev开发和打包构建》。
- 《第二篇:进阶功能配置,环境变量,支持css,less,图片和媒体资源,css3前缀,babel兼容》。
- 《第三篇:优化构建速度:构建耗时分析,持久化缓存,多线程loader,devtool,loader作用范围》。
- 《第四篇》优化化构建结果:构建结果分析,抽离css文件,压缩css,js文件,hash合理配置,代码分割,tree-shaking清理css和js,打包生成gzip》。
- 代码格式规范:editorconfig统一编辑器配置,prettier自动格式化代码,stylelint规范样式和保存自动修复,代码提交自动格式化css和js代码等。
- 代码语法规范:eslint检测js代码语法,style-lint检测样式代码语法,使用tsc检测类型报错,lint-staged按需检测代码等。
- git提交规范:代码提交时husky检测代码语法规范,代码提交时husky检测commit备注规范,commitizen配置commit辅助信息等。
全系列概览
二. 优化构建结果文件
2.1 webpack包分析工具
webpack-bundle-analyzer是分析webpack打包后文件的插件,使用交互式可缩放树形图可视化 webpack 输出文件的大小。通过该插件可以对打包后的文件进行观察和分析,可以方便我们对不完美的地方针对性的优化,安装依赖:
npm install webpack-bundle-analyzer -D
修改webpack.analy.js
// webpack.analy.js
const prodConfig = require('./webpack.prod.js')
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
const { merge } = require('webpack-merge')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') // 引入分析打包结果插件
module.exports = smp.wrap(merge(prodConfig, {
plugins: [
new BundleAnalyzerPlugin() // 配置分析打包结果插件
]
}))
配置好后,执行npm run build:analy命令,打包完成后浏览器会自动打开窗口,可以看到打包文件的分析结果页面,可以看到各个文件所占的资源大小。
2.2 抽取css样式文件
在开发环境我们希望css嵌入在style标签里面,方便样式热替换,但打包时我们希望把css单独抽离出来,方便配置缓存策略。而插件mini-css-extract-plugin就是来帮我们做这件事的,安装依赖:
npm i mini-css-extract-plugin -D
修改webpack.base.js, 根据环境变量设置开发环境使用style-looader,打包模式抽离css
// webpack.base.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const isDev = process.env.NODE_ENV === 'development' // 是否是开发模式
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /.css$/, //匹配所有的 css 文件
include: [path.resolve(__dirname, '../src')],
use: [
// 开发环境使用style-looader,打包模式抽离css
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
},
{
test: /.less$/, //匹配所有的 less 文件
include: [path.resolve(__dirname, '../src')],
use: [
// 开发环境使用style-looader,打包模式抽离css
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'less-loader'
]
},
]
},
// ...
}
再修改webpack.prod.js, 打包时添加抽离css插件
// webpack.prod.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = merge(baseConfig, {
mode: 'production',
plugins: [
// ...
// 抽离css插件
new MiniCssExtractPlugin({
filename: 'static/css/[name].css' // 抽离css的输出目录和名称
}),
]
})
配置完成后,在开发模式css会嵌入到style标签里面,方便样式热替换,打包时会把css抽离成单独的css文件。
2.3 压缩css文件
上面配置了打包时把css抽离为单独css文件的配置,打开打包后的文件查看,可以看到默认css是没有压缩的,需要手动配置一下压缩css的插件。
可以借助css-minimizer-webpack-plugin来压缩css,安装依赖
npm i css-minimizer-webpack-plugin -D
修改webpack.prod.js文件, 需要在优化项optimization下的minimizer属性中配置
// webpack.prod.js
// ...
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
// ...
optimization: {
minimizer: [
new CssMinimizerPlugin(), // 压缩css
],
},
}
再次执行打包就可以看到css已经被压缩了。
2.4 压缩js文件
设置mode为production时,webpack会使用内置插件terser-webpack-plugin压缩js文件,该插件默认支持多线程压缩,但是上面配置optimization.minimizer压缩css后,js压缩就失效了,需要手动再添加一下,webpack内部安装了该插件,由于pnpm解决了幽灵依赖问题,如果用的pnpm的话,需要手动再安装一下依赖。
npm i terser-webpack-plugin -D
修改webpack.prod.js文件
// ...
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
// ...
optimization: {
minimizer: [
// ...
new TerserPlugin({ // 压缩js
parallel: true, // 开启多线程压缩
terserOptions: {
compress: {
pure_funcs: ["console.log"] // 删除console.log
}
}
}),
],
},
}
配置完成后再打包,css和js就都可以被压缩了。
2.5 合理配置打包文件hash
项目维护的时候,一般只会修改一部分代码,可以合理配置文件缓存,来提升前端加载页面速度和减少服务器压力,而hash就是浏览器缓存策略很重要的一部分。webpack打包的hash分三种:
- hash:跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值
- chunkhash:不同的入口文件进行依赖文件解析、构建对应的chunk,生成对应的哈希值,文件本身修改或者依赖文件修改,chunkhash值会变化
- contenthash:每个文件自己单独的 hash 值,文件的改动只会影响自身的 hash 值
hash是在输出文件时配置的,格式是filename: "[name].[chunkhash:8][ext]",[xx] 格式是webpack提供的占位符, :8是生成hash的长度。
占位符 | 解释 |
---|---|
ext | 文件后缀名 |
name | 文件名 |
path | 文件相对路径 |
folder | 文件所在文件夹 |
hash | 每次构建生成的唯一 hash 值 |
chunkhash | 根据 chunk 生成 hash 值 |
contenthash | 根据文件内容生成hash 值 |
因为js我们在生产环境里会把一些公共库和程序入口文件区分开,单独打包构建,采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响,可以继续使用浏览器缓存,所以js适合使用chunkhash。
css和图片资源媒体资源一般都是单独存在的,可以采用contenthash,只有文件本身变化后会生成新hash值。
修改webpack.base.js,把js输出的文件名称格式加上chunkhash,把css和图片媒体资源输出格式加上contenthash
// webpack.base.js
// ...
module.exports = {
// 打包文件出口
output: {
filename: 'static/js/[name].[chunkhash:8].js', // // 加上[chunkhash:8]
// ...
},
module: {
rules: [
{
test:/.(png|jpg|jpeg|gif|svg)$/, // 匹配图片文件
// ...
generator:{
filename:'static/images/[name].[contenthash:8][ext]' // 加上[contenthash:8]
},
},
{
test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体文件
// ...
generator:{
filename:'static/fonts/[name].[contenthash:8][ext]', // 加上[contenthash:8]
},
},
{
test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
// ...
generator:{
filename:'static/media/[name].[contenthash:8][ext]', // 加上[contenthash:8]
},
},
]
},
// ...
}
再修改webpack.prod.js,修改抽离css文件名称格式
// webpack.prod.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = merge(baseConfig, {
mode: 'production',
plugins: [
// 抽离css插件
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css' // 加上[contenthash:8]
}),
// ...
],
// ...
})
再次打包就可以看到文件后面的hash了
2.6 代码分割第三方包和公共模块
一般第三方包的代码变化频率比较小,可以单独把node_modules中的代码单独打包, 当第三包代码没变化时,对应chunkhash值也不会变化,可以有效利用浏览器缓存,还有公共的模块也可以提取出来,避免重复打包加大代码整体体积, webpack提供了代码分隔功能, 需要我们手动在优化项optimization中手动配置下代码分隔splitChunks规则。
修改webpack.prod.js
module.exports = {
// ...
optimization: {
// ...
splitChunks: { // 分隔代码
cacheGroups: {
vendors: { // 提取node_modules代码
test: /node_modules/, // 只匹配node_modules里面的模块
name: 'vendors', // 提取文件命名为vendors,js后缀和chunkhash会自动加
minChunks: 1, // 只要使用一次就提取出来
chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
minSize: 0, // 提取代码体积大于0就提取出来
priority: 1, // 提取优先级为1
},
commons: { // 提取页面公共代码
name: 'commons', // 提取文件命名为commons
minChunks: 2, // 只要使用两次就提取出来
chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
minSize: 0, // 提取代码体积大于0就提取出来
}
}
}
}
}
配置完成后执行打包,可以看到node_modules里面的模块被抽离到vendors.6aeb3c4c.js中,业务代码在main.f70933df.js中。
测试一下,此时verdors.js的chunkhash是6aeb3c4c.js,main.js文件的chunkhash是f70933df,改动一下App.vue,再次打包,可以看到下图main.js的chunkhash值变化了,但是vendors.js的chunkhash还是原先的,这样发版后,浏览器就可以继续使用缓存中的verdors.ec725ef1.js,只需要重新请求main.js就可以了。
2.7 tree-shaking清理未引用js
Tree Shaking的意思就是摇树,伴随着摇树这个动作,树上的枯叶都会被摇晃下来,这里的tree-shaking在代码中摇掉的是未使用到的代码,也就是未引用的代码,最早是在rollup库中出现的,webpack在2版本之后也开始支持。模式mode为production时就会默认开启tree-shaking功能以此来标记未引入代码然后移除掉,测试一下。
在src/components目录下新增Demo1,Demo2两个组件
// src/components/Demo1.vue
<template>
我是Demo1组件
</template>
// src/components/Demo2.vue
<template>
我是Demo2组件
</template>
再在src/components目录下新增index.ts, 把Demo1和Demo2组件引入进来再暴露出去
// src/components/index.ts
export { default as Demo1 } from './Demo1'
export { default as Demo2 } from './Demo2'
在App.vue中引入两个组件,但只使用Demo1组件
<template>
<img :src="smallImg" alt="小于10kb的图片" />
<img :src="bigImg" alt="大于于10kb的图片" />
<!-- 小图片背景容器 -->
<div className='smallImg'></div>
<!-- 大图片背景容器 -->
<div className='bigImg'></div>
<div> 修改App.vue</div>
<!-- 使用Demo1组件 -->
<Demo1 />
</template>
<script setup lang="ts">
import smallImg from './assets/imgs/5kb.png'
import bigImg from './assets/imgs/22kb.png'
import './app.css'
import './app.less'
import { Demo1, Demo2 } from '@/components' //引入Demo1和Demo2组件
</script>
<style scoped>
</style>
再次执行npm run build:dev打包,可以看到在main.js中搜索Demo,只搜索到了Demo1, 代表Demo2组件被tree-shaking移除掉了。
2.8 tree-shaking清理未使用css
js中会有未使用到的代码,css中也会有未被页面使用到的样式,可以通过purgecss-webpack-plugin插件打包的时候移除未使用到的css样式,这个插件是和mini-css-extract-plugin插件配合使用的,在上面已经安装过,还需要glob-all来选择要检测哪些文件里面的类名和id还有标签名称, 安装依赖:
npm i purgecss-webpack-plugin glob-all -D
修改webpack.prod.js
// webpack.prod.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const globAll = require('glob-all')
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin')
module.exports = {
// ...
plugins: [
// 抽离css插件
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css'
}),
// 清理无用css
new PurgeCSSPlugin({
// 检测src下所有vue文件和public下index.html中使用的类名和id和标签名称
// 只打包这些文件中用到的样式
paths: globAll.sync([
`${path.join(__dirname, '../src')}/**/*.vue`,
path.join(__dirname, '../public/index.html')
]),
}),
]
}
测试一下, 现在App.vue中有两个div,类名分别是smallImg和bigImg,当前app.less代码为
#root {
.smallImg {
width: 69px;
height: 75px;
background: url('./assets/imgs/5kb.png') no-repeat;
}
.bigImg {
width: 232px;
height: 154px;
background: url('./assets/imgs/22kb.png') no-repeat;
}
}
此时先执行一下打包,查看main.css
因为页面中有 smallImg和bigImg类名,所以打包后的css也有,此时修改一下app.less中的 .smallImg为 .smallImg1,后面加一个1,这样 .smallImg1就是无用样式了,因为没有页面没有类名为 .smallImg1的节点,再打包后查看 main.css
可以看到main.css已经没有 .smallImg1类名的样式了,做到了删除无用css的功能。
但是purgecss-webpack-plugin插件不是全能的,由于项目业务代码的复杂,插件不能百分百识别哪些样式用到了,哪些没用到,所以请不要寄希望于它能够百分百完美解决你的问题,这个是不现实的。
插件本身也提供了一些白名单safelist属性,符合配置规则选择器都不会被删除掉,比如使用了组件库element-plus, purgecss-webpack-plugin插件检测src文件下vue文件中使用的类名和id时,是检测不到在src中使用element-plus组件的类名的,打包的时候就会把element-plus的类名都给过滤掉,可以配置一下安全选择列表,避免删除element-plus组件库的前缀el-。
new PurgeCSSPlugin({
// ...
safelist: {
standard: [/^el-/], // 过滤以el-开头的类名,哪怕没用到也不删除
}
})
2.9 打包时生成gzip文件
前端代码在浏览器运行,需要从服务器把html,css,js资源下载执行,下载的资源体积越小,页面加载速度就会越快。一般会采用gzip压缩,现在大部分浏览器和服务器都支持gzip,可以有效减少静态资源文件大小,压缩率在 70% 左右。
nginx可以配置gzip: on来开启压缩,但是只在nginx层面开启,会在每次请求资源时都对资源进行压缩,压缩文件会需要时间和占用服务器cpu资源,更好的方式是前端在打包的时候直接生成gzip资源,服务器接收到请求,可以直接把对应压缩好的gzip文件返回给浏览器,节省时间和cpu。
webpack可以借助compression-webpack-plugin 插件在打包时生成 gzip 文章,安装依赖
npm i compression-webpack-plugin -D
添加配置,修改webpack.prod.js
// ...
const CompressionPlugin = require('compression-webpack-plugin')
module.exports = {
// ...
plugins: [
// ...
new CompressionPlugin({
test: /.(js|css)$/, // 只生成css,js压缩文件
filename: '[path][base].gz', // 文件命名
algorithm: 'gzip', // 压缩格式,默认是gzip
test: /.(js|css)$/, // 只生成css,js压缩文件
threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
minRatio: 0.8 // 压缩率,默认值是 0.8
})
]
}
配置完成后再打包,可以看到打包后js的目录下多了一个 .gz 结尾的文件
因为只有verdors.js的大小超过了10k, 所以只有它生成了gzip压缩文件,借助serve -s dist启动dist,查看verdors.js加载情况
可以看到verdors.js的原始大小是61kb, 使用gzip压缩后的文件只剩下了23kb,减少了**70%**左右 的大小,可以极大提升页面初始加载速度,也能减轻服务器压力。
总结
到目前为止已经使用webpack5把vue3+ts的基本构建环境配置完成,并且配置比较常见的优化构建速度和构建结果的配置,完整代码已上传到webpack5-vue3-ts 。还有细节需要优化,比如把容易改变的配置单独写个config.js来配置,输出文件路径封装。这篇文章只是配置,如果想学好webpack,还需要学习webpack的构建原理以及loader和plugin的实现机制。
后续会继续更新代码格式规范, 代码语法规范,git提交规范,集成vue-router,pinia,element-ui等基础项目等配置。
文章系列:
- 《第一篇:基础功能配置,webpack5配置vue3+ts基础环境,实现dev开发和打包构建》。
- 《第二篇:进阶功能配置,环境变量,支持css,less,图片和媒体资源,css3前缀,babel兼容》。
- 《第三篇:优化构建速度:构建耗时分析,持久化缓存,多线程loader,devtool,loader作用范围》。
- 《第四篇》优化化构建结果:构建结果分析,抽离css文件,压缩css,js文件,hash合理配置,代码分割,tree-shaking清理css和js,打包生成gzip》。
- 代码格式规范:editorconfig统一编辑器配置,prettier自动格式化代码,stylelint规范样式和保存自动修复,代码提交自动格式化css和js代码等。
- 代码语法规范:eslint检测js代码语法,style-lint检测样式代码语法,使用tsc检测类型报错,lint-staged按需检测代码等。
- git提交规范:代码提交时husky检测代码语法规范,代码提交时husky检测commit备注规范,commitizen配置commit辅助信息等。
附上上面安装依赖的版本
"dependencies": {
"vue": "^3.3.4"
},
"devDependencies": {
"@babel/core": "^7.22.1",
"@babel/preset-typescript": "^7.21.5",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.2",
"compression-webpack-plugin": "^10.0.0",
"copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3",
"css-loader": "^6.8.1",
"css-minimizer-webpack-plugin": "^5.0.1",
"glob-all": "^3.3.1",
"html-webpack-plugin": "^5.5.1",
"less": "^4.1.3",
"less-loader": "^11.1.2",
"mini-css-extract-plugin": "^2.7.6",
"postcss-loader": "^7.3.2",
"purgecss-webpack-plugin": "^5.0.0",
"speed-measure-webpack-plugin": "^1.5.0",
"style-loader": "^3.3.3",
"terser-webpack-plugin": "^5.3.9",
"thread-loader": "^4.0.2",
"vue-loader": "^17.2.2",
"webpack": "^5.85.1",
"webpack-bundle-analyzer": "^4.9.0",
"webpack-cli": "^5.1.3",
"webpack-dev-server": "^4.15.0",
"webpack-merge": "^5.9.0"
}