├── .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 | ![xmind.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3b88fa105a2f4548bd0fd683aad9d208~tplv-k3u1fbpfcp-watermark.image?) 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 | 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 | 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 | ![1.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6a1fa1f5a4854af1a9e511685075516a~tplv-k3u1fbpfcp-watermark.image?) 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 | ![2.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b66c45a5b0fd45fd9ffa219a965f8ce5~tplv-k3u1fbpfcp-watermark.image?) 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 | ![3.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/303cf8ce1a5145a29502ab715b3055a9~tplv-k3u1fbpfcp-watermark.image?) 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 | ![4.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2fc4609967d242fb9606004fd47bd971~tplv-k3u1fbpfcp-watermark.image?) 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 | 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 | ![5.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bf479c32d9954bdfbeb70efbe40ad771~tplv-k3u1fbpfcp-watermark.image?) 887 | 888 | **css**中的背景图片一样也可以解析,修改**app.vue**。 889 | 890 | ```vue 891 | 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 | ![6.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf92a2d51bb1455d834bc42de187dea9~tplv-k3u1fbpfcp-watermark.image?) 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 | ![6.1.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1afc736a148a49a99d854a723c54148a~tplv-k3u1fbpfcp-watermark.image?) 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 | ![7.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6365790dba09490cbf5aa48cc8df4704~tplv-k3u1fbpfcp-watermark.image?) 1042 | 1043 | 缓存的存储位置在**node\_modules/.cache/webpack**,里面又区分了**development**和**production**缓存 1044 | 1045 | ![8转存失败,建议直接上传图片文件](/Users/guojiongwei/Desktop/md文章/webpack5-vue3-ts/8.png) 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 | 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 | ![9.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1b013a30d57d4b09ba2ddbc3fd30a9eb~tplv-k3u1fbpfcp-watermark.image?) 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 | ![10.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2beb58e8f23f4468a6b9287b72bed0c7~tplv-k3u1fbpfcp-watermark.image?) 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 | ![11.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1e9e46701db34ddf986c33f6581c4c77~tplv-k3u1fbpfcp-watermark.image?) 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 | ![12.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d1da22ad5a5049dca5cd1936fb33e254~tplv-k3u1fbpfcp-watermark.image?) 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 | 1604 | 1605 | // src/components/Demo2.vue 1606 | 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 | 1633 | 1634 | 1641 | 1642 | 1644 | ``` 1645 | 1646 | 再次执行**npm run build:dev**打包,可以看到在**main.js**中搜索**Demo**,只搜索到了**Demo1**, 代表**Demo2**组件被**tree-shaking**移除掉了。 1647 | 1648 | ![13.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b30871d4fb944d4aa0f6a0dda554dafb~tplv-k3u1fbpfcp-watermark.image?) 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 | ![14.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7838458f7e824fe2b34e2eddbe51d3dd~tplv-k3u1fbpfcp-watermark.image?) 1706 | 1707 | 因为页面中有 **smallImg**和**bigImg**类名,所以打包后的**css**也有,此时修改一下**app.less**中的 **.smallImg**为 **.smallImg1**,后面加一个1,这样 **.smallImg1**就是无用样式了,因为没有页面没有类名为 **.smallImg1**的节点,再打包后查看 **main.css** 1708 | 1709 | ![微信截图\_20220617141901.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fbcd882dc21944d58a3e30bf1816efea~tplv-k3u1fbpfcp-watermark.image?) 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 | ![15.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ce0dd7ab80d14f2190740c8b231344c0~tplv-k3u1fbpfcp-watermark.image?) 1762 | 1763 | 因为只有**verdors.js**的大小超过了**10k**, 所以只有它生成了**gzip**压缩文件,借助**serve -s dist**启动**dist**,查看**verdors.js**加载情况 1764 | 1765 | ![16.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/17a7f30cb3574f4dbc8971ac64035e29~tplv-k3u1fbpfcp-watermark.image?) 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 | 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 | -------------------------------------------------------------------------------- /src/components/Demo2.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------