背景

原项目使用vue2+vuecli脚手架搭建的。(note: vue cli现已处于维护模式,官方推荐使用create-vue来创建基于vite的新项目了)
vuecli,CLI 服务 (@vue/cli-service) 是一个开发环境依赖。它是一个 npm 包,局部安装在每个 @vue/cli 创建的项目中。CLI 服务是构建于 webpackwebpack-dev-server 之上的。

configureWebpack

此时,webpack配置最简单的方式就是在 vue.config.js 中的 configureWebpack 选项提供一个对象:

1
2
3
4
5
6
7
8
// vue.config.js
module.exports = {
configureWebpack: {
plugins: [
new MyAwesomeWebpackPlugin()
]
}
}

该对象将会被 webpack-merge 合并入最终的 webpack 配置。

如果你需要基于环境有条件地配置行为,或者想要直接修改配置,那就换成一个函数 (该函数会在环境变量被设置之后懒执行)。该方法的第一个参数会收到已经解析好的配置。在函数内,你可以直接修改配置,或者返回一个将会被合并的对象:

1
2
3
4
5
6
7
8
9
10
// vue.config.js
module.exports = {
configureWebpack: config => {
if (process.env.NODE_ENV === 'production') {
// 为生产环境修改配置...
} else {
// 为开发环境修改配置...
}
}
}

查看vue-cli项目的webpack配置:(用于审查的被序列化的格式)
image.png
image.png

webpack-chain

vue-cli中webpack 内部的配置是通过 webpack-chain(opens new window)维护的。这个库提供了一个 webpack 原始配置的上层抽象,使其可以定义具名的 loader 规则和具名插件,并有机会在后期进入这些规则并对它们的选项进行修改。
它允许我们更细粒度的控制其内部配置。这里有一些例子:
编译一个依赖模块、替换已有的基础的 Loader、修改插件选项等
例如:默认情况下 Babel 配置会跳过:

1
2
3
4
5
6
7
8
9
// vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('js')
.include
.add(/some-module-to-transpile/)
}
}

原项目vue.config.js配置

使用了compression-webpack-plugin进行gzip压缩资源;
设置使用svg-sprite-loader,在vue中使用svg;
使用ScriptExtHtmlWebpackPlugin(相当于增强版本的html-webpack-plugin,提供在生成的 html 文件中的 script 标签上增加某些属性,比如 async defer 、crossorigin等等),对runtime chunk 进行内联,不会在请求文件,减少http请求。
使用了splitChunks,进行分包( 最终的目的就是减少请求资源的大小和请求次数。因这两者是互相矛盾的,故要以项目实际的情况去使用 SplitChunks 插件,需切记中庸之道。 )。
Webpack 中一个提取或分离代码的插件,主要作用是提取公共代码,防止代码被重复打包,拆分过大的 js 文件,合并零散的 js 文件。例如将node_modules第三方依赖打包成一个chunk,由于elmentUI也比较大,也可以另外打包出来一个chunk,最后在把src/componts重复引入超过3次的组件也打包出来一个chunk。

抽象

一个项目(新项目),怎么考虑配置webpack?

目标:热启动、打包速度、打包体积
分析工具有哪些
分析当前项目构建问题
对策方案
优化前后对比(打包速度和体积大小)

改成vite的使用?

打包速度 | 配置难易 | 打包大小
vite使用esbuild+esm,现代浏览器esm优势,热更新启动快。vite的配置语法也比webpack简单一些,但webpack插件等社区生态比较丰富。而webpack目前没有支持esm,使用polyfill加载模块,打包大小会比vite大。
其他原因
已经稳定运行的webpack项目要换构建工具是一个潜在成本很大的事情。
webpack5
最近随着 WebAssembly 这个概念的兴起, 其实Webpack5在性能上也有一些质的飞跃
在项目维护上,能否在生产环境使用webpack,在测试环境使用vite?

案例情况

  1. 发现同一库存在多个版本;
  2. 第三方包太大;(例如moment)
  3. 没有按需加载;(例如lodash,echart)
  4. 在某些场景下才会使用到的包;(例如某些插件并不是每次打开页面的时候都会开启,这样可以把流程修改成在 url 上添加一个开启该插件的参数,并且在代码中改成动态加载)

思考

  1. 是否引入了第三方库的多个版本,例如 antd,react 等库
  2. 在引入第三方库的时候要先进行思考(考验的就是选型的能力)
  3. 是否要引入第三方库,能否自己手写实现一个?
  4. 多个第三方库进行对比,哪个更适合当下的场景?
  5. 第三方库包体积有多大?是否支持按需引入?
  6. 总结&寻求更多的包体积优化方案,在这里讲的只是发现问题的方式之一。
  7. 建立前端性能指标,例如包体积应该要小于多少才是合理的?(当然还有其他指标)。
  8. 寻求自动化解决方案,例如在构建完之后获取检查包体积是否符合指标,不符合则进行邮件 OR 钉钉通知对应的负责人进行优化。

项目分析和webpack优化

分析工具

webpack-bundle-analyzer

安装webpack-bundle-analyzer -D (vue-cli 里面会有)
在构建命令后面拼接 –report( package.json: script :”build:test”: “vue-cli-service build –report”,)
构建执行了之后,会生成report.html,即打包分析报告:
image.png
(打包后,有静态文件,其中,images文件夹的大小也不低,这里能否优化?)
report.html:
image.png
image.png
可以发现打包后的体积大概在4M,其中,echarts占1M(GZipped 320K):优化方向:按需使用;或者使用CDN方式引用(风险就是CDN失效会有问题,公共免费的CDN不稳定,除非公司自己有自己的CDN)。

打包体积优化:echarts按需使用

方案:https://echarts.apache.org/handbook/zh/basics/import/
注意:不要引入echarts的主题,不然按需引入的方式,会额外存在echarts的引入;
优化后:echart占了469KB(Gzipped 150KB)
image.png

打包体积优化:使用compression-webpack-plugin

线上的项目,一般都会结合构建工具 webpack 插件或服务端配置 nginx,来实现 http 传输的 gzip 压缩,目的就是把服务端响应文件的体积尽量减小,优化返回速度。
前端将文件打包成 .gz 文件,然后通过 nginx 的配置,让浏览器直接解析 .gz 文件,可以大大提升文件加载的速度,浏览器可以直接解析 .gz 文件并解压。

压缩文件格式介绍
.gz:浏览器可以直接解析并解压。
.br:BR 文件是使用 Brotli(一种开源数据压缩算法)压缩的文件,它包含网页资产,例如 CSS 、XML、SVG、JS 文件,以 Brotli 压缩数据格式压缩。 Web 浏览器(例如 Chrome 和 Firefox)使用 BR 文件来提高页面加载速度,Brotli 数据压缩格式旨在替代 Zopfli 压缩算法,该格式允许的压缩率比Zopfli 高大约 20%。
.br 压缩需要基于 nodejs >=v10.16.0 版本才能生成,一般本地没问题,需要注意线上服务器会可能发生找不到 zlib 的情况并进行安装。
使用插件:

1
2
3
4
5
6
7
new CompressionPlugin({
cache: false, // 不启用文件缓存
test: /\.(js|css|html)?$/i, // 压缩文件格式
filename: '[path].gz[query]', // 压缩后的文件名
algorithm: 'gzip', // 使用gzip压缩
minRatio: 0.8 // 压缩率小于1才会压缩
})

效果:3M->1M
image.png

打包体积优化使用splitChunks进行分包

最终的目的就是减少请求资源的大小和请求次数。因这两者是互相矛盾的,故要以项目实际的情况去使用 SplitChunks 插件,需切记中庸之道。

图片压缩

基于 Node 库的 imagemin
配置 image-webpack-loader
image.png

  • 有很多定制选项
  • 可以引入更多第三方优化插件,例如pngquant
  • 可以处理多种图片格式

pngquant: 是一款PNG压缩器,通过将图像转换为具有alpha通道(通常比24/32位PNG
文件小60-80%)的更高效的8位PNG格式,可显著减小文件大小。
pngcrush:其主要目的是通过尝试不同的压缩级别和PNG过滤方法来降低PNG IDAT数据
流的大小。
optipng:其设计灵感来自于pngcrush。optipng可将图像文件重新压缩为更小尺寸,而不
会丢失任何信息。
tinypng:也是将24位png文件转化为更小有索引的8位图片,同时所有非必要的metadata
也会被剥离掉

speed-measure-webpack-plugin

speed-measure-webpack-plugin 是一个 Webpack 插件,可以帮助你测量和分析构建项目时各个插件和 Loader 的执行时间。这可以帮助你优化构建速度,因为你可以找出慢速插件并优化它们。
安装npm install speed-measure-webpack-plugin –save-dev
vue-config.js 中引入

1
2
3
4
5
6
7
8
9
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin({
outputFormat: 'human'
})
module.exports = {
configureWebpack: smp.wrap({
plugins: []
})
}

执行构建:
image.png

多线程打包

方案一:happyPack (作者已经不在维护,其推荐使用thread-loader)
由于运行在 Node.js 之上的 webpack 是单线程模型的,我们需要 webpack 能同一时间处理多个任务,发挥多核 CPU 电脑的威力.HappyPack 就能实现多线程打包,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程,来提升打包速度。

方案二:thread-loader(vue-cli带有)
使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。

其他

缩小文件搜索范围

  • 优化loader配置:include 、 exclude
  • 优化module.noParse配置: 忽略对部分没采用模块化的文件的递归解析处理
  • 优化resolve.modules配置: 去哪些目录下寻找第三方模块
  • 优化resolve.alias配置
  • 优化resolve.mainFields配置
  • 优化resolve.extensions配置:配置在尝试匹配过程中用到的后缀列表

减少打包文件

  • 提取公共代码
  • 动态链接DllPlugin (dll 选项将被删除。 Webpack 4 应该提供足够好的性能,并且在 Vue CLI 内维护 DLL 模式的成本不再合理;issue:https://github.com/vuejs/vue-cli/issues/1205
  • externals
  • Tree Shaking

缓存(二次打包时间)

  • babel 缓存 cacheDirectory: true
  • cache-loader
  • contenthash
  • image.png
  • image.png

多进程

  • happypack
  • thread-loader
  • image.png

** 并行压缩:**

  • terser-webpack-plugin (vue-cli默认带有)
  • terser-webpack-plugin 使用 Terser 来进行 JavaScript 代码的压缩和混淆。Terser 是一个 JavaScript 压缩器和美化器,它可以将 JavaScript 代码压缩成更小的体积,从而加快网页的加载速度,提高用户体验。

修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
configureWebpack: {
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 移除console.log
},
},
}),
],
},
},
};

优化前后对比

项目其实只添加了echarts按需加载,其他vue-cli已经做了优化和项目已经配置了分包策略、gzip压缩。
优化前后对比:
打包体积:4M->3.6M
打包速度:80s->50s
做了缓存,二次打包速度加快。

项目改用成vite(rollup配置)和对比

为了保证老项目的正常运行,在生产环境还是使用webpack,而在开发环境使用vite。
创建并配置vite.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import { defineConfig } from 'vite'
import { createHtmlPlugin } from 'vite-plugin-html'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path-browserify'
import { createVuePlugin } from 'vite-plugin-vue2'
import dotenv from 'dotenv'
import dynamicImport from 'vite-plugin-dynamic-import'
import ViteRequireContext from '@originjs/vite-plugin-require-context'

const resolve = (dir) => {
return path.resolve(__dirname, dir)
}
export default defineConfig(({ mode }) => {
dotenv.config()
dotenv.config({ path: `./.env.${mode}` })
const env = process.env
const {
VUE_APP_NAME: appName,
VUE_APP_BUILD_MODE: buildMode,
VUE_APP_BASE_URL: baseUrl,
VUE_APP_PORT: appPort = 3002,
VUE_APP_GATEWAY_PATH: GATEWAY_PATH = 'pscpadmin',
VUE_APP_BASE_API,
} = env
console.log(env)

return ({
base: '/',
define:{
'process.env': {
...env,
Vite: true
}
},
resolve: {
alias: [
// 针对以 ~@/ 开头,替换为 src/
{ find: /^~@\//, replacement: path.join(__dirname, 'src/') },
// 针对以 @/ 开头的,替换为 src/
{ find: /^@\//, replacement: path.join(__dirname, './src', '/') },
{
// this is required for the SCSS modules
find: /^~(.*)$/,
replacement: '$1'
}
],
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
plugins: [
vue(),
vueJsx(),
createVuePlugin({ jsx: true }),
createHtmlPlugin({
minify: true,
/**
* 在这里写entry后,你将不需要在`index.html`内添加 script 标签,原有标签需要删除
* @default src/main.ts
*/
entry: 'src/main.js',
/**
* 如果你想将 `index.html`存放在指定文件夹,可以修改它,否则不需要配置
* files in the public directory are served at the root path.
* Instead of /public/index.html, use /index.html
* @default index.html
*/
template: 'public/index.html',

/**
* 需要注入 index.html ejs 模版的数据
*/
inject: {
data: {
title: 'index-test',
injectScript: `<script type="module" src="src/main.js"></script>`
},
tags: [
{
injectTo: 'body-prepend',
tag: 'div',
attrs: {
id: 'tag'
}
}
]
}
}),
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹(路径为存放所有svg图标的文件夹不单个svg图标)
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],
// 指定symbolId格式
symbolId: 'icon-[name]'
}),
dynamicImport(),
// ViteRequireContext()
],
server: {
host: true,
port: appPort,
open: true,
proxy: {
[process.env.VUE_APP_BASE_API]: {
// target: `http://127.0.0.1:18888`,
target: `http://220.197.14.198:8000`,
changeOrigin: true,
bypass: (req, res, proxyOption) => {
console.log( `当前请求代理:${req.url} -> ${proxyOption.target}` );
},
}
},
},
})
})

注意安装vite、@vitejs/plugin-vue和其他插件的版本,不然可能会报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
{
"name": "shenghong",
"version": "1.0.0",
"description": "高速公路管理系统",
"author": "zcc",
"license": "MIT",
"scripts": {
"dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"build:test": "vue-cli-service build --report",
"preview": "node build/index.js --preview",
"lint": "eslint --ext .js,.vue src",
"vite": "vite"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,vue}": [
"eslint --fix",
"git add"
]
},
"keywords": [
"vue",
"admin",
"dashboard",
"element-ui",
"boilerplate",
"admin-template",
"management-system"
],
"repository": {
"type": "git",
"url": "https://gitee.com/y_project/ShengHong-Cloud.git"
},
"dependencies": {
"@riophae/vue-treeselect": "0.4.0",
"axios": "0.24.0",
"clipboard": "2.0.8",
"core-js": "3.25.3",
"dayjs": "^1.11.10",
"dotenv": "^16.3.1",
"echarts": "5.2.2",
"element-ui": "2.15.10",
"file-saver": "2.0.5",
"fuse.js": "6.4.3",
"highlight.js": "9.18.5",
"js-beautify": "1.13.0",
"js-cookie": "3.0.1",
"jsencrypt": "3.0.0-rc.1",
"nprogress": "0.2.0",
"path-browserify": "^1.0.1",
"quill": "1.3.7",
"screenfull": "5.0.2",
"sortablejs": "1.10.2",
"swiper": "5.x",
"vite-plugin-svg-icons": "^2.0.1",
"vue": "2.6.12",
"vue-awesome-swiper": "^4.1.1",
"vue-count-to": "1.0.13",
"vue-cropper": "0.5.5",
"vue-meta": "2.4.0",
"vue-router": "3.0.1",
"vuedraggable": "2.24.3",
"vuex": "3.6.0"
},
"devDependencies": {
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@originjs/vite-plugin-require-context": "^1.0.9",
"@vitejs/plugin-vue": "1.10.0",
"@vitejs/plugin-vue-jsx": "1.2.0",
"@vue/cli-plugin-babel": "4.4.6",
"@vue/cli-plugin-eslint": "4.4.6",
"@vue/cli-service": "4.4.6",
"@vue/compiler-sfc": "^3.3.4",
"babel-eslint": "10.1.0",
"babel-plugin-dynamic-import-node": "2.3.3",
"chalk": "4.1.0",
"compression-webpack-plugin": "5.0.2",
"connect": "3.6.6",
"eslint": "7.15.0",
"eslint-plugin-vue": "7.2.0",
"html-webpack-plugin": "^5.5.1",
"lint-staged": "10.5.3",
"runjs": "4.4.2",
"sass": "1.32.13",
"sass-loader": "10.1.1",
"script-ext-html-webpack-plugin": "2.1.5",
"svg-sprite-loader": "5.1.1",
"vite": "2.9.2",
"vite-plugin-commonjs": "^0.10.0",
"vite-plugin-dynamic-import": "^1.5.0",
"vite-plugin-html": "^3.2.0",
"vite-plugin-vue2": "^2.0.3",
"vue-template-compiler": "2.6.12"
},
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions"
],
"volta": {
"node": "14.21.3",
"yarn": "1.22.17"
}
}

几个关键的点:

使用vite-plugin-html做 index.html ejs模板的创建和数据的注入;

使用dotenv做vue cli变量的转换和在Vite下的兼容:

image.png

路径path使用path-browserify,文件名搜索extensions和做路径的alias别名定义:

image.png

vue、jsx等语法识别插件:

image.png

SVGIcons组件的改造和使用

image.png
main.js导入:
image.png

webpack wequire.context 和vite import.meta.globEager

image.png

vite动态路由,路由的使用

两种方式:(原理都是搜索路径,拿到文件路径值)
1 vite.config.js插件
image.png
2 使用vite import.meta.glob
image.png

cjs兼容问题

vite无法识别使用module.exports