├── .browserslistrc
├── .gitignore
├── README.md
├── babel.config.js
├── build
├── webpack.analy.js
├── webpack.base.js
├── webpack.dev.js
└── webpack.prod.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
└── index.html
├── src
├── App.vue
├── app.css
├── app.less
├── assets
│ └── imgs
│ │ ├── 22kb.png
│ │ └── 5kb.png
├── components
│ ├── Demo1.vue
│ ├── Demo2.vue
│ └── index.ts
├── images.d.ts
└── index.ts
└── tsconfig.json
/.browserslistrc:
--------------------------------------------------------------------------------
1 | IE 9 # 兼容IE 9
2 | chrome 35 # 兼容chrome 35
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 【前端工程化】webpack5从零搭建完整的vue3+ts开发和打包环境
2 |
3 | ## 目录
4 |
5 | 1. 前言
6 | 2. 初始化项目
7 | 3. 配置基础版**vue3**+**ts**环境
8 | 4. 常用功能配置
9 | 5. 优化构建速度
10 | 6. 优化构建结果文件
11 | 7. 总结
12 |
13 | **全文概览**
14 |
15 | 
16 |
17 | ## 一. 前言
18 |
19 | 本文是专栏[《前端工程化》](https://juejin.cn/column/7092293219064479752 "https://juejin.cn/column/7092293219064479752")第8篇文章,会持续更新前端工程化方面详细高质量的详细教程
20 |
21 | 从**2020**年**10**月**10**日,**webpack** 升级至 5 版本到现在已经快两年,**webpack5**版本优化了很多原有的功能比如**tree-shakin**g优化,也新增了很多新特性,比如联邦模块,具体变动可以看这篇文章[阔别两年,webpack 5 正式发布了!](https://juejin.cn/post/6882663278712094727)。
22 |
23 | 本文将使用最新的**webpack5**一步一步从零搭建一个完整的**vue3+ts**开发和打包环境,配置完善的模块热替换以及**构建速度**和**构建结果**的优化,完整代码已上传到[webpack5-vue3-ts](https://github.com/guojiongwei/webpack5-vue3-ts.git)。本文只是配置**webpack5**的,配置**代码规范和工程化**会在下一篇文章发布。
24 |
25 | ## 二. 初始化项目
26 |
27 | 在开始**webpack**配置之前,先手动初始化一个基本的**vue3**+**ts**项目,新建项目文件夹**webpack5-vue3-18**, 在项目下执行
28 |
29 | ```bash
30 | npm init -y
31 | ```
32 |
33 | 初始化好**package.json**后,在项目下新增以下所示目录结构和文件
34 |
35 | ```yaml
36 | ├── build
37 | | ├── webpack.base.js # 公共配置
38 | | ├── webpack.dev.js # 开发环境配置
39 | | └── webpack.prod.js # 打包环境配置
40 | ├── public
41 | │ └── index.html # html模板
42 | ├── src
43 | | ├── App.vue
44 | │ └── index.ts # vue3应用入口页面
45 | ├── tsconfig.json # ts配置
46 | └── package.json
47 | ```
48 |
49 | 安装**webpack**依赖
50 |
51 | ```sh
52 | npm i webpack@5.85.1 webpack-cli@5.1.3 -D
53 | ```
54 |
55 | 安装**vue3**依赖
56 |
57 | ```sh
58 | npm i vue@^3.3.4 -S
59 | ```
60 |
61 | 添加**public/index.html**内容
62 |
63 | ```html
64 |
65 |
66 |
67 |
68 |
69 |
70 | webpack5-vue3-ts
71 |
72 |
73 |
74 |
75 |
76 |
77 | ```
78 |
79 | 添加**tsconfig.json**内容
80 |
81 | ```json
82 | {
83 | "compilerOptions": {
84 | "target": "ES2020",
85 | "useDefineForClassFields": true,
86 | "module": "ESNext",
87 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
88 | "skipLibCheck": true,
89 |
90 | /* Bundler mode */
91 | "moduleResolution": "bundler",
92 | "allowImportingTsExtensions": true,
93 | "resolveJsonModule": true,
94 | "isolatedModules": true,
95 | "noEmit": true,
96 | "jsx": "preserve",
97 |
98 | /* Linting */
99 | "strict": true,
100 | "noUnusedLocals": true,
101 | "noUnusedParameters": true,
102 | "noFallthroughCasesInSwitch": true
103 | },
104 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"],
105 | }
106 | ```
107 |
108 | 添加**src/App.vue**内容
109 |
110 | ```vue
111 |
112 | webpack5-vue3-ts
113 |
114 |
115 |
117 |
119 | ```
120 |
121 | 添加**src/index.ts**内容
122 |
123 | ```ts
124 | import { createApp } from 'vue'
125 | import App from './App.vue'
126 |
127 | createApp(App).mount('#root')
128 | ```
129 |
130 | 现在项目业务代码已经添加好了,接下来可以配置**webpack**的代码了。
131 |
132 | ## 三. 配置基础版vue3+ts环境
133 |
134 | ### 2.1. webpack公共配置
135 |
136 | 修改**webpack.base.js**
137 |
138 | **1. 配置入口文件**
139 |
140 | ```js
141 | // webpack.base.js
142 | const path = require('path')
143 |
144 | module.exports = {
145 | entry: path.join(__dirname, '../src/index.ts'), // 入口文件
146 | }
147 | ```
148 |
149 | **2. 配置出口文件**
150 |
151 | ```js
152 | // webpack.base.js
153 | const path = require('path')
154 |
155 | module.exports = {
156 | // ...
157 | // 打包文件出口
158 | output: {
159 | filename: 'static/js/[name].js', // 每个输出js的名称
160 | path: path.join(__dirname, '../dist'), // 打包结果输出路径
161 | clean: true, // webpack4需要配置clean-webpack-plugin来删除dist文件,webpack5内置了
162 | publicPath: '/' // 打包后文件的公共前缀路径
163 | },
164 | }
165 | ```
166 |
167 | **3. 配置loader解析ts和vue**
168 |
169 | 由于**webpack**默认只能识别**js**文件,不能识别**vue**语法,需要配置**loader**的预设预设 [**@babel/preset-typescript**](https://www.npmjs.com/package/vue-loader) 来先**ts**语法转换为 **js** 语法。再借助 [**vue-loader**](https://www.npmjs.com/package/vue-loader) 来识别**vue**语法。
170 |
171 | **安装babel核心模块和babel预设以及vue-loader**
172 |
173 | ```sh
174 | npm i babel-loader@^9.1.2 @babel/core@^7.22.1 @babel/preset-typescript@^7.21.5 vue-loader@^17.2.2 -D
175 | ```
176 |
177 | 在**webpack.base.js**添加**module.rules**配置和**vue-loader**对应插件配置
178 |
179 | ```js
180 | // webpack.base.js
181 | const { VueLoaderPlugin } = require('vue-loader')
182 | module.exports = {
183 | // ...
184 | module: {
185 | rules: [
186 | {
187 | test: /\.vue$/, // 匹配.vue文件
188 | use: 'vue-loader', // 用vue-loader去解析vue文件
189 | },
190 | {
191 | test: /\.ts$/, // 匹配.ts文件
192 | use: {
193 | loader: 'babel-loader',
194 | options: {
195 | presets: [
196 | [
197 | "@babel/preset-typescript",
198 | {
199 | allExtensions: true, //支持所有文件扩展名(重要)
200 | },
201 | ],
202 | ]
203 | }
204 | }
205 | }
206 | ]
207 | },
208 | plugins: [
209 | // ...
210 | new VueLoaderPlugin(), // vue-loader插件
211 | ]
212 | }
213 | ```
214 |
215 | 单文件.vue文件被 vue-loader 解析三个部分,script 部分会由 ts 来处理,但是ts不会处理 .vue 结尾的文件,所以要在预设里面加上 allExtensions: true配置项来支持所有文件扩展名。
216 |
217 | **4. 配置extensions**
218 |
219 | **extensions**是**webpack**的**resolve**解析配置下的选项,在引入模块时不带文件后缀时,会来该配置数组里面依次添加后缀查找文件,因为**ts**不支持引入以 **.ts**, **.vue**为后缀的文件,所以要在**extensions**中配置,而第三方库里面很多引入**js**文件没有带后缀,所以也要配置下**js**
220 |
221 | 修改**webpack.base.js**,注意把高频出现的文件后缀放在前面
222 |
223 | ```js
224 | // webpack.base.js
225 | module.exports = {
226 | // ...
227 | resolve: {
228 | extensions: ['.vue', '.ts', '.js', '.json'],
229 | }
230 | }
231 | ```
232 |
233 | 这里只配置**js**, **vue**和**ts和json**,其他文件引入都要求带后缀,可以提升构建速度。
234 |
235 | **4. 添加html-webpack-plugin插件**
236 |
237 | **webpack**需要把最终构建好的静态资源都引入到一个**html**文件中,这样才能在浏览器中运行,[html-webpack-plugin](https://www.npmjs.com/package/html-webpack-plugin)就是来做这件事情的,安装依赖:
238 |
239 | ```sh
240 | npm i html-webpack-plugin -D
241 | ```
242 |
243 | 因为该插件在开发和构建打包模式都会用到,所以还是放在公共配置**webpack.base.js**里面
244 |
245 | ```js
246 | // webpack.base.js
247 | const path = require('path')
248 | const HtmlWebpackPlugin = require('html-webpack-plugin')
249 |
250 | module.exports = {
251 | // ...
252 | plugins: [
253 | new HtmlWebpackPlugin({
254 | template: path.resolve(__dirname, '../public/index.html'), // 模板取定义root节点的模板
255 | inject: true, // 自动注入静态资源
256 | })
257 | ]
258 | }
259 | ```
260 |
261 | 到这里一个最基础的**vue3**基本公共配置就已经配置好了,需要在此基础上分别配置开发环境和打包环境了。
262 |
263 | ### 2.2. webpack开发环境配置
264 |
265 | **1. 安装 webpack-dev-server**
266 |
267 | 开发环境配置代码在**webpack.dev.js**中,需要借助 [webpack-dev-server](https://www.npmjs.com/package/webpack-dev-server)在开发环境启动服务器来辅助开发,还需要依赖[webpack-merge](https://www.npmjs.com/package/webpack-merge)来合并基本配置,安装依赖:
268 |
269 | ```sh
270 | npm i webpack-dev-server webpack-merge -D
271 | ```
272 |
273 | 修改**webpack.dev.js**代码, 合并公共配置,并添加开发模式配置
274 |
275 | ```js
276 | // webpack.dev.js
277 | const path = require('path')
278 | const { merge } = require('webpack-merge')
279 | const baseConfig = require('./webpack.base.js')
280 |
281 | // 合并公共配置,并添加开发环境配置
282 | module.exports = merge(baseConfig, {
283 | mode: 'development', // 开发模式,打包更加快速,省了代码优化步骤
284 | devtool: 'eval-cheap-module-source-map', // 源码调试模式,后面会讲
285 | devServer: {
286 | port: 3000, // 服务端口号
287 | compress: false, // gzip压缩,开发环境不开启,提升热更新速度
288 | hot: true, // 开启热更新,后面会讲vue3模块热替换具体配置
289 | historyApiFallback: true, // 解决history路由404问题
290 | static: {
291 | directory: path.join(__dirname, "../public"), //托管静态资源public文件夹
292 | }
293 | }
294 | })
295 | ```
296 |
297 | **2. package.json添加dev脚本**
298 |
299 | 在**package.json**的**scripts**中添加
300 |
301 | ```js
302 | // package.json
303 | "scripts": {
304 | "dev": "webpack-dev-server -c build/webpack.dev.js"
305 | },
306 | ```
307 |
308 | **-c 是 --config的缩写,是指定webpack-dev-server的配置文件。**
309 |
310 | 执行**npm run dev**,就能看到项目已经启动起来了,访问,就可以看到项目界面,具体完善的**vue3**模块热替换在下面会讲到。
311 |
312 | ### 2.3. webpack打包环境配置
313 |
314 | **1. 修改webpack.prod.js代码**
315 |
316 | ```js
317 | // webpack.prod.js
318 | const { merge } = require('webpack-merge')
319 | const baseConfig = require('./webpack.base.js')
320 | module.exports = merge(baseConfig, {
321 | mode: 'production', // 生产模式,会开启tree-shaking和压缩代码,以及其他优化
322 | })
323 | ```
324 |
325 | **2. package.json添加build打包命令脚本**
326 |
327 | 在**package.json**的**scripts**中添加**build**打包命令
328 |
329 | ```js
330 | "scripts": {
331 | "dev": "webpack-dev-server -c build/webpack.dev.js",
332 | "build": "webpack -c build/webpack.prod.js"
333 | },
334 | ```
335 |
336 | 执行**npm run build**,最终打包在**dist**文件中, 打包结果:
337 |
338 | ```sh
339 | dist
340 | ├── static
341 | | ├── js
342 | | ├── main.js
343 | ├── index.html
344 | ```
345 |
346 | **3. 浏览器查看打包结果**
347 |
348 | 打包后的**dist**文件可以在本地借助**node**服务器**serve**打开,全局安装**serve**
349 |
350 | ```sh
351 | npm i serve -g
352 | ```
353 |
354 | 然后在项目根目录命令行执行**serve -s dist**,就可以启动打包后的项目了。
355 |
356 | 到现在一个基础的支持**vue3**和**ts**的**webpack5**就配置好了,但只有这些功能是远远不够的,还需要继续添加其他配置。
357 |
358 | ## 四. 基础功能配置
359 |
360 | ### 4.1 配置环境变量
361 |
362 | 环境变量按作用来分分两种
363 |
364 | 1. 区分是开发模式还是打包构建模式
365 | 2. 区分项目业务环境,开发/测试/预测/正式环境
366 |
367 | 区分开发模式还是打包构建模式可以用**process.env.NODE\_ENV**,因为很多第三方包里面判断都是采用的这个环境变量。
368 |
369 | 区分项目接口环境可以自定义一个环境变量**process.env.BASE\_ENV**,设置环境变量可以借助[cross-env](https://www.npmjs.com/package/cross-env)和[webpack.DefinePlugin](https://www.webpackjs.com/plugins/define-plugin/)来设置。
370 |
371 | * **cross-env**:兼容各系统的设置环境变量的包
372 | * **webpack.DefinePlugin**:**webpack**内置的插件,可以为业务代码注入环境变量
373 |
374 | 安装**cross-env**
375 |
376 | ```sh
377 | npm i cross-env -D
378 | ```
379 |
380 | 修改**package.json**的**scripts**脚本字段,删除原先的**dev**和**build**,改为
381 |
382 | ```js
383 | "scripts": {
384 | "dev:dev": "cross-env NODE_ENV=development BASE_ENV=development webpack-dev-server -c build/webpack.dev.js",
385 | "dev:test": "cross-env NODE_ENV=development BASE_ENV=test webpack-dev-server -c build/webpack.dev.js",
386 | "dev:pre": "cross-env NODE_ENV=development BASE_ENV=pre webpack-dev-server -c build/webpack.dev.js",
387 | "dev:prod": "cross-env NODE_ENV=development BASE_ENV=production webpack-dev-server -c build/webpack.dev.js",
388 |
389 | "build:dev": "cross-env NODE_ENV=production BASE_ENV=development webpack -c build/webpack.prod.js",
390 | "build:test": "cross-env NODE_ENV=production BASE_ENV=test webpack -c build/webpack.prod.js",
391 | "build:pre": "cross-env NODE_ENV=production BASE_ENV=pre webpack -c build/webpack.prod.js",
392 | "build:prod": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.prod.js"
393 | }
394 | ```
395 |
396 | **dev**开头是开发模式,**build**开头是打包模式,冒号后面对应的**dev**/**test**/**pre**/**prod**是对应的业务环境的**开发**/**测试**/**预测**/**正式**环境。
397 |
398 | **process.env.NODE\_ENV**环境变量**webpack**会自动根据设置的**mode**字段来给业务代码注入对应的**development**和**prodction**,这里在命令中再次设置环境变量**NODE\_ENV**是为了在**webpack**和**babel**的配置文件中访问到。
399 |
400 | 在**webpack.base.js**中打印一下设置的环境变量
401 |
402 | ```js
403 | // webpack.base.js
404 | // ...
405 | console.log('NODE_ENV', process.env.NODE_ENV)
406 | console.log('BASE_ENV', process.env.BASE_ENV)
407 | ```
408 |
409 | 执行**npm run build:dev**,可以看到打印的信息
410 |
411 | ```js
412 | // NODE_ENV production
413 | // BASE_ENV development
414 | ```
415 |
416 | 当前是打包模式,业务环境是开发环境,这里需要把**process.env.BASE\_ENV**注入到业务代码里面,就可以通过该环境变量设置对应环境的接口地址和其他数据,要借助**webpack.DefinePlugin**插件。
417 |
418 | 修改**webpack.base.js**
419 |
420 | ```js
421 | // webpack.base.js
422 | // ...
423 | const webpack = require('webpack')
424 | module.export = {
425 | // ...
426 | plugins: [
427 | // ...
428 | new webpack.DefinePlugin({
429 | 'process.env.BASE_ENV': JSON.stringify(process.env.BASE_ENV),
430 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
431 | })
432 | ]
433 | }
434 | ```
435 |
436 | 配置后会把值注入到业务代码里面去,**webpack**解析代码匹配到**process.env.BASE\_ENV**,就会设置到对应的值。测试一下,在**src/index.vue**打印一下两个环境变量
437 |
438 | ```vue
439 | // src/index.vue
440 | // ...
441 | console.log('NODE_ENV', process.env.NODE_ENV)
442 | console.log('BASE_ENV', process.env.BASE_ENV)
443 | ```
444 |
445 | 执行**npm run dev:test**,可以在浏览器控制台看到打印的信息
446 |
447 | ```js
448 | // NODE_ENV development
449 | // BASE_ENV test
450 | ```
451 |
452 | 当前是开发模式,业务环境是测试环境。
453 |
454 | ### 4.2 处理css和less文件
455 |
456 | 在**src**下新增**app.css**
457 |
458 | ```css
459 | h2 {
460 | color: red;
461 | transform: translateY(100px);
462 | }
463 | ```
464 |
465 | 在**src/App.vue**中引入**app.css**
466 |
467 | ```vue
468 |
469 | webpack5-vue3-ts
470 |
471 |
472 |
475 |
476 |
478 | ```
479 |
480 | 执行打包命令**npm run build:dev**,会发现有报错, 因为**webpack**默认只认识**js**,是不识别**css**文件的,需要使用**loader**来解析**css**, 安装依赖
481 |
482 | ```sh
483 | npm i style-loader css-loader -D
484 | ```
485 |
486 | * **style-loader**: 把解析后的**css**代码从**js**中抽离,放到头部的**style**标签中(在运行时做的)
487 | * **css-loader:** 解析**css**文件代码
488 |
489 | 因为解析**css**的配置开发和打包环境都会用到,所以加在公共配置**webpack.base.js**中
490 |
491 | ```js
492 | // webpack.base.js
493 | // ...
494 | module.exports = {
495 | // ...
496 | module: {
497 | rules: [
498 | // ...
499 | {
500 | test: /\.css$/, //匹配 css 文件
501 | use: ['style-loader','css-loader']
502 | }
503 | ]
504 | },
505 | // ...
506 | }
507 | ```
508 |
509 | 上面提到过,**loader**执行顺序是从右往左,从下往上的,匹配到**css**文件后先用**css-loader**解析**css**, 最后借助**style-loader**把**css**插入到头部**style**标签中。
510 |
511 | 配置完成后再**npm run build:dev**打包,借助**serve -s dist**启动后在浏览器查看,可以看到样式生效了。
512 |
513 | 
514 |
515 | ### 4.3 支持less或scss
516 |
517 | 项目开发中,为了更好的提升开发体验,一般会使用**css**超集**less**或者**scss**,对于这些超集也需要对应的**loader**来识别解析。以**less**为例,需要安装依赖:
518 |
519 | ```sh
520 | npm i less-loader less -D
521 | ```
522 |
523 | * **less-loader**: 解析**less**文件代码,把**less**编译为**css**
524 | * **less**: **less**核心
525 |
526 | 实现支持**less**也很简单,只需要在**rules**中添加**less**文件解析,遇到**less**文件,使用**less-loader**解析为**css**,再进行**css**解析流程,修改**webpack.base.js**:
527 |
528 | ```js
529 | // webpack.base.js
530 | module.exports = {
531 | // ...
532 | module: {
533 | // ...
534 | rules: [
535 | // ...
536 | {
537 | test: /.(css|less)$/, //匹配 css和less 文件
538 | use: ['style-loader','css-loader', 'less-loader']
539 | }
540 | ]
541 | },
542 | // ...
543 | }
544 | ```
545 |
546 | 测试一下,新增**src/app.less**
547 |
548 | ```less
549 | #root {
550 | h2 {
551 | font-size: 20px;
552 | }
553 | }
554 | ```
555 |
556 | 在**App.vue**中引入**app.less**,执行**npm run build:dev**打包,借助**serve -s dist**启动项目,可以看到**less**文件编写的样式编译**css**后也插入到**style**标签了了。
557 |
558 | 
559 |
560 | ### 4.4 处理css3前缀兼容(可省略)
561 |
562 | 使用了**vue3**了基本上就不用考虑低版本浏览器了,但有时候需要给**css3**加前缀,可以学一下,可以借助插件来自动加前缀, [postcss-loader](https://link.juejin.cn/?target=https%3A%2F%2Fwebpack.docschina.org%2Floaders%2Fpostcss-loader%2F)就是来给**css3**加浏览器前缀的,安装依赖:
563 |
564 | ```sh
565 | npm i postcss-loader autoprefixer -D
566 | ```
567 |
568 | * **postcss-loader**:处理**css**时自动加前缀
569 | * **autoprefixer**:决定添加哪些浏览器前缀到**css**中
570 |
571 | 修改**webpack.base.js**, 在解析**css**和**less**的规则中添加配置
572 |
573 | ```js
574 | module.exports = {
575 | // ...
576 | module: {
577 | rules: [
578 | // ...
579 | {
580 | test: /.(css|less)$/, //匹配 css和less 文件
581 | use: [
582 | 'style-loader',
583 | 'css-loader',
584 | // 新增
585 | {
586 | loader: 'postcss-loader',
587 | options: {
588 | postcssOptions: {
589 | plugins: ['autoprefixer']
590 | }
591 | }
592 | },
593 | 'less-loader'
594 | ]
595 | }
596 | ]
597 | },
598 | // ...
599 | }
600 | ```
601 |
602 | 配置完成后,需要有一份要兼容浏览器的清单,让**postcss-loader**知道要加哪些浏览器的前缀,在根目录创建 **.browserslistrc**文件
603 |
604 | ```sh
605 | IE 9 # 兼容IE 9
606 | chrome 35 # 兼容chrome 35
607 | ```
608 |
609 | 以兼容到**ie9**和**chrome35**版本为例,配置好后,执行**npm run build:dev**打包,可以看到打包后的**css**文件已经加上了**ie**和谷歌内核的前缀
610 |
611 | 
612 |
613 | 上面可以看到解析**css**和**less**有很多重复配置,可以进行提取**postcss-loader**配置优化一下
614 |
615 | **postcss.config.js**是**postcss-loader**的配置文件,会自动读取配置,根目录新建**postcss.config.js**:
616 |
617 | ```js
618 | module.exports = {
619 | plugins: ['autoprefixer']
620 | }
621 | ```
622 |
623 | 修改**webpack.base.js**, 取消**postcss-loader**的**options**配置
624 |
625 | ```js
626 | // webpack.base.js
627 | // ...
628 | module.exports = {
629 | // ...
630 | module: {
631 | rules: [
632 | // ...
633 | {
634 | test: /\.(css|less)$/, //匹配 css和less 文件
635 | use: [
636 | 'style-loader',
637 | 'css-loader',
638 | 'postcss-loader',
639 | 'less-loader'
640 | ]
641 | },
642 | ]
643 | },
644 | // ...
645 | }
646 | ```
647 |
648 | 提取**postcss-loader**配置后,再次打包,可以看到依然可以解析**css**, **less**文件, **css3**对应前缀依然存在。
649 |
650 | ### 4.5 babel预设处理js兼容
651 |
652 | 现在**js**不断新增很多方便好用的标准语法来方便开发,甚至还有非标准语法比如装饰器,都极大的提升了代码可读性和开发效率。但前者标准语法很多低版本浏览器不支持,后者非标准语法所有的浏览器都不支持。需要把最新的标准语法转换为低版本语法,把非标准语法转换为标准语法才能让浏览器识别解析,而**babel**就是来做这件事的,这里只讲配置,更详细的可以看[Babel 那些事儿](https://juejin.cn/post/6992371845349507108)。
653 |
654 | 安装依赖
655 |
656 | ```sh
657 | npm i babel-loader @babel/core @babel/preset-env core-js -D
658 | ```
659 |
660 | * babel-loader: 使用 **babel** 加载最新js代码并将其转换为 **ES5**(上面已经安装过)
661 | * @babel/corer: **babel** 编译的核心包
662 | * @babel/preset-env: **babel** 编译的预设,可以转换目前最新的**js**标准语法
663 | * core-js: 使用低版本**js**语法模拟高版本的库,也就是垫片
664 |
665 | 修改**webpack.base.js**
666 |
667 | ```js
668 | // webpack.base.js
669 | module.exports = {
670 | // ...
671 | module: {
672 | rules: [
673 | {
674 | test: /\.ts$/,
675 | use: {
676 | loader: 'babel-loader',
677 | options: {
678 | // 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法
679 | presets: [
680 | [
681 | "@babel/preset-env",
682 | {
683 | // 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc
684 | // "targets": {
685 | // "chrome": 35,
686 | // "ie": 9
687 | // },
688 | "useBuiltIns": "usage", // 根据配置的浏览器兼容,以及代码中使用到的api进行引入polyfill按需添加
689 | "corejs": 3, // 配置使用core-js低版本
690 | }
691 | ],
692 | [
693 | '@babel/preset-typescript',
694 | {
695 | allExtensions: true, //支持所有文件扩展名,很关键
696 | },
697 | ]
698 | ]
699 | }
700 | }
701 | }
702 | ]
703 | }
704 | }
705 | ```
706 |
707 | 此时再打包就会把语法转换为对应浏览器兼容的语法了。
708 |
709 | 为了避免**webpack**配置文件过于庞大,可以把**babel-loader**的配置抽离出来, 新建**babel.config.js**文件,使用**js**作为配置文件,是因为可以访问到**process.env.NODE\_ENV**环境变量来区分是开发还是打包模式。
710 |
711 | ```js
712 | // babel.config.js
713 | module.exports = {
714 | // 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法
715 | "presets": [
716 | [
717 | "@babel/preset-env",
718 | {
719 | // 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc
720 | // "targets": {
721 | // "chrome": 35,
722 | // "ie": 9
723 | // },
724 | "useBuiltIns": "usage", // 根据配置的浏览器兼容,以及代码中使用到的api进行引入polyfill按需添加
725 | "corejs": 3 // 配置使用core-js使用的版本
726 | }
727 | ],
728 | [
729 | "@babel/preset-typescript",
730 | {
731 | allExtensions: true, //支持所有文件扩展名,很关键
732 | },
733 | ],
734 | ]
735 | }
736 | ```
737 |
738 | 移除**webpack.base.js**中**babel-loader**的**options**配置
739 |
740 | ```js
741 | // webpack.base.js
742 | module.exports = {
743 | // ...
744 | module: {
745 | rules: [
746 | {
747 | test: /\.ts$/,
748 | use: 'babel-loader'
749 | },
750 | // 如果使用到了js,可以把js文件配置加上
751 | // {
752 | // test: /.(js)$/,
753 | // use: 'babel-loader'
754 | // }
755 | // ...
756 | ]
757 | }
758 | }
759 | ```
760 |
761 | ### 4.8 复制public文件夹
762 |
763 | 一般**public**文件夹都会放一些静态资源,可以直接根据绝对路径引入,比如**图片**,**css**,**js**文件等,不需要**webpack**进行解析,只需要打包的时候把**public**下内容复制到构建出口文件夹中,可以借助[copy-webpack-plugin](https://www.npmjs.com/package/copy-webpack-plugin)插件,安装依赖
764 |
765 | ```sh
766 | npm i copy-webpack-plugin -D
767 | ```
768 |
769 | 开发环境已经在**devServer**中配置了**static**托管了**public**文件夹,在开发环境使用绝对路径可以访问到**public**下的文件,但打包构建时不做处理会访问不到,所以现在需要在打包配置文件**webpack.prod.js**中新增**copy**插件配置。
770 |
771 | ```js
772 | // webpack.prod.js
773 | // ..
774 | const path = require('path')
775 | const CopyPlugin = require('copy-webpack-plugin');
776 | module.exports = merge(baseConfig, {
777 | mode: 'production',
778 | plugins: [
779 | // 复制文件插件
780 | new CopyPlugin({
781 | patterns: [
782 | {
783 | from: path.resolve(__dirname, '../public'), // 复制public下文件
784 | to: path.resolve(__dirname, '../dist'), // 复制到dist目录中
785 | filter: source => {
786 | return !source.includes('index.html') // 忽略index.html
787 | }
788 | },
789 | ],
790 | }),
791 | ]
792 | })
793 | ```
794 |
795 | 在上面的配置中,忽略了**index.html**,因为**html-webpack-plugin**会以**public**下的**index.html**为模板生成一个**index.html**到**dist**文件下,所以不需要再复制该文件了。
796 |
797 | 测试一下,在**public**中新增一个[**favicon.ico**](https://guojiongwei.top/favicon.ico)图标文件,在**index.html**中引入
798 |
799 | ```html
800 |
801 |
802 |
803 |
804 |
805 |
806 |
807 |
808 | webpack5-vue3-ts
809 |
810 |
811 |
812 |
813 |
814 |
815 | ```
816 |
817 | 再执行**npm run build:dev**打包,就可以看到**public**下的**favicon.ico**图标文件被复制到**dist**文件中了。
818 |
819 | 
820 |
821 | ### 4.7 处理图片文件
822 |
823 | 对于图片文件,**webpack4**使用**file-loader**和**url-loader**来处理的,但**webpack5**不使用这两个**loader**了,而是采用自带的[**asset-module**](https://webpack.js.org/guides/asset-modules/#root)来处理
824 |
825 | 修改**webpack.base.js**,添加图片解析配置
826 |
827 | ```js
828 | module.exports = {
829 | module: {
830 | rules: [
831 | // ...
832 | {
833 | test:/.(png|jpg|jpeg|gif|svg)$/, // 匹配图片文件
834 | type: "asset", // type选择asset
835 | parser: {
836 | dataUrlCondition: {
837 | maxSize: 10 * 1024, // 小于10kb转base64位
838 | }
839 | },
840 | generator:{
841 | filename:'static/images/[name][ext]', // 文件输出目录和命名
842 | },
843 | },
844 | ]
845 | }
846 | }
847 | ```
848 |
849 | 测试一下,准备一张小于[10kb](https://github.com/guojiongwei/webpack5-vue3-ts/blob/main/src/assets/imgs/5kb.png)的图片和大于[10kb](https://github.com/guojiongwei/webpack5-vue3-ts/blob/main/src/assets/imgs/22kb.png)的图片,放在**src/assets/imgs**目录下, 修改**App.vue**:
850 |
851 | ```vue
852 |
853 |
854 |
855 |
856 |
857 |
863 |
864 |
866 | ```
867 |
868 | > 这个时候在引入图片的地方会报:**找不到模块“./assets/imgs/22kb.png”或其相应的类型声明**,需要添加一个图片的声明文件
869 |
870 | 新增**src/images.d.ts**文件,添加内容
871 |
872 | ```js
873 | declare module '*.svg'
874 | declare module '*.png'
875 | declare module '*.jpg'
876 | declare module '*.jpeg'
877 | declare module '*.gif'
878 | declare module '*.bmp'
879 | declare module '*.tiff'
880 | declare module '*.less'
881 | declare module '*.css'
882 | ```
883 |
884 | 添加图片声明文件后,就可以正常引入图片了, 然后执行**npm run build:dev**打包,借助**serve -s dist**查看效果,可以看到可以正常解析图片了,并且小于**10kb**的图片被转成了**base64**位格式的。
885 |
886 | 
887 |
888 | **css**中的背景图片一样也可以解析,修改**app.vue**。
889 |
890 | ```vue
891 |
892 |
893 |
894 |
895 |
896 |
897 |
898 |
899 |
900 |
906 |
907 |
909 | ```
910 |
911 | 修改**app.less**
912 |
913 | ```less
914 | // app.less
915 | #root {
916 | .smallImg {
917 | width: 69px;
918 | height: 75px;
919 | background: url('./assets/imgs/5kb.png') no-repeat;
920 | }
921 | .bigImg {
922 | width: 232px;
923 | height: 154px;
924 | background: url('./assets/imgs/22kb.png') no-repeat;
925 | }
926 | }
927 | ```
928 |
929 | 可以看到背景图片也一样可以识别,小于**10kb**转为**base64**位。
930 |
931 | 
932 |
933 | ### 4.8 处理字体和媒体文件
934 |
935 | 字体文件和媒体文件这两种资源处理方式和处理图片是一样的,只需要把匹配的路径和打包后放置的路径修改一下就可以了。修改**webpack.base.js**文件:
936 |
937 | ```js
938 | // webpack.base.js
939 | module.exports = {
940 | module: {
941 | rules: [
942 | // ...
943 | {
944 | test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件
945 | type: "asset", // type选择asset
946 | parser: {
947 | dataUrlCondition: {
948 | maxSize: 10 * 1024, // 小于10kb转base64位
949 | }
950 | },
951 | generator:{
952 | filename:'static/fonts/[name][ext]', // 文件输出目录和命名
953 | },
954 | },
955 | {
956 | test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
957 | type: "asset", // type选择asset
958 | parser: {
959 | dataUrlCondition: {
960 | maxSize: 10 * 1024, // 小于10kb转base64位
961 | }
962 | },
963 | generator:{
964 | filename:'static/media/[name][ext]', // 文件输出目录和命名
965 | },
966 | },
967 | ]
968 | }
969 | }
970 | ```
971 |
972 | ## 五. 优化构建速度
973 |
974 | ### 5.1 构建耗时分析
975 |
976 | 当进行优化的时候,肯定要先知道时间都花费在哪些步骤上了,而[speed-measure-webpack-plugin](https://link.juejin.cn/?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fspeed-measure-webpack-plugin)插件可以帮我们做到,安装依赖:
977 |
978 | ```sh
979 | npm i speed-measure-webpack-plugin -D
980 | ```
981 |
982 | 使用的时候为了不影响到正常的开发/打包模式,我们选择新建一个配置文件,新增**webpack**构建分析配置文件**build/webpack.analy.js**
983 |
984 | ```js
985 | const prodConfig = require('./webpack.prod.js') // 引入打包配置
986 | const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); // 引入webpack打包速度分析插件
987 | const smp = new SpeedMeasurePlugin(); // 实例化分析插件
988 | const { merge } = require('webpack-merge') // 引入合并webpack配置方法
989 |
990 | // 使用smp.wrap方法,把生产环境配置传进去,由于后面可能会加分析配置,所以先留出合并空位
991 | module.exports = smp.wrap(merge(prodConfig, {
992 |
993 | }))
994 | ```
995 |
996 | 修改**package.json**添加启动**webpack**打包分析脚本命令,在**scripts**新增:
997 |
998 | ```js
999 | {
1000 | // ...
1001 | "scripts": {
1002 | // ...
1003 | "build:analy": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.analy.js"
1004 | }
1005 | // ...
1006 | }
1007 | ```
1008 |
1009 | 执行**npm run build:analy**命令
1010 |
1011 | 
1012 |
1013 | 可以在图中看到各**plugin**和**loader**的耗时时间,现在因为项目内容比较少,所以耗时都比较少,在真正的项目中可以通过这个来分析打包时间花费在什么地方,然后来针对性的优化。
1014 |
1015 | ### 5.2 开启持久化存储缓存
1016 |
1017 | 在**webpack5**之前做缓存是使用**babel-loader**缓存解决**js**的解析结果,**cache-loader**缓存**css**等资源的解析结果,还有模块缓存插件**hard-source-webpack-plugin**,配置好缓存后第二次打包,通过对文件做哈希对比来验证文件前后是否一致,如果一致则采用上一次的缓存,可以极大地节省时间。
1018 |
1019 | **webpack5** 较于 **webpack4**,新增了持久化缓存、改进缓存算法等优化,通过配置 [webpack 持久化缓存](https%3A%2F%2Fwebpack.docschina.org%2Fconfiguration%2Fcache%2F%23root),来缓存生成的 **webpack** 模块和 **chunk**,改善下一次打包的构建速度,可提速 **90%** 左右,配置也简单,修改**webpack.base.js**
1020 |
1021 | ```js
1022 | // webpack.base.js
1023 | // ...
1024 | module.exports = {
1025 | // ...
1026 | cache: {
1027 | type: 'filesystem', // 使用文件缓存
1028 | },
1029 | }
1030 | ```
1031 |
1032 | 当前文章代码的测试结果
1033 |
1034 | | 模式 | 第一次耗时 | 第二次耗时 |
1035 | | ------ | ------ | ----- |
1036 | | 启动开发模式 | 2869毫秒 | 687毫秒 |
1037 | | 启动打包模式 | 5455毫秒 | 552毫秒 |
1038 |
1039 | 通过开启**webpack5**持久化存储缓存,再次打包的时间提升了**90%**。
1040 |
1041 | 
1042 |
1043 | 缓存的存储位置在**node\_modules/.cache/webpack**,里面又区分了**development**和**production**缓存
1044 |
1045 | 
1046 |
1047 | ### 5.3 开启多线程loader
1048 |
1049 | **webpack**的**loader**默认在单线程执行,现代电脑一般都有多核**cpu**,可以借助多核**cpu**开启多线程**loader**解析,可以极大地提升**loader**解析的速度,[thread-loader](https://link.juejin.cn/?target=https%3A%2F%2Fwebpack.docschina.org%2Floaders%2Fthread-loader%2F%23root)就是用来开启多进程解析**loader**的,安装依赖
1050 |
1051 | ```sh
1052 | npm i thread-loader -D
1053 | ```
1054 |
1055 | 使用时,需将此 **loader** 放置在其他 **loader** 之前。放置在此 **loader** 之后的 **loader** 会在一个独立的 **worker** 池中运行。
1056 |
1057 | 修改**webpack.base.js**
1058 |
1059 | ```js
1060 | // webpack.base.js
1061 | module.exports = {
1062 | // ...
1063 | module: {
1064 | rules: [
1065 | {
1066 | test: /\.vue/,
1067 | use: ['thread-loader', 'vue-loader']
1068 | },
1069 | {
1070 | test: /\.ts$/,
1071 | use: ['thread-loader', 'babel-loader']
1072 | }
1073 | ]
1074 | }
1075 | }
1076 | ```
1077 |
1078 | 由于**thread-loader**不支持抽离css插件**MiniCssExtractPlugin.loader**(下面会讲),所以这里只配置了多进程解析**js**,开启多线程也是需要启动时间,大约**500ms**左右,所以适合规模比较大的项目。
1079 |
1080 | ### 5.4 配置alias别名
1081 |
1082 | **webpack**支持设置别名**alias**,设置别名可以让后续引用的地方减少路径的复杂度。
1083 |
1084 | 修改**webpack.base.js**
1085 |
1086 | ```js
1087 | module.export = {
1088 | // ...
1089 | resolve: {
1090 | // ...
1091 | alias: {
1092 | '@': path.join(__dirname, '../src')
1093 | }
1094 | }
1095 | }
1096 | ```
1097 |
1098 | 修改**tsconfig.json**,添加**baseUrl**和**paths**
1099 |
1100 | ```js
1101 | {
1102 | "compilerOptions": {
1103 | // ...
1104 | "baseUrl": ".",
1105 | "paths": {
1106 | "@/*": [
1107 | "src/*"
1108 | ]
1109 | }
1110 | }
1111 | }
1112 | ```
1113 |
1114 | 配置修改完成后,在项目中使用 **@/xxx.xx**,就会指向项目中**src/xxx.xx,**在**js/ts**文件和**vue**文件中都可以用。
1115 |
1116 | **src/App.vue**可以修改为
1117 |
1118 | ```vue
1119 |
1120 |
1121 |
1122 |
1123 |
1124 |
1125 |
1126 |
1127 |
1128 |
1134 |
1135 |
1137 | ```
1138 |
1139 | **src/app.less**可以修改为
1140 |
1141 | ```less
1142 | // app.less
1143 | #root {
1144 | .smallImg {
1145 | width: 69px;
1146 | height: 75px;
1147 | background: url('@/assets/imgs/5kb.png') no-repeat;
1148 | }
1149 | .bigImg {
1150 | width: 232px;
1151 | height: 154px;
1152 | background: url('@/assets/imgs/22kb.png') no-repeat;
1153 | }
1154 | }
1155 | ```
1156 |
1157 | ### 5.5 缩小loader作用范围
1158 |
1159 | 一般第三库都是已经处理好的,不需要再次使用**loader**去解析,可以按照实际情况合理配置**loader**的作用范围,来减少不必要的**loader**解析,节省时间,通过使用 **include**和**exclude** 两个配置项,可以实现这个功能,常见的例如:
1160 |
1161 | * **include**:只解析该选项配置的模块
1162 | * **exclude**:不解该选项配置的模块,优先级更高
1163 |
1164 | 修改**webpack.base.js**
1165 |
1166 | ```js
1167 | // webpack.base.js
1168 | const path = require('path')
1169 | module.exports = {
1170 | // ...
1171 | module: {
1172 | rules: [
1173 | {
1174 | include: [path.resolve(__dirname, '../src')], 只对项目src文件的vue进行loader解析
1175 | test: /\.vue$/,
1176 | use: ['thread-loader', 'vue-loader']
1177 | },
1178 | {
1179 | include: [path.resolve(__dirname, '../src')], 只对项目src文件的ts进行loader解析
1180 | test: /\.ts/,
1181 | use: ['thread-loader', 'babel-loader']
1182 | },
1183 | ]
1184 | }
1185 | }
1186 | ```
1187 |
1188 | 其他**loader**也是相同的配置方式,如果除**src**文件外也还有需要解析的,就把对应的目录地址加上就可以了,比如需要引入**antd**的**css**,可以把**antd**的文件目录路径添加解析**css**规则到**include**里面。
1189 |
1190 | ### 5.6 精确使用loader
1191 |
1192 | **loader**在**webpack**构建过程中使用的位置是在**webpack**构建模块依赖关系引入新文件时,会根据文件后缀来倒序遍历**rules**数组,如果文件后缀和**test**正则匹配到了,就会使用该**rule**中配置的**loader**依次对文件源代码进行处理,最终拿到处理后的**sourceCode**结果,可以通过避免使用无用的**loader**解析来提升构建速度,比如使用**less-loader**解析**css**文件。
1193 |
1194 | 可以拆分上面配置的**less**和**css**, 避免让**less-loader**再去解析**css**文件
1195 |
1196 | ```js
1197 | // webpack.base.js
1198 | // ...
1199 | module.exports = {
1200 | module: {
1201 | // ...
1202 | rules: [
1203 | // ...
1204 | {
1205 | test: /\.css$/, //匹配所有的 css 文件
1206 | include: [path.resolve(__dirname, '../src')],
1207 | use: [
1208 | 'style-loader',
1209 | 'css-loader',
1210 | 'postcss-loader'
1211 | ]
1212 | },
1213 | {
1214 | test: /\.less$/, //匹配所有的 less 文件
1215 | include: [path.resolve(__dirname, '../src')],
1216 | use: [
1217 | 'style-loader',
1218 | 'css-loader',
1219 | 'postcss-loader',
1220 | 'less-loader'
1221 | ]
1222 | },
1223 | ]
1224 | }
1225 | }
1226 | ```
1227 |
1228 | ### 5.7 缩小模块搜索范围
1229 |
1230 | **node**里面模块有三种
1231 |
1232 | * **node**核心模块
1233 | * **node\_modules**模块
1234 | * 自定义文件模块
1235 |
1236 | 使用**require**和**import**引入模块时如果有准确的相对或者绝对路径,就会去按路径查询,如果引入的模块没有路径,会优先查询**node**核心模块,如果没有找到会去当前目录下**node\_modules**中寻找,如果没有找到会查从父级文件夹查找**node\_modules**,一直查到系统**node**全局模块。
1237 |
1238 | 这样会有两个问题,一个是当前项目没有安装某个依赖,但是上一级目录下**node\_modules**或者全局模块有安装,就也会引入成功,但是部署到服务器时可能就会找不到造成报错,另一个问题就是一级一级查询比较消耗时间。可以告诉**webpack**搜索目录范围,来规避这两个问题。
1239 |
1240 | 修改**webpack.base.js**
1241 |
1242 | ```js
1243 | // webpack.base.js
1244 | const path = require('path')
1245 | module.exports = {
1246 | // ...
1247 | resolve: {
1248 | // ...
1249 | // 如果用的是pnpm 就暂时不要配置这个,会有幽灵依赖的问题,访问不到很多模块。
1250 | modules: [path.resolve(__dirname, '../node_modules')], // 查找第三方模块只在本项目的node_modules中查找
1251 | },
1252 | }
1253 | ```
1254 |
1255 | ### 5.8 devtool 配置
1256 |
1257 | 开发过程中或者打包后的代码都是**webpack**处理后的代码,如果进行调试肯定希望看到源代码,而不是编译后的代码, [source map](http://blog.teamtreehouse.com/introduction-source-maps)就是用来做源码映射的,不同的映射模式会明显影响到构建和重新构建的速度, [**devtool**](https://webpack.js.org/configuration/devtool/)选项就是**webpack**提供的选择源码映射方式的配置。
1258 |
1259 | **devtool**的命名规则为 **^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map\$**
1260 |
1261 | | 关键字 | 描述 |
1262 | | --------- | -------------------------------------------- |
1263 | | inline | 代码内通过 dataUrl 形式引入 SourceMap |
1264 | | hidden | 生成 SourceMap 文件,但不使用 |
1265 | | eval | `eval(...)` 形式执行代码,通过 dataUrl 形式引入 SourceMap |
1266 | | nosources | 不生成 SourceMap |
1267 | | cheap | 只需要定位到行信息,不需要列信息 |
1268 | | module | 展示源代码中的错误位置 |
1269 |
1270 | 开发环境推荐:**eval-cheap-module-source-map**
1271 |
1272 | * 本地开发首次打包慢点没关系,因为 **eval** 缓存的原因, 热更新会很快
1273 | * 开发中,我们每行代码不会写的太长,只需要定位到行就行,所以加上 **cheap**
1274 | * 我们希望能够找到源代码的错误,而不是打包后的,所以需要加上 **module**
1275 |
1276 | 修改**webpack.dev.js**
1277 |
1278 | ```js
1279 | // webpack.dev.js
1280 | module.exports = {
1281 | // ...
1282 | devtool: 'eval-cheap-module-source-map'
1283 | }
1284 | ```
1285 |
1286 | 打包环境推荐:**none**(就是不配置**devtool**选项了,不是配置**devtool**: '**none**')
1287 |
1288 | ```js
1289 | // webpack.prod.js
1290 | module.exports = {
1291 | // ...
1292 | // devtool: '', // 不用配置devtool此项
1293 | }
1294 | ```
1295 |
1296 | * **none**话调试只能看到编译后的代码,也不会泄露源代码,打包速度也会比较快。
1297 | * 只是不方便线上排查问题, 但一般都可以根据报错信息在本地环境很快找出问题所在。
1298 |
1299 | ### 5.9 其他优化配置
1300 |
1301 | 除了上面的配置外,**webpack**还提供了其他的一些优化方式,本次搭建没有使用到,所以只简单罗列下
1302 |
1303 | * [**externals**](https://www.webpackjs.com/configuration/externals/): 外包拓展,打包时会忽略配置的依赖,会从上下文中寻找对应变量
1304 | * [**module.noParse**](https://www.webpackjs.com/configuration/module/#module-noparse): 匹配到设置的模块,将不进行依赖解析,适合**jquery**,**boostrap**这类不依赖外部模块的包
1305 | * [**ignorePlugin**](https://webpack.js.org/plugins/ignore-plugin/#root): 可以使用正则忽略一部分文件,常在使用多语言的包时可以把非中文语言包过滤掉
1306 |
1307 | ## 六. 优化构建结果文件
1308 |
1309 | ### 6.1 webpack包分析工具
1310 |
1311 | [webpack-bundle-analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer)是分析**webpack**打包后文件的插件,使用交互式可缩放树形图可视化 **webpack** 输出文件的大小。通过该插件可以对打包后的文件进行观察和分析,可以方便我们对不完美的地方针对性的优化,安装依赖:
1312 |
1313 | ```sh
1314 | npm install webpack-bundle-analyzer -D
1315 | ```
1316 |
1317 | 修改**webpack.analy.js**
1318 |
1319 | ```js
1320 | // webpack.analy.js
1321 | const prodConfig = require('./webpack.prod.js')
1322 | const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
1323 | const smp = new SpeedMeasurePlugin();
1324 | const { merge } = require('webpack-merge')
1325 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') // 引入分析打包结果插件
1326 | module.exports = smp.wrap(merge(prodConfig, {
1327 | plugins: [
1328 | new BundleAnalyzerPlugin() // 配置分析打包结果插件
1329 | ]
1330 | }))
1331 | ```
1332 |
1333 | 配置好后,执行**npm run build:analy**命令,打包完成后浏览器会自动打开窗口,可以看到打包文件的分析结果页面,可以看到各个文件所占的资源大小。
1334 |
1335 | 
1336 |
1337 | ### 6.2 抽取css样式文件
1338 |
1339 | 在开发环境我们希望**css**嵌入在**style**标签里面,方便样式热替换,但打包时我们希望把**css**单独抽离出来,方便配置缓存策略。而插件[mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin)就是来帮我们做这件事的,安装依赖:
1340 |
1341 | ```sh
1342 | npm i mini-css-extract-plugin -D
1343 | ```
1344 |
1345 | 修改**webpack.base.js**, 根据环境变量设置开发环境使用**style-looader**,打包模式抽离**css**
1346 |
1347 | ```js
1348 | // webpack.base.js
1349 | // ...
1350 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
1351 | const isDev = process.env.NODE_ENV === 'development' // 是否是开发模式
1352 | module.exports = {
1353 | // ...
1354 | module: {
1355 | rules: [
1356 | // ...
1357 | {
1358 | test: /.css$/, //匹配所有的 css 文件
1359 | include: [path.resolve(__dirname, '../src')],
1360 | use: [
1361 | // 开发环境使用style-looader,打包模式抽离css
1362 | isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
1363 | 'css-loader',
1364 | 'postcss-loader'
1365 | ]
1366 | },
1367 | {
1368 | test: /.less$/, //匹配所有的 less 文件
1369 | include: [path.resolve(__dirname, '../src')],
1370 | use: [
1371 | // 开发环境使用style-looader,打包模式抽离css
1372 | isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
1373 | 'css-loader',
1374 | 'postcss-loader',
1375 | 'less-loader'
1376 | ]
1377 | },
1378 | ]
1379 | },
1380 | // ...
1381 | }
1382 | ```
1383 |
1384 | 再修改**webpack.prod.js**, 打包时添加抽离css插件
1385 |
1386 | ```js
1387 | // webpack.prod.js
1388 | // ...
1389 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
1390 | module.exports = merge(baseConfig, {
1391 | mode: 'production',
1392 | plugins: [
1393 | // ...
1394 | // 抽离css插件
1395 | new MiniCssExtractPlugin({
1396 | filename: 'static/css/[name].css' // 抽离css的输出目录和名称
1397 | }),
1398 | ]
1399 | })
1400 | ```
1401 |
1402 | 配置完成后,在开发模式**css**会嵌入到**style**标签里面,方便样式热替换,打包时会把**css**抽离成单独的**css**文件。
1403 |
1404 | ### 6.3 压缩css文件
1405 |
1406 | 上面配置了打包时把**css**抽离为单独**css**文件的配置,打开打包后的文件查看,可以看到默认**css**是没有压缩的,需要手动配置一下压缩**css**的插件。
1407 |
1408 | 
1409 |
1410 | 可以借助[css-minimizer-webpack-plugin](https://link.juejin.cn/?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fcss-minimizer-webpack-plugin)来压缩css,安装依赖
1411 |
1412 | ```sh
1413 | npm i css-minimizer-webpack-plugin -D
1414 | ```
1415 |
1416 | 修改**webpack.prod.js**文件, 需要在优化项[optimization](https://webpack.js.org/configuration/optimization/)下的[minimizer](https://webpack.js.org/configuration/optimization/#optimizationminimizer)属性中配置
1417 |
1418 | ```js
1419 | // webpack.prod.js
1420 | // ...
1421 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
1422 | module.exports = {
1423 | // ...
1424 | optimization: {
1425 | minimizer: [
1426 | new CssMinimizerPlugin(), // 压缩css
1427 | ],
1428 | },
1429 | }
1430 | ```
1431 |
1432 | 再次执行打包就可以看到**css**已经被压缩了。
1433 |
1434 | ### 6.4 压缩js文件
1435 |
1436 | 设置**mode**为**production**时,**webpack**会使用内置插件[terser-webpack-plugin](https://www.npmjs.com/package/terser-webpack-plugin)压缩**js**文件,该插件默认支持多线程压缩,但是上面配置**optimization.minimizer**压缩**css**后,**js**压缩就失效了,需要手动再添加一下,**webpack**内部安装了该插件,由于**pnpm**解决了幽灵依赖问题,如果用的**pnpm**的话,需要手动再安装一下依赖。
1437 |
1438 | ```sh
1439 | npm i terser-webpack-plugin -D
1440 | ```
1441 |
1442 | 修改**webpack.prod.js**文件
1443 |
1444 | ```js
1445 | // ...
1446 | const TerserPlugin = require('terser-webpack-plugin')
1447 | module.exports = {
1448 | // ...
1449 | optimization: {
1450 | minimizer: [
1451 | // ...
1452 | new TerserPlugin({ // 压缩js
1453 | parallel: true, // 开启多线程压缩
1454 | terserOptions: {
1455 | compress: {
1456 | pure_funcs: ["console.log"] // 删除console.log
1457 | }
1458 | }
1459 | }),
1460 | ],
1461 | },
1462 | }
1463 | ```
1464 |
1465 | 配置完成后再打包,**css**和**js**就都可以被压缩了。
1466 |
1467 | ### 6.5 合理配置打包文件hash
1468 |
1469 | 项目维护的时候,一般只会修改一部分代码,可以合理配置文件缓存,来提升前端加载页面速度和减少服务器压力,而**hash**就是浏览器缓存策略很重要的一部分。**webpack**打包的**hash**分三种:
1470 |
1471 | * **hash**:跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的**hash**值都会更改,并且全部文件都共用相同的**hash**值
1472 | * **chunkhash**:不同的入口文件进行依赖文件解析、构建对应的**chunk**,生成对应的哈希值,文件本身修改或者依赖文件修改,**chunkhash**值会变化
1473 | * **contenthash**:每个文件自己单独的 **hash** 值,文件的改动只会影响自身的 **hash** 值
1474 |
1475 | **hash**是在输出文件时配置的,格式是**filename: "\[name].\[chunkhash:8]\[ext]"**,**\[xx]** 格式是**webpack**提供的占位符, **:8**是生成**hash**的长度。
1476 |
1477 | | 占位符 | 解释 |
1478 | | ----------- | ------------------ |
1479 | | ext | 文件后缀名 |
1480 | | name | 文件名 |
1481 | | path | 文件相对路径 |
1482 | | folder | 文件所在文件夹 |
1483 | | hash | 每次构建生成的唯一 hash 值 |
1484 | | chunkhash | 根据 chunk 生成 hash 值 |
1485 | | contenthash | 根据文件内容生成hash 值 |
1486 |
1487 | 因为**js**我们在生产环境里会把一些公共库和程序入口文件区分开,单独打包构建,采用**chunkhash**的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响,可以继续使用浏览器缓存,所以**js**适合使用**chunkhash**。
1488 |
1489 | **css**和图片资源媒体资源一般都是单独存在的,可以采用**contenthash**,只有文件本身变化后会生成新**hash**值。
1490 |
1491 | 修改**webpack.base.js**,把**js**输出的文件名称格式加上**chunkhash**,把**css**和图片媒体资源输出格式加上**contenthash**
1492 |
1493 | ```js
1494 | // webpack.base.js
1495 | // ...
1496 | module.exports = {
1497 | // 打包文件出口
1498 | output: {
1499 | filename: 'static/js/[name].[chunkhash:8].js', // // 加上[chunkhash:8]
1500 | // ...
1501 | },
1502 | module: {
1503 | rules: [
1504 | {
1505 | test:/.(png|jpg|jpeg|gif|svg)$/, // 匹配图片文件
1506 | // ...
1507 | generator:{
1508 | filename:'static/images/[name].[contenthash:8][ext]' // 加上[contenthash:8]
1509 | },
1510 | },
1511 | {
1512 | test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体文件
1513 | // ...
1514 | generator:{
1515 | filename:'static/fonts/[name].[contenthash:8][ext]', // 加上[contenthash:8]
1516 | },
1517 | },
1518 | {
1519 | test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
1520 | // ...
1521 | generator:{
1522 | filename:'static/media/[name].[contenthash:8][ext]', // 加上[contenthash:8]
1523 | },
1524 | },
1525 | ]
1526 | },
1527 | // ...
1528 | }
1529 | ```
1530 |
1531 | 再修改**webpack.prod.js**,修改抽离**css**文件名称格式
1532 |
1533 | ```js
1534 | // webpack.prod.js
1535 | // ...
1536 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
1537 | module.exports = merge(baseConfig, {
1538 | mode: 'production',
1539 | plugins: [
1540 | // 抽离css插件
1541 | new MiniCssExtractPlugin({
1542 | filename: 'static/css/[name].[contenthash:8].css' // 加上[contenthash:8]
1543 | }),
1544 | // ...
1545 | ],
1546 | // ...
1547 | })
1548 | ```
1549 |
1550 | 再次打包就可以看到文件后面的**hash**了
1551 |
1552 | ### 6.6 代码分割第三方包和公共模块
1553 |
1554 | 一般第三方包的代码变化频率比较小,可以单独把**node\_modules**中的代码单独打包, 当第三包代码没变化时,对应**chunkhash**值也不会变化,可以有效利用浏览器缓存,还有公共的模块也可以提取出来,避免重复打包加大代码整体体积, **webpack**提供了代码分隔功能, 需要我们手动在优化项[optimization](https://webpack.js.org/configuration/optimization/)中手动配置下代码分隔[splitChunks](https://webpack.js.org/configuration/optimization/#optimizationsplitchunks)规则。
1555 |
1556 | 修改**webpack.prod.js**
1557 |
1558 | ```js
1559 | module.exports = {
1560 | // ...
1561 | optimization: {
1562 | // ...
1563 | splitChunks: { // 分隔代码
1564 | cacheGroups: {
1565 | vendors: { // 提取node_modules代码
1566 | test: /node_modules/, // 只匹配node_modules里面的模块
1567 | name: 'vendors', // 提取文件命名为vendors,js后缀和chunkhash会自动加
1568 | minChunks: 1, // 只要使用一次就提取出来
1569 | chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
1570 | minSize: 0, // 提取代码体积大于0就提取出来
1571 | priority: 1, // 提取优先级为1
1572 | },
1573 | commons: { // 提取页面公共代码
1574 | name: 'commons', // 提取文件命名为commons
1575 | minChunks: 2, // 只要使用两次就提取出来
1576 | chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
1577 | minSize: 0, // 提取代码体积大于0就提取出来
1578 | }
1579 | }
1580 | }
1581 | }
1582 | }
1583 | ```
1584 |
1585 | 配置完成后执行打包,可以看到**node\_modules**里面的模块被抽离到**vendors.6aeb3c4c.js**中,业务代码在**main.f70933df.js**中。
1586 |
1587 | 
1588 |
1589 | 测试一下,此时**verdors.js**的**chunkhash**是**6aeb3c4c.js**,**main.js**文件的chunkhash是**f70933df**,改动一下**App.vue**,再次打包,可以看到下图**main.js**的chunkhash值变化了,但是**vendors.js**的chunkhash还是原先的,这样发版后,浏览器就可以继续使用缓存中的**verdors.ec725ef1.js**,只需要重新请求**main.js**就可以了。
1590 |
1591 | 
1592 |
1593 | ### 6.7 tree-shaking清理未引用js
1594 |
1595 | [Tree Shaking](https://link.juejin.cn/?target=https%3A%2F%2Fwebpack.docschina.org%2Fguides%2Ftree-shaking%2F)的意思就是摇树,伴随着摇树这个动作,树上的枯叶都会被摇晃下来,这里的**tree-shaking**在代码中摇掉的是未使用到的代码,也就是未引用的代码,最早是在**rollup**库中出现的,**webpack**在**2**版本之后也开始支持。模式**mode**为**production**时就会默认开启**tree-shaking**功能以此来标记未引入代码然后移除掉,测试一下。
1596 |
1597 | 在**src/components**目录下新增**Demo1**,**Demo2**两个组件
1598 |
1599 | ```vue
1600 | // src/components/Demo1.vue
1601 |
1602 | 我是Demo1组件
1603 |
1604 |
1605 | // src/components/Demo2.vue
1606 |
1607 | 我是Demo2组件
1608 |
1609 | ```
1610 |
1611 | 再在**src/components**目录下新增**index.ts**, 把**Demo1**和**Demo2**组件引入进来再暴露出去
1612 |
1613 | ```ts
1614 | // src/components/index.ts
1615 | export { default as Demo1 } from './Demo1'
1616 | export { default as Demo2 } from './Demo2'
1617 | ```
1618 |
1619 | 在**App.vue**中引入两个组件,但只使用**Demo1**组件
1620 |
1621 | ```vue
1622 |
1623 |
1624 |
1625 |
1626 |
1627 |
1628 |
1629 | 修改App.vue
1630 |
1631 |
1632 |
1633 |
1634 |
1641 |
1642 |
1644 | ```
1645 |
1646 | 再次执行**npm run build:dev**打包,可以看到在**main.js**中搜索**Demo**,只搜索到了**Demo1**, 代表**Demo2**组件被**tree-shaking**移除掉了。
1647 |
1648 | 
1649 |
1650 | ### 6.8 tree-shaking清理未使用css
1651 |
1652 | **js**中会有未使用到的代码,**css**中也会有未被页面使用到的样式,可以通过[purgecss-webpack-plugin](https://www.npmjs.com/package/purgecss-webpack-plugin)插件打包的时候移除未使用到的**css**样式,这个插件是和[mini-css-extract-plugin](https://www.npmjs.com/package/mini-css-extract-plugin)插件配合使用的,在上面已经安装过,还需要[**glob-all**](https://www.npmjs.com/package/glob-all)来选择要检测哪些文件里面的类名和**id**还有标签名称, 安装依赖:
1653 |
1654 | ```sh
1655 | npm i purgecss-webpack-plugin glob-all -D
1656 | ```
1657 |
1658 | 修改**webpack.prod.js**
1659 |
1660 | ```js
1661 | // webpack.prod.js
1662 | // ...
1663 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
1664 | const globAll = require('glob-all')
1665 | const { PurgeCSSPlugin } = require('purgecss-webpack-plugin')
1666 | module.exports = {
1667 | // ...
1668 | plugins: [
1669 | // 抽离css插件
1670 | new MiniCssExtractPlugin({
1671 | filename: 'static/css/[name].[contenthash:8].css'
1672 | }),
1673 | // 清理无用css
1674 | new PurgeCSSPlugin({
1675 | // 检测src下所有vue文件和public下index.html中使用的类名和id和标签名称
1676 | // 只打包这些文件中用到的样式
1677 | paths: globAll.sync([
1678 | `${path.join(__dirname, '../src')}/**/*.vue`,
1679 | path.join(__dirname, '../public/index.html')
1680 | ]),
1681 | }),
1682 | ]
1683 | }
1684 | ```
1685 |
1686 | 测试一下, 现在**App.vue**中有两个**div**,类名分别是**smallImg**和**bigImg**,当前**app.less**代码为
1687 |
1688 | ```less
1689 | #root {
1690 | .smallImg {
1691 | width: 69px;
1692 | height: 75px;
1693 | background: url('./assets/imgs/5kb.png') no-repeat;
1694 | }
1695 | .bigImg {
1696 | width: 232px;
1697 | height: 154px;
1698 | background: url('./assets/imgs/22kb.png') no-repeat;
1699 | }
1700 | }
1701 | ```
1702 |
1703 | 此时先执行一下打包,查看**main.css**
1704 |
1705 | 
1706 |
1707 | 因为页面中有 **smallImg**和**bigImg**类名,所以打包后的**css**也有,此时修改一下**app.less**中的 **.smallImg**为 **.smallImg1**,后面加一个1,这样 **.smallImg1**就是无用样式了,因为没有页面没有类名为 **.smallImg1**的节点,再打包后查看 **main.css**
1708 |
1709 | 
1710 |
1711 | 可以看到**main.css**已经没有 **.smallImg1**类名的样式了,做到了删除无用**css**的功能。
1712 |
1713 | 但是**purgecss-webpack-plugin**插件不是全能的,由于项目业务代码的复杂,插件不能百分百识别哪些样式用到了,哪些没用到,所以请不要寄希望于它能够百分百完美解决你的问题,这个是不现实的。
1714 |
1715 | 插件本身也提供了一些白名单**safelist**属性,符合配置规则选择器都不会被删除掉,比如使用了组件库[element-plus](https://element-plus.org/zh-CN/), **purgecss-webpack-plugin**插件检测**src**文件下**vue**文件中使用的类名和**id**时,是检测不到在**src**中使用**element-plus**组件的类名的,打包的时候就会把**element-plus**的类名都给过滤掉,可以配置一下安全选择列表,避免删除**element-plus**组件库的前缀**el-**。
1716 |
1717 | ```js
1718 | new PurgeCSSPlugin({
1719 | // ...
1720 | safelist: {
1721 | standard: [/^el-/], // 过滤以el-开头的类名,哪怕没用到也不删除
1722 | }
1723 | })
1724 | ```
1725 |
1726 | ### 6.9 打包时生成gzip文件
1727 |
1728 | 前端代码在浏览器运行,需要从服务器把**html**,**css**,**js**资源下载执行,下载的资源体积越小,页面加载速度就会越快。一般会采用**gzip**压缩,现在大部分浏览器和服务器都支持**gzip**,可以有效减少静态资源文件大小,压缩率在 **70%** 左右。
1729 |
1730 | **nginx**可以配置**gzip: on**来开启压缩,但是只在**nginx**层面开启,会在每次请求资源时都对资源进行压缩,压缩文件会需要时间和占用服务器**cpu**资源,更好的方式是前端在打包的时候直接生成**gzip**资源,服务器接收到请求,可以直接把对应压缩好的**gzip**文件返回给浏览器,节省时间和**cpu**。
1731 |
1732 | **webpack**可以借助[compression-webpack-plugin](https://www.npmjs.com/package/compression-webpack-plugin) 插件在打包时生成 **gzip** 文章,安装依赖
1733 |
1734 | ```sh
1735 | npm i compression-webpack-plugin -D
1736 | ```
1737 |
1738 | 添加配置,修改**webpack.prod.js**
1739 |
1740 | ```js
1741 | // ...
1742 | const CompressionPlugin = require('compression-webpack-plugin')
1743 | module.exports = {
1744 | // ...
1745 | plugins: [
1746 | // ...
1747 | new CompressionPlugin({
1748 | test: /.(js|css)$/, // 只生成css,js压缩文件
1749 | filename: '[path][base].gz', // 文件命名
1750 | algorithm: 'gzip', // 压缩格式,默认是gzip
1751 | test: /.(js|css)$/, // 只生成css,js压缩文件
1752 | threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
1753 | minRatio: 0.8 // 压缩率,默认值是 0.8
1754 | })
1755 | ]
1756 | }
1757 | ```
1758 |
1759 | 配置完成后再打包,可以看到打包后js的目录下多了一个 **.gz** 结尾的文件
1760 |
1761 | 
1762 |
1763 | 因为只有**verdors.js**的大小超过了**10k**, 所以只有它生成了**gzip**压缩文件,借助**serve -s dist**启动**dist**,查看**verdors.js**加载情况
1764 |
1765 | 
1766 |
1767 | 可以看到**verdors.js**的原始大小是**61kb**, 使用**gzip**压缩后的文件只剩下了**23kb**,减少了\*\*70%\*\*左右 的大小,可以极大提升页面初始加载速度,也能减轻服务器压力。
1768 |
1769 | ## 七. 总结
1770 |
1771 | 到目前为止已经使用**webpack5**把**vue3+ts**的基本构建环境配置完成,并且配置比较常见的**优化构建速度**和**构建结果**的配置,完整代码已上传到[webpack5-vue3-ts](https://github.com/guojiongwei/webpack5-vue3-ts.git)
1772 | 。还有细节需要优化,比如把容易改变的配置单独写个**config.js**来配置,输出文件路径封装。这篇文章只是配置,如果想学好**webpack**,还需要学习**webpack**的构建原理以及**loader**和**plugin**的实现机制。
1773 |
1774 | 本文是总结自己在工作中使用**webpack5**搭建**vue3+ts**构建环境中使用到的配置, 肯定也很多没有做好的地方,后续有好的使用技巧和配置也会继续更新记录。
1775 |
1776 | 附上上面安装依赖的版本
1777 |
1778 | ```json
1779 | "dependencies": {
1780 | "vue": "^3.3.4"
1781 | },
1782 | "devDependencies": {
1783 | "@babel/core": "^7.22.1",
1784 | "@babel/preset-typescript": "^7.21.5",
1785 | "autoprefixer": "^10.4.14",
1786 | "babel-loader": "^9.1.2",
1787 | "compression-webpack-plugin": "^10.0.0",
1788 | "copy-webpack-plugin": "^11.0.0",
1789 | "cross-env": "^7.0.3",
1790 | "css-loader": "^6.8.1",
1791 | "css-minimizer-webpack-plugin": "^5.0.1",
1792 | "glob-all": "^3.3.1",
1793 | "html-webpack-plugin": "^5.5.1",
1794 | "less": "^4.1.3",
1795 | "less-loader": "^11.1.2",
1796 | "mini-css-extract-plugin": "^2.7.6",
1797 | "postcss-loader": "^7.3.2",
1798 | "purgecss-webpack-plugin": "^5.0.0",
1799 | "speed-measure-webpack-plugin": "^1.5.0",
1800 | "style-loader": "^3.3.3",
1801 | "terser-webpack-plugin": "^5.3.9",
1802 | "thread-loader": "^4.0.2",
1803 | "vue-loader": "^17.2.2",
1804 | "webpack": "^5.85.1",
1805 | "webpack-bundle-analyzer": "^4.9.0",
1806 | "webpack-cli": "^5.1.3",
1807 | "webpack-dev-server": "^4.15.0",
1808 | "webpack-merge": "^5.9.0"
1809 | }
1810 | ```
1811 |
1812 | ## 参考
1813 |
1814 | 1. [webpack官网](https://www.webpackjs.com/)
1815 | 2. [babel官网](https://www.babeljs.cn/)
1816 | 3. [【万字】透过分析 webpack 面试题,构建 webpack5.x 知识体系](https://juejin.cn/post/7023242274876162084)
1817 | 4. [Babel 那些事儿](https://juejin.cn/post/6992371845349507108)
1818 | 5. [阔别两年,webpack 5 正式发布了!](https://juejin.cn/post/6882663278712094727)
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | // babel.config.js
2 | module.exports = {
3 | // 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法
4 | "presets": [
5 | [
6 | "@babel/preset-env",
7 | {
8 | // 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc
9 | // "targets": {
10 | // "chrome": 35,
11 | // "ie": 9
12 | // },
13 | "useBuiltIns": "usage", // 根据配置的浏览器兼容,以及代码中使用到的api进行引入polyfill按需添加
14 | "corejs": 3 // 配置使用core-js使用的版本
15 | }
16 | ],
17 | [
18 | "@babel/preset-typescript",
19 | {
20 | allExtensions: true, //支持所有文件扩展名,很关键
21 | },
22 | ],
23 | ]
24 | }
--------------------------------------------------------------------------------
/build/webpack.analy.js:
--------------------------------------------------------------------------------
1 | // webpack.analy.js
2 | const prodConfig = require('./webpack.prod.js')
3 | const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
4 | const smp = new SpeedMeasurePlugin();
5 | const { merge } = require('webpack-merge')
6 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') // 引入分析打包结果插件
7 | module.exports = smp.wrap(merge(prodConfig, {
8 | plugins: [
9 | // new BundleAnalyzerPlugin() // 配置分析打包结果插件
10 | ]
11 | }))
--------------------------------------------------------------------------------
/build/webpack.base.js:
--------------------------------------------------------------------------------
1 | // webpack.base.js
2 |
3 | const path = require('path')
4 | const HtmlWebpackPlugin = require('html-webpack-plugin')
5 | const { VueLoaderPlugin } = require('vue-loader')
6 | const webpack = require('webpack')
7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
8 | const isDev = process.env.NODE_ENV === 'development' // 是否是开发模式
9 |
10 | module.exports = {
11 | entry: path.join(__dirname, '../src/index.ts'), // 入口文件
12 | // 打包文件出口
13 | output: {
14 | filename: 'static/js/[name].[chunkhash:8].js', // 每个输出js的名称
15 | path: path.join(__dirname, '../dist'), // 打包结果输出路径
16 | clean: true, // webpack4需要配置clean-webpack-plugin来删除dist文件,webpack5内置了
17 | publicPath: '/' // 打包后文件的公共前缀路径
18 | },
19 | module: {
20 | rules: [
21 | {
22 | test: /\.vue/,
23 | use: ['thread-loader', 'vue-loader']
24 | },
25 | {
26 | test: /\.ts$/,
27 | use: ['thread-loader', 'babel-loader']
28 | },
29 | {
30 | test: /\.css$/, //匹配所有的 css 文件
31 | include: [path.resolve(__dirname, '../src')],
32 | use: [
33 | // 开发环境使用style-looader,打包模式抽离css
34 | isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
35 | 'css-loader',
36 | 'postcss-loader'
37 | ],
38 | },
39 | {
40 | test: /\.less$/, //匹配所有的 less 文件
41 | include: [path.resolve(__dirname, '../src')],
42 | use: [
43 | // 开发环境使用style-looader,打包模式抽离css
44 | isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
45 | 'css-loader',
46 | 'postcss-loader',
47 | 'less-loader'
48 | ]
49 | },
50 | {
51 | test:/.(png|jpg|jpeg|gif|svg)$/, // 匹配图片文件
52 | type: "asset", // type选择asset
53 | parser: {
54 | dataUrlCondition: {
55 | maxSize: 10 * 1024, // 小于10kb转base64位
56 | }
57 | },
58 | generator:{
59 | filename:'static/images/[name].[contenthash:8][ext]', // 文件输出目录和命名
60 | },
61 | },
62 | {
63 | test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件
64 | type: "asset", // type选择asset
65 | parser: {
66 | dataUrlCondition: {
67 | maxSize: 10 * 1024, // 小于10kb转base64位
68 | }
69 | },
70 | generator:{
71 | filename:'static/fonts/[name].[contenthash:8][ext]', // 文件输出目录和命名
72 | },
73 | },
74 | {
75 | test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
76 | type: "asset", // type选择asset
77 | parser: {
78 | dataUrlCondition: {
79 | maxSize: 10 * 1024, // 小于10kb转base64位
80 | }
81 | },
82 | generator:{
83 | filename:'static/media/[name].[contenthash:8][ext]', // 文件输出目录和命名
84 | },
85 | },
86 | ]
87 | },
88 | resolve: {
89 | extensions: ['.vue', '.ts', '.js', '.json'],
90 | alias: {
91 | '@': path.join(__dirname, '../src')
92 | }
93 | },
94 | plugins: [
95 | new HtmlWebpackPlugin({
96 | template: path.resolve(__dirname, '../public/index.html'), // 模板取定义root节点的模板
97 | inject: true, // 自动注入静态资源
98 | }),
99 | new VueLoaderPlugin(),
100 | new webpack.DefinePlugin({
101 | 'process.env.BASE_ENV': JSON.stringify(process.env.BASE_ENV),
102 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
103 | })
104 | ],
105 | cache: {
106 | type: 'filesystem', // 使用文件缓存
107 | },
108 | }
--------------------------------------------------------------------------------
/build/webpack.dev.js:
--------------------------------------------------------------------------------
1 | // webpack.dev.js
2 | const path = require('path')
3 | const { merge } = require('webpack-merge')
4 | const baseConfig = require('./webpack.base.js')
5 |
6 | // 合并公共配置,并添加开发环境配置
7 | module.exports = merge(baseConfig, {
8 | mode: 'development', // 开发模式,打包更加快速,省了代码优化步骤
9 | devtool: 'eval-cheap-module-source-map', // 源码调试模式,后面会讲
10 | devServer: {
11 | port: 3000, // 服务端口号
12 | compress: false, // gzip压缩,开发环境不开启,提升热更新速度
13 | hot: true, // 开启热更新,后面会讲vue3模块热替换具体配置
14 | historyApiFallback: true, // 解决history路由404问题
15 | static: {
16 | directory: path.join(__dirname, "../public"), //托管静态资源public文件夹
17 | }
18 | }
19 | })
--------------------------------------------------------------------------------
/build/webpack.prod.js:
--------------------------------------------------------------------------------
1 | // webpack.prod.js
2 | const { merge } = require('webpack-merge')
3 | const baseConfig = require('./webpack.base.js')
4 | const path = require('path')
5 | const CopyPlugin = require('copy-webpack-plugin');
6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
7 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
8 | const TerserPlugin = require('terser-webpack-plugin')
9 | const globAll = require('glob-all')
10 | const { PurgeCSSPlugin } = require('purgecss-webpack-plugin')
11 | const CompressionPlugin = require('compression-webpack-plugin')
12 |
13 | module.exports = merge(baseConfig, {
14 | mode: 'production', // 生产模式,会开启tree-shaking和压缩代码,以及其他优化
15 | plugins: [
16 | new CopyPlugin({
17 | patterns: [
18 | {
19 | from: path.resolve(__dirname, '../public'), // 复制public下文件
20 | to: path.resolve(__dirname, '../dist'), // 复制到dist目录中
21 | filter: source => {
22 | return !source.includes('index.html') // 忽略index.html
23 | }
24 | },
25 | ],
26 | }),
27 | new MiniCssExtractPlugin({
28 | filename: 'static/css/[name].[contenthash:8].css' // 抽离css的输出目录和名称
29 | }),
30 | new CompressionPlugin({
31 | test: /\.(js|css)$/, // 只生成css,js压缩文件
32 | filename: '[path][base].gz', // 文件命名
33 | algorithm: 'gzip', // 压缩格式,默认是gzip
34 | test: /\.(js|css)$/, // 只生成css,js压缩文件
35 | threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
36 | minRatio: 0.8 // 压缩率,默认值是 0.8
37 | })
38 | ],
39 | optimization: {
40 | minimizer: [
41 | new CssMinimizerPlugin(), // 压缩css
42 | new TerserPlugin({ // 压缩js
43 | parallel: true, // 开启多线程压缩
44 | terserOptions: {
45 | compress: {
46 | pure_funcs: ["console.log"] // 删除console.log
47 | }
48 | }
49 | }),
50 | // 清理无用css
51 | new PurgeCSSPlugin({
52 | // 检测src下所有vue文件和public下index.html中使用的类名和id和标签名称
53 | // 只打包这些文件中用到的样式
54 | paths: globAll.sync([
55 | `${path.join(__dirname, '../src')}/**/*.vue`,
56 | path.join(__dirname, '../public/index.html')
57 | ]),
58 | }),
59 | ],
60 | splitChunks: { // 分隔代码
61 | cacheGroups: {
62 | vendors: { // 提取node_modules代码
63 | test: /node_modules/, // 只匹配node_modules里面的模块
64 | name: 'vendors', // 提取文件命名为vendors,js后缀和chunkhash会自动加
65 | minChunks: 1, // 只要使用一次就提取出来
66 | chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
67 | minSize: 0, // 提取代码体积大于0就提取出来
68 | priority: 1, // 提取优先级为1
69 | },
70 | commons: { // 提取页面公共代码
71 | name: 'commons', // 提取文件命名为commons
72 | minChunks: 2, // 只要使用两次就提取出来
73 | chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
74 | minSize: 0, // 提取代码体积大于0就提取出来
75 | }
76 | }
77 | }
78 | },
79 | })
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webpack5-vue3-18",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev:dev": "cross-env NODE_ENV=development BASE_ENV=development webpack-dev-server -c build/webpack.dev.js",
8 | "dev:test": "cross-env NODE_ENV=development BASE_ENV=test webpack-dev-server -c build/webpack.dev.js",
9 | "dev:pre": "cross-env NODE_ENV=development BASE_ENV=pre webpack-dev-server -c build/webpack.dev.js",
10 | "dev:prod": "cross-env NODE_ENV=development BASE_ENV=production webpack-dev-server -c build/webpack.dev.js",
11 | "build:dev": "cross-env NODE_ENV=production BASE_ENV=development webpack -c build/webpack.prod.js",
12 | "build:test": "cross-env NODE_ENV=production BASE_ENV=test webpack -c build/webpack.prod.js",
13 | "build:pre": "cross-env NODE_ENV=production BASE_ENV=pre webpack -c build/webpack.prod.js",
14 | "build:prod": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.prod.js",
15 | "build:analy": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.analy.js"
16 | },
17 | "keywords": [],
18 | "author": "",
19 | "license": "ISC",
20 | "dependencies": {
21 | "vue": "^3.3.4"
22 | },
23 | "devDependencies": {
24 | "@babel/core": "^7.22.5",
25 | "@babel/preset-env": "^7.22.5",
26 | "@babel/preset-typescript": "^7.22.5",
27 | "autoprefixer": "^10.4.14",
28 | "babel-loader": "^9.1.2",
29 | "compression-webpack-plugin": "^10.0.0",
30 | "copy-webpack-plugin": "^11.0.0",
31 | "cross-env": "^7.0.3",
32 | "css-loader": "^6.8.1",
33 | "css-minimizer-webpack-plugin": "^5.0.1",
34 | "glob-all": "^3.3.1",
35 | "html-webpack-plugin": "^5.5.1",
36 | "less": "^4.1.3",
37 | "less-loader": "^11.1.2",
38 | "mini-css-extract-plugin": "^2.7.6",
39 | "postcss-loader": "^7.3.2",
40 | "purgecss-webpack-plugin": "^5.0.0",
41 | "speed-measure-webpack-plugin": "^1.5.0",
42 | "style-loader": "^3.3.3",
43 | "terser-webpack-plugin": "^5.3.9",
44 | "thread-loader": "^4.0.2",
45 | "vue-loader": "^17.2.2",
46 | "webpack": "^5.85.1",
47 | "webpack-bundle-analyzer": "^4.9.0",
48 | "webpack-cli": "^5.1.3",
49 | "webpack-dev-server": "^4.15.0",
50 | "webpack-merge": "^5.9.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ['autoprefixer']
3 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guojiongwei/webpack5-vue3-ts/aa64e8095ba73f0e02b8cdb3d6178c83855ac0c5/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | webpack5-vue3-ts
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 修改App.vue
9 |
10 |
11 |
12 |
13 |
20 |
21 |
23 |
24 |
--------------------------------------------------------------------------------
/src/app.css:
--------------------------------------------------------------------------------
1 | h2 {
2 | color: red;
3 | transform: translateY(100px);
4 | }
--------------------------------------------------------------------------------
/src/app.less:
--------------------------------------------------------------------------------
1 | // app.less
2 | #root {
3 | .smallImg {
4 | width: 69px;
5 | height: 75px;
6 | background: url('@/assets/imgs/5kb.png') no-repeat;
7 | }
8 | .bigImg1 {
9 | width: 232px;
10 | height: 154px;
11 | background: url('@/assets/imgs/22kb.png') no-repeat;
12 | }
13 | }
--------------------------------------------------------------------------------
/src/assets/imgs/22kb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guojiongwei/webpack5-vue3-ts/aa64e8095ba73f0e02b8cdb3d6178c83855ac0c5/src/assets/imgs/22kb.png
--------------------------------------------------------------------------------
/src/assets/imgs/5kb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guojiongwei/webpack5-vue3-ts/aa64e8095ba73f0e02b8cdb3d6178c83855ac0c5/src/assets/imgs/5kb.png
--------------------------------------------------------------------------------
/src/components/Demo1.vue:
--------------------------------------------------------------------------------
1 |
2 | 我是Demo1组件
3 |
--------------------------------------------------------------------------------
/src/components/Demo2.vue:
--------------------------------------------------------------------------------
1 |
2 | 我是Demo2组件
3 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Demo1 } from './Demo1.vue'
2 | export { default as Demo2 } from './Demo2.vue'
--------------------------------------------------------------------------------
/src/images.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg'
2 | declare module '*.png'
3 | declare module '*.jpg'
4 | declare module '*.jpeg'
5 | declare module '*.gif'
6 | declare module '*.bmp'
7 | declare module '*.tiff'
8 | declare module '*.less'
9 | declare module '*.css'
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.vue'
3 |
4 | createApp(App).mount('#root')
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "preserve",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true,
22 |
23 | "baseUrl": ".",
24 | "paths": {
25 | "@/*": [
26 | "src/*"
27 | ]
28 | }
29 | },
30 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
31 | }
32 |
--------------------------------------------------------------------------------