├── .browserslistrc ├── postcss.config.js ├── src ├── app.css ├── assets │ └── imgs │ │ ├── 22kb.png │ │ └── 5kb.png ├── components │ ├── Demo1.tsx │ ├── Demo2.tsx │ ├── LazyDemo.tsx │ ├── PreloadDemo.tsx │ ├── PreFetchDemo.tsx │ ├── index.ts │ └── Class.tsx ├── index.tsx ├── images.d.ts ├── app.less └── App.tsx ├── public ├── favicon.ico └── index.html ├── .gitignore ├── tsconfig.json ├── babel.config.js ├── package.json └── README.md /.browserslistrc: -------------------------------------------------------------------------------- 1 | IE 9 # 兼容IE 9 2 | chrome 35 # 兼容chrome 35 -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['autoprefixer'] 3 | } -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | h2 { 2 | color: red; 3 | transform: translateY(100px); 4 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guojiongwei/webpack5-react-ts/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/imgs/22kb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guojiongwei/webpack5-react-ts/HEAD/src/assets/imgs/22kb.png -------------------------------------------------------------------------------- /src/assets/imgs/5kb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guojiongwei/webpack5-react-ts/HEAD/src/assets/imgs/5kb.png -------------------------------------------------------------------------------- /src/components/Demo1.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Demo1() { 4 | return

我是Demo1组件

5 | } 6 | 7 | export default Demo1 -------------------------------------------------------------------------------- /src/components/Demo2.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Demo2() { 4 | return

我是Demo2组件

5 | } 6 | 7 | export default Demo2 -------------------------------------------------------------------------------- /src/components/LazyDemo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function LazyDemo() { 4 | return

我是懒加载组件组件

5 | } 6 | 7 | export default LazyDemo -------------------------------------------------------------------------------- /src/components/PreloadDemo.tsx: -------------------------------------------------------------------------------- 1 | // src/components/PreloadDemo.tsx 2 | import React from "react"; 3 | function PreloadDemo() { 4 | return

我是PreloadDemo组件

5 | } 6 | export default PreloadDemo -------------------------------------------------------------------------------- /src/components/PreFetchDemo.tsx: -------------------------------------------------------------------------------- 1 | // src/components/PreFetchDemo.tsx 2 | import React from "react"; 3 | function PreFetchDemo() { 4 | return

我是PreFetchDemo组件

5 | } 6 | export default PreFetchDemo -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App'; 4 | 5 | const root = document.getElementById('root'); 6 | if(root) { 7 | createRoot(root).render() 8 | } -------------------------------------------------------------------------------- /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/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Demo1 } from './Demo1' 2 | export { default as Demo2 } from './Demo2' 3 | 4 | 5 | 6 | declare global { 7 | interface Window { 8 | stringify: () => string 9 | } 10 | } 11 | window.stringify = function() { 12 | return JSON.stringify(this) 13 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | webpack5-react-ts 9 | 10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /src/components/Class.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | 3 | // 装饰器为,组件添加age属性 4 | function addAge(Target: Function) { 5 | Target.prototype.age = 111 6 | } 7 | // 使用装饰圈 8 | @addAge 9 | class Class extends PureComponent { 10 | 11 | age?: number 12 | aaaaaaaaa?: number 13 | 14 | render() { 15 | return ( 16 | <> 17 |

我是类1组件---{this.age}----{this.aaaaaaaaa}

18 | 19 | 20 | ) 21 | } 22 | } 23 | Class.prototype.aaaaaaaaa = 1111 24 | export default Class -------------------------------------------------------------------------------- /src/app.less: -------------------------------------------------------------------------------- 1 | #root { 2 | color: red; 3 | h2 { 4 | color: blue; 5 | } 6 | .smallImg1 { 7 | width: 69px; 8 | height: 75px; 9 | background: url('./assets/imgs/5kb.png') no-repeat; 10 | } 11 | .smallImg { 12 | width: 69px; 13 | height: 75px; 14 | background: url('./assets/imgs/5kb.png') no-repeat; 15 | } 16 | .bigImg { 17 | width: 232px; 18 | height: 154px; 19 | background: url('./assets/imgs/22kb.png') no-repeat; 20 | } 21 | } 22 | .ant-aaaa { 23 | color: red; 24 | } 25 | .p-demo { 26 | color: yellow; 27 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "allowJs": false, 8 | "skipLibCheck": true, 9 | "esModuleInterop": false, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "ESNext", 14 | "moduleResolution": "Node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react", 19 | "baseUrl": ".", 20 | "paths": { 21 | "@/*": [ 22 | "src/*" 23 | ] 24 | } 25 | }, 26 | "include": ["./src/**/*.ts", "./src/**/*.tsx"] 27 | } 28 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const isDev = process.env.NODE_ENV === 'development'; 2 | 3 | module.exports = { 4 | // 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法 5 | "presets": [ 6 | [ 7 | "@babel/preset-env", 8 | { 9 | // 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc 10 | // "targets": { 11 | // "chrome": 35, 12 | // "ie": 9 13 | // }, 14 | "useBuiltIns": "usage", // 根据配置的浏览器兼容,以及代码中使用到的api进行引入polyfill按需添加 15 | "corejs": 3 // 配置使用core-js低版本 16 | } 17 | ], 18 | "@babel/preset-react", 19 | "@babel/preset-typescript" 20 | ], 21 | "plugins": [ 22 | isDev && require.resolve('react-refresh/babel'), // 配置react开发环境热替换 23 | [ 24 | "@babel/plugin-proposal-decorators", { "legacy": true } 25 | ] 26 | ].filter(Boolean) 27 | } 28 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense, useState } from 'react' 2 | import smallImg from './assets/imgs/5kb.png' 3 | import bigImg from './assets/imgs/22kb.png' 4 | import Class from './components/Class' 5 | import './app.less' 6 | 7 | // prefetch 8 | const PreFetchDemo = lazy(() => import( 9 | /* webpackChunkName: "PreFetchDemo" */ 10 | /*webpackPrefetch: true*/ 11 | '@/components/PreFetchDemo' 12 | )) 13 | // preload 14 | const PreloadDemo = lazy(() => import( 15 | /* webpackChunkName: "PreloadDemo" */ 16 | /*webpackPreload: true*/ 17 | '@/components/PreloadDemo' 18 | )) 19 | 20 | function App() { 21 | const [ show, setShow ] = useState(false) 22 | 23 | const onClickSSSSSSS = () => { 24 | setShow(true) 25 | } 26 | return ( 27 | <> 28 |

展示

29 | 30 | {/* show为true时加载组件 */} 31 | { show && ( 32 | <> 33 | 小于10kb的图片 34 | 大于于10kb的图片 35 |
{/* 小图片背景容器 */} 36 |
{/* 大图片背景容器 */} 37 | 38 | 39 | 40 | ) } 41 | 42 | ) 43 | } 44 | export default App -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack5-react-ts", 3 | "version": "1.0.0", 4 | "description": "使用webpack5从零搭建完整的react18+ts构建环境", 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 | "analy": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.analy.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/guojiongwei/webpack5-react-ts.git" 20 | }, 21 | "keywords": [], 22 | "author": "", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/guojiongwei/webpack5-react-ts/issues" 26 | }, 27 | "homepage": "https://github.com/guojiongwei/webpack5-react-ts#readme", 28 | "devDependencies": { 29 | "@babel/core": "^7.18.2", 30 | "@babel/plugin-proposal-decorators": "^7.18.2", 31 | "@babel/plugin-transform-runtime": "^7.18.5", 32 | "@babel/preset-env": "^7.18.2", 33 | "@babel/preset-react": "^7.17.12", 34 | "@babel/preset-typescript": "^7.17.12", 35 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", 36 | "@types/react": "^18.0.12", 37 | "@types/react-dom": "^18.0.5", 38 | "autoprefixer": "^10.4.7", 39 | "babel-loader": "^8.2.5", 40 | "compression-webpack-plugin": "^10.0.0", 41 | "copy-webpack-plugin": "^11.0.0", 42 | "core-js": "^3.23.0", 43 | "cross-env": "^7.0.3", 44 | "css-loader": "^6.7.1", 45 | "css-minimizer-webpack-plugin": "^4.0.0", 46 | "glob-all": "^3.3.0", 47 | "html-webpack-plugin": "^5.5.0", 48 | "less": "^4.1.3", 49 | "less-loader": "^11.0.0", 50 | "mini-css-extract-plugin": "^2.6.1", 51 | "postcss": "^8.4.14", 52 | "postcss-loader": "^7.0.0", 53 | "purgecss-webpack-plugin": "^4.1.3", 54 | "react-refresh": "^0.14.0", 55 | "speed-measure-webpack-plugin": "^1.5.0", 56 | "style-loader": "^3.3.1", 57 | "thread-loader": "^3.0.4", 58 | "typescript": "^4.7.3", 59 | "vue-loader": "^17.0.0", 60 | "webpack": "^5.73.0", 61 | "webpack-bundle-analyzer": "^4.5.0", 62 | "webpack-cli": "^4.9.2", 63 | "webpack-dev-server": "^4.9.1", 64 | "webpack-merge": "^5.8.0" 65 | }, 66 | "dependencies": { 67 | "react": "^18.1.0", 68 | "react-dom": "^18.1.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 【前端工程化】webpack5从零搭建完整的react18+ts开发和打包环境 2 | ## 目录 3 | 4 | 1. 前言 5 | 2. 初始化项目 6 | 3. 配置基础版**react**+**ts**环境 7 | 4. 常用功能配置 8 | 5. 配置**react**模块热替换 9 | 6. 优化构建速度 10 | 7. 优化构建结果文件 11 | 8. 总结 12 | 13 | 14 | **全文概览** 15 | 16 | ![webpack5+react+ts1.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/51f79254001f4b959c1586c7c6373317~tplv-k3u1fbpfcp-watermark.image?) 17 | 18 | ## 一. 前言 19 | 20 | 从**2020**年**10**月**10**日,**webpack** 升级至 5 版本到现在已经快两年,**webpack5**版本优化了很多原有的功能比如**tree-shakin**g优化,也新增了很多新特性,比如联邦模块,具体变动可以看这篇文章[阔别两年,webpack 5 正式发布了!](https://juejin.cn/post/6882663278712094727)。 21 | 22 | 本文将使用最新的**webpack5**一步一步从零搭建一个完整的**react18+ts**开发和打包环境,配置完善的模块热替换以及**构建速度**和**构建结果**的优化,完整代码已上传到[webpack5-react-ts](https://github.com/guojiongwei/webpack5-react-ts.git)。本文只是配置**webpack**的,配置代码规范相关的可以看这篇文章[搭建react18+vite2+ts+prettier+eslint+lint-staged+husky+stylelint开发环境](https://juejin.cn/post/7101596844181962788) 23 | 24 | ## 二. 初始化项目 25 | 26 | 在开始**webpack**配置之前,先手动初始化一个基本的**react**+**ts**项目,新建项目文件夹**webpack5-react-18**, 在项目下执行 27 | 28 | ```bash 29 | npm init -y 30 | ``` 31 | 32 | 初始化好**package.json**后,在项目下新增以下所示目录结构和文件 33 | 34 | ```yaml 35 | ├── build 36 | | ├── webpack.base.js # 公共配置 37 | | ├── webpack.dev.js # 开发环境配置 38 | | └── webpack.prod.js # 打包环境配置 39 | ├── public 40 | │ └── index.html # html模板 41 | ├── src 42 | | ├── App.tsx 43 | │ └── index.tsx # react应用入口页面 44 | ├── tsconfig.json # ts配置 45 | └── package.json 46 | ``` 47 | 48 | 安装**webpack**依赖 49 | 50 | ```sh 51 | npm i webpack webpack-cli -D 52 | ``` 53 | 54 | 安装**react**依赖 55 | 56 | ```sh 57 | npm i react react-dom -S 58 | ``` 59 | 安装**react**类型依赖 60 | 61 | ```sh 62 | npm i @types/react @types/react-dom -D 63 | ``` 64 | 65 | 添加**public/index.html**内容 66 | 67 | ```html 68 | 69 | 70 | 71 | 72 | 73 | 74 | webpack5-react-ts 75 | 76 | 77 | 78 |
79 | 80 | 81 | ``` 82 | 83 | 添加**tsconfig.json**内容 84 | 85 | ```json 86 | { 87 | "compilerOptions": { 88 | "target": "ESNext", 89 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 90 | "allowJs": false, 91 | "skipLibCheck": false, 92 | "esModuleInterop": false, 93 | "allowSyntheticDefaultImports": true, 94 | "strict": true, 95 | "forceConsistentCasingInFileNames": true, 96 | "module": "ESNext", 97 | "moduleResolution": "Node", 98 | "resolveJsonModule": true, 99 | "isolatedModules": true, 100 | "noEmit": true, 101 | "jsx": "react", // react18这里也可以改成react-jsx 102 | }, 103 | "include": ["./src"] 104 | } 105 | ``` 106 | 107 | 添加**src/App.tsx**内容 108 | 109 | ```tsx 110 | import React from 'react' 111 | 112 | function App() { 113 | return

webpack5-react-ts

114 | } 115 | export default App 116 | ``` 117 | 118 | 添加**src/index.tsx**内容 119 | 120 | ```tsx 121 | import React from 'react'; 122 | import { createRoot } from 'react-dom/client'; 123 | import App from './App'; 124 | 125 | const root = document.getElementById('root'); 126 | if(root) { 127 | createRoot(root).render() 128 | } 129 | ``` 130 | 131 | 现在项目业务代码已经添加好了,接下来可以配置**webpack**的代码了。 132 | 133 | ## 三. 配置基础版React+ts环境 134 | 135 | ### 2.1. webpack公共配置 136 | 137 | 修改**webpack.base.js** 138 | 139 | **1. 配置入口文件** 140 | 141 | ```js 142 | // webpack.base.js 143 | const path = require('path') 144 | 145 | module.exports = { 146 | entry: path.join(__dirname, '../src/index.tsx'), // 入口文件 147 | } 148 | ``` 149 | 150 | **2. 配置出口文件** 151 | 152 | ```js 153 | // webpack.base.js 154 | const path = require('path') 155 | 156 | module.exports = { 157 | // ... 158 | // 打包文件出口 159 | output: { 160 | filename: 'static/js/[name].js', // 每个输出js的名称 161 | path: path.join(__dirname, '../dist'), // 打包结果输出路径 162 | clean: true, // webpack4需要配置clean-webpack-plugin来删除dist文件,webpack5内置了 163 | publicPath: '/' // 打包后文件的公共前缀路径 164 | }, 165 | } 166 | ``` 167 | 168 | **3. 配置loader解析ts和jsx** 169 | 170 | 由于**webpack**默认只能识别**js**文件,不能识别**jsx**语法,需要配置**loader**的预设预设 [**@babel/preset-typescript**](https://www.babeljs.cn/docs/babel-preset-typescript) 来先**ts**语法转换为 **js** 语法,再借助预设 [**@babel/preset-react**](https://www.babeljs.cn/docs/babel-preset-react) 来识别**jsx**语法。 171 | 172 | **安装babel核心模块和babel预设** 173 | 174 | ```sh 175 | npm i babel-loader @babel/core @babel/preset-react @babel/preset-typescript -D 176 | ``` 177 | 178 | 在**webpack.base.js**添加**module.rules**配置 179 | 180 | ```js 181 | // webpack.base.js 182 | module.exports = { 183 | // ... 184 | module: { 185 | rules: [ 186 | { 187 | test: /.(ts|tsx)$/, // 匹配.ts, tsx文件 188 | use: { 189 | loader: 'babel-loader', 190 | options: { 191 | // 预设执行顺序由右往左,所以先处理ts,再处理jsx 192 | presets: [ 193 | '@babel/preset-react', 194 | '@babel/preset-typescript' 195 | ] 196 | } 197 | } 198 | } 199 | ] 200 | } 201 | } 202 | ``` 203 | 204 | **4. 配置extensions** 205 | 206 | **extensions**是**webpack**的**resolve**解析配置下的选项,在引入模块时不带文件后缀时,会来该配置数组里面依次添加后缀查找文件,因为**ts**不支持引入以 **.ts**, **tsx**为后缀的文件,所以要在**extensions**中配置,而第三方库里面很多引入**js**文件没有带后缀,所以也要配置下**js** 207 | 208 | 修改**webpack.base.js**,注意把高频出现的文件后缀放在前面 209 | 210 | ```js 211 | // webpack.base.js 212 | module.exports = { 213 | // ... 214 | resolve: { 215 | extensions: ['.js', '.tsx', '.ts'], 216 | } 217 | } 218 | ``` 219 | 220 | 这里只配置**js**, **tsx**和**ts**,其他文件引入都要求带后缀,可以提升构建速度。 221 | 222 | **4. 添加html-webpack-plugin插件** 223 | 224 | **webpack**需要把最终构建好的静态资源都引入到一个**html**文件中,这样才能在浏览器中运行,[html-webpack-plugin](https://www.npmjs.com/package/html-webpack-plugin)就是来做这件事情的,安装依赖: 225 | 226 | ```sh 227 | npm i html-webpack-plugin -D 228 | ``` 229 | 230 | 因为该插件在开发和构建打包模式都会用到,所以还是放在公共配置**webpack.base.js**里面 231 | 232 | ```js 233 | // webpack.base.js 234 | const path = require('path') 235 | const HtmlWebpackPlugin = require('html-webpack-plugin') 236 | 237 | module.exports = { 238 | // ... 239 | plugins: [ 240 | new HtmlWebpackPlugin({ 241 | template: path.resolve(__dirname, '../public/index.html'), // 模板取定义root节点的模板 242 | inject: true, // 自动注入静态资源 243 | }) 244 | ] 245 | } 246 | ``` 247 | 248 | 到这里一个最基础的**react**基本公共配置就已经配置好了,需要在此基础上分别配置开发环境和打包环境了。 249 | 250 | ### 2.2. webpack开发环境配置 251 | 252 | **1. 安装 webpack-dev-server** 253 | 254 | 开发环境配置代码在**webpack.dev.js**中,需要借助 [webpack-dev-server](https://www.npmjs.com/package/webpack-dev-server)在开发环境启动服务器来辅助开发,还需要依赖[webpack-merge](https://www.npmjs.com/package/webpack-merge)来合并基本配置,安装依赖: 255 | 256 | ```sh 257 | npm i webpack-dev-server webpack-merge -D 258 | ``` 259 | 260 | 修改**webpack.dev.js**代码, 合并公共配置,并添加开发模式配置 261 | 262 | ```js 263 | // webpack.dev.js 264 | const path = require('path') 265 | const { merge } = require('webpack-merge') 266 | const baseConfig = require('./webpack.base.js') 267 | 268 | // 合并公共配置,并添加开发环境配置 269 | module.exports = merge(baseConfig, { 270 | mode: 'development', // 开发模式,打包更加快速,省了代码优化步骤 271 | devtool: 'eval-cheap-module-source-map', // 源码调试模式,后面会讲 272 | devServer: { 273 | port: 3000, // 服务端口号 274 | compress: false, // gzip压缩,开发环境不开启,提升热更新速度 275 | hot: true, // 开启热更新,后面会讲react模块热替换具体配置 276 | historyApiFallback: true, // 解决history路由404问题 277 | static: { 278 | directory: path.join(__dirname, "../public"), //托管静态资源public文件夹 279 | } 280 | } 281 | }) 282 | ``` 283 | 284 | **2. package.json添加dev脚本** 285 | 286 | 在**package.json**的**scripts**中添加 287 | 288 | ```js 289 | // package.json 290 | "scripts": { 291 | "dev": "webpack-dev-server -c build/webpack.dev.js" 292 | }, 293 | ``` 294 | 295 | 执行**npm run dev**,就能看到项目已经启动起来了,访问,就可以看到项目界面,具体完善的**react**模块热替换在下面会讲到。 296 | 297 | ### 2.3. webpack打包环境配置 298 | 299 | **1. 修改webpack.prod.js代码** 300 | 301 | ```js 302 | // webpack.prod.js 303 | 304 | const { merge } = require('webpack-merge') 305 | const baseConfig = require('./webpack.base.js') 306 | module.exports = merge(baseConfig, { 307 | mode: 'production', // 生产模式,会开启tree-shaking和压缩代码,以及其他优化 308 | }) 309 | ``` 310 | 311 | **2. package.json添加build打包命令脚本** 312 | 313 | 在**package.json**的**scripts**中添加**build**打包命令 314 | 315 | ```js 316 | "scripts": { 317 | "dev": "webpack-dev-server -c build/webpack.dev.js", 318 | "build": "webpack -c build/webpack.prod.js" 319 | }, 320 | ``` 321 | 322 | 执行**npm run build**,最终打包在**dist**文件中, 打包结果: 323 | 324 | ```sh 325 | dist 326 | ├── static 327 | | ├── js 328 | | ├── main.js 329 | ├── index.html 330 | ``` 331 | 332 | **3. 浏览器查看打包结果** 333 | 334 | 打包后的**dist**文件可以在本地借助**node**服务器**serve**打开,全局安装**serve** 335 | 336 | ```sh 337 | npm i serve -g 338 | ``` 339 | 340 | 然后在项目根目录命令行执行**serve -s dist**,就可以启动打包后的项目了。 341 | 342 | 到现在一个基础的支持**react**和**ts**的**webpack5**就配置好了,但只有这些功能是远远不够的,还需要继续添加其他配置。 343 | 344 | ## 四. 基础功能配置 345 | 346 | ### 4.1 配置环境变量 347 | 348 | 环境变量按作用来分分两种 349 | 350 | 1. 区分是开发模式还是打包构建模式 351 | 2. 区分项目业务环境,开发/测试/预测/正式环境 352 | 353 | 区分开发模式还是打包构建模式可以用**process.env.NODE_ENV**,因为很多第三方包里面判断都是采用的这个环境变量。 354 | 355 | 区分项目接口环境可以自定义一个环境变量**process.env.BASE_ENV**,设置环境变量可以借助[cross-env](https://www.npmjs.com/package/cross-env)和[webpack.DefinePlugin](https://www.webpackjs.com/plugins/define-plugin/)来设置。 356 | 357 | - **cross-env**:兼容各系统的设置环境变量的包 358 | - **webpack.DefinePlugin**:**webpack**内置的插件,可以为业务代码注入环境变量 359 | 360 | 安装**cross-env** 361 | 362 | ```sh 363 | npm i cross-env -D 364 | ``` 365 | 366 | 修改**package.json**的**scripts**脚本字段,删除原先的**dev**和**build**,改为 367 | 368 | ```js 369 | "scripts": { 370 | "dev:dev": "cross-env NODE_ENV=development BASE_ENV=development webpack-dev-server -c build/webpack.dev.js", 371 | "dev:test": "cross-env NODE_ENV=development BASE_ENV=test webpack-dev-server -c build/webpack.dev.js", 372 | "dev:pre": "cross-env NODE_ENV=development BASE_ENV=pre webpack-dev-server -c build/webpack.dev.js", 373 | "dev:prod": "cross-env NODE_ENV=development BASE_ENV=production webpack-dev-server -c build/webpack.dev.js", 374 | 375 | "build:dev": "cross-env NODE_ENV=production BASE_ENV=development webpack -c build/webpack.prod.js", 376 | "build:test": "cross-env NODE_ENV=production BASE_ENV=test webpack -c build/webpack.prod.js", 377 | "build:pre": "cross-env NODE_ENV=production BASE_ENV=pre webpack -c build/webpack.prod.js", 378 | "build:prod": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.prod.js", 379 | }, 380 | ``` 381 | 382 | **dev**开头是开发模式,**build**开头是打包模式,冒号后面对应的**dev**/**test**/**pre**/**prod**是对应的业务环境的**开发**/**测试**/**预测**/**正式**环境。 383 | 384 | **process.env.NODE_ENV**环境变量**webpack**会自动根据设置的**mode**字段来给业务代码注入对应的**development**和**prodction**,这里在命令中再次设置环境变量**NODE_ENV**是为了在**webpack**和**babel**的配置文件中访问到。 385 | 386 | 在**webpack.base.js**中打印一下设置的环境变量 387 | 388 | ```js 389 | // webpack.base.js 390 | // ... 391 | console.log('NODE_ENV', process.env.NODE_ENV) 392 | console.log('BASE_ENV', process.env.BASE_ENV) 393 | ``` 394 | 395 | 执行**npm run build:dev**,可以看到打印的信息 396 | 397 | ```js 398 | // NODE_ENV production 399 | // BASE_ENV development 400 | ``` 401 | 402 | 当前是打包模式,业务环境是开发环境,这里需要把**process.env.BASE_ENV**注入到业务代码里面,就可以通过该环境变量设置对应环境的接口地址和其他数据,要借助**webpack.DefinePlugin**插件。 403 | 404 | 修改**webpack.base.js** 405 | 406 | ```js 407 | // webpack.base.js 408 | // ... 409 | const webpack = require('webpack') 410 | module.export = { 411 | // ... 412 | plugins: [ 413 | // ... 414 | new webpack.DefinePlugin({ 415 | 'process.env.BASE_ENV': JSON.stringify(process.env.BASE_ENV) 416 | }) 417 | ] 418 | } 419 | ``` 420 | 421 | 配置后会把值注入到业务代码里面去,**webpack**解析代码匹配到**process.env.BASE_ENV**,就会设置到对应的值。测试一下,在**src/index.tsx**打印一下两个环境变量 422 | 423 | ```tsx 424 | // src/index.tsx 425 | // ... 426 | console.log('NODE_ENV', process.env.NODE_ENV) 427 | console.log('BASE_ENV', process.env.BASE_ENV) 428 | ``` 429 | 430 | 执行**npm run dev:test**,可以在浏览器控制台看到打印的信息 431 | 432 | ```js 433 | // NODE_ENV development 434 | // BASE_ENV test 435 | ``` 436 | 437 | 当前是开发模式,业务环境是测试环境。 438 | 439 | ### 4.2 处理css和less文件 440 | 441 | 在**src**下新增**app.css** 442 | 443 | ```css 444 | h2 { 445 | color: red; 446 | transform: translateY(100px); 447 | } 448 | ``` 449 | 450 | 在**src/App.tsx**中引入**app.css** 451 | 452 | ```tsx 453 | import React from 'react' 454 | import './app.css' 455 | 456 | function App() { 457 | return

webpack5-rea11ct-ts

458 | } 459 | export default App 460 | ``` 461 | 462 | 执行打包命令**npm run build:dev**,会发现有报错, 因为**webpack**默认只认识**js**,是不识别**css**文件的,需要使用**loader**来解析**css**, 安装依赖 463 | 464 | ```sh 465 | npm i style-loader css-loader -D 466 | ``` 467 | 468 | - **style-loader**: 把解析后的**css**代码从**js**中抽离,放到头部的**style**标签中(在运行时做的) 469 | - **css-loader:** 解析**css**文件代码 470 | 471 | 因为解析**css**的配置开发和打包环境都会用到,所以加在公共配置**webpack.base.js**中 472 | 473 | ```js 474 | // webpack.base.js 475 | // ... 476 | module.exports = { 477 | // ... 478 | module: { 479 | rules: [ 480 | // ... 481 | { 482 | test: /.css$/, //匹配 css 文件 483 | use: ['style-loader','css-loader'] 484 | } 485 | ] 486 | }, 487 | // ... 488 | } 489 | ``` 490 | 491 | 上面提到过,**loader**执行顺序是从右往左,从下往上的,匹配到**css**文件后先用**css-loader**解析**css**, 最后借助**style-loader**把**css**插入到头部**style**标签中。 492 | 493 | 配置完成后再**npm run build:dev**打包,借助**serve -s dist**启动后在浏览器查看,可以看到样式生效了。 494 | 495 | ![微信截图_20220608102524.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/da3256ae5e674e5c81760a9f079c0bae~tplv-k3u1fbpfcp-watermark.image?) 496 | 497 | ### 4.3 支持less或scss 498 | 499 | 项目开发中,为了更好的提升开发体验,一般会使用**css**超集**less**或者**scss**,对于这些超集也需要对应的**loader**来识别解析。以**less**为例,需要安装依赖: 500 | 501 | ```sh 502 | npm i less-loader less -D 503 | ``` 504 | 505 | - **less-loader**: 解析**less**文件代码,把**less**编译为**css** 506 | - **less**: **less**核心 507 | 508 | 实现支持**less**也很简单,只需要在**rules**中添加**less**文件解析,遇到**less**文件,使用**less-loader**解析为**css**,再进行**css**解析流程,修改**webpack.base.js**: 509 | 510 | ```js 511 | // webpack.base.js 512 | module.exports = { 513 | // ... 514 | module: { 515 | // ... 516 | rules: [ 517 | // ... 518 | { 519 | test: /.(css|less)$/, //匹配 css和less 文件 520 | use: ['style-loader','css-loader', 'less-loader'] 521 | } 522 | ] 523 | }, 524 | // ... 525 | } 526 | ``` 527 | 528 | 测试一下,新增**src/app.less** 529 | 530 | ```less 531 | #root { 532 | h2 { 533 | font-size: 20px; 534 | } 535 | } 536 | ``` 537 | 538 | 在**App.tsx**中引入**app.less**,执行**npm run build:dev**打包,借助**serve -s dist**启动项目,可以看到**less**文件编写的样式编译**css**后也插入到**style**标签了了。 539 | 540 | ![微信截图_20220608102536.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/63d48c6252214ee9a48e3ec8d8c8b3c6~tplv-k3u1fbpfcp-watermark.image?) 541 | 542 | ### 4.4 处理css3前缀兼容 543 | 544 | 虽然**css3**现在浏览器支持率已经很高了, 但有时候需要兼容一些低版本浏览器,需要给**css3**加前缀,可以借助插件来自动加前缀, [postcss-loader](https://link.juejin.cn/?target=https%3A%2F%2Fwebpack.docschina.org%2Floaders%2Fpostcss-loader%2F)就是来给**css3**加浏览器前缀的,安装依赖: 545 | 546 | ```sh 547 | npm i postcss-loader autoprefixer -D 548 | ``` 549 | 550 | - **postcss-loader**:处理**css**时自动加前缀 551 | - **autoprefixer**:决定添加哪些浏览器前缀到**css**中 552 | 553 | 修改**webpack.base.js**, 在解析**css**和**less**的规则中添加配置 554 | 555 | ```js 556 | module.exports = { 557 | // ... 558 | module: { 559 | rules: [ 560 | // ... 561 | { 562 | test: /.(css|less)$/, //匹配 css和less 文件 563 | use: [ 564 | 'style-loader', 565 | 'css-loader', 566 | // 新增 567 | { 568 | loader: 'postcss-loader', 569 | options: { 570 | postcssOptions: { 571 | plugins: ['autoprefixer'] 572 | } 573 | } 574 | }, 575 | 'less-loader' 576 | ] 577 | } 578 | ] 579 | }, 580 | // ... 581 | } 582 | ``` 583 | 584 | 配置完成后,需要有一份要兼容浏览器的清单,让**postcss-loader**知道要加哪些浏览器的前缀,在根目录创建 **.browserslistrc**文件 585 | 586 | ```sh 587 | IE 9 # 兼容IE 9 588 | chrome 35 # 兼容chrome 35 589 | ``` 590 | 591 | 以兼容到**ie9**和**chrome35**版本为例,配置好后,执行**npm run build:dev**打包,可以看到打包后的**css**文件已经加上了**ie**和谷歌内核的前缀 592 | 593 | ![微信截图_20220608102538.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/959e4ff35dd6421fb3877ca7abf760bd~tplv-k3u1fbpfcp-watermark.image?) 594 | 595 | 上面可以看到解析**css**和**less**有很多重复配置,可以进行提取**postcss-loader**配置优化一下 596 | 597 | **postcss.config.js**是**postcss-loader**的配置文件,会自动读取配置,根目录新建**postcss.config.js**: 598 | 599 | ```js 600 | module.exports = { 601 | plugins: ['autoprefixer'] 602 | } 603 | ``` 604 | 605 | 修改**webpack.base.js**, 取消**postcss-loader**的**options**配置 606 | 607 | ```js 608 | // webpack.base.js 609 | // ... 610 | module.exports = { 611 | // ... 612 | module: { 613 | rules: [ 614 | // ... 615 | { 616 | test: /.(css|less)$/, //匹配 css和less 文件 617 | use: [ 618 | 'style-loader', 619 | 'css-loader', 620 | 'postcss-loader', 621 | 'less-loader' 622 | ] 623 | }, 624 | ] 625 | }, 626 | // ... 627 | } 628 | ``` 629 | 630 | 提取**postcss-loader**配置后,再次打包,可以看到依然可以解析**css**, **less**文件, **css3**对应前缀依然存在。 631 | 632 | ### 4.5 babel预设处理js兼容 633 | 634 | 现在**js**不断新增很多方便好用的标准语法来方便开发,甚至还有非标准语法比如装饰器,都极大的提升了代码可读性和开发效率。但前者标准语法很多低版本浏览器不支持,后者非标准语法所有的浏览器都不支持。需要把最新的标准语法转换为低版本语法,把非标准语法转换为标准语法才能让浏览器识别解析,而**babel**就是来做这件事的,这里只讲配置,更详细的可以看[Babel 那些事儿](https://juejin.cn/post/6992371845349507108)。 635 | 636 | 安装依赖 637 | 638 | ```sh 639 | npm i babel-loader @babel/core @babel/preset-env core-js -D 640 | ``` 641 | 642 | - babel-loader: 使用 **babel** 加载最新js代码并将其转换为 **ES5**(上面已经安装过) 643 | - @babel/corer: **babel** 编译的核心包 644 | - @babel/preset-env: **babel** 编译的预设,可以转换目前最新的**js**标准语法 645 | - core-js: 使用低版本**js**语法模拟高版本的库,也就是垫片 646 | 647 | 修改**webpack.base.js** 648 | 649 | ```js 650 | // webpack.base.js 651 | module.exports = { 652 | // ... 653 | module: { 654 | rules: [ 655 | { 656 | test: /.(ts|tsx)$/, 657 | use: { 658 | loader: 'babel-loader', 659 | options: { 660 | // 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法 661 | presets: [ 662 | [ 663 | "@babel/preset-env", 664 | { 665 | // 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc 666 | // "targets": { 667 | // "chrome": 35, 668 | // "ie": 9 669 | // }, 670 | "useBuiltIns": "usage", // 根据配置的浏览器兼容,以及代码中使用到的api进行引入polyfill按需添加 671 | "corejs": 3, // 配置使用core-js低版本 672 | } 673 | ], 674 | '@babel/preset-react', 675 | '@babel/preset-typescript' 676 | ] 677 | } 678 | } 679 | } 680 | ] 681 | } 682 | } 683 | ``` 684 | 685 | 此时再打包就会把语法转换为对应浏览器兼容的语法了。 686 | 687 | 为了避免**webpack**配置文件过于庞大,可以把**babel-loader**的配置抽离出来, 新建**babel.config.js**文件,使用**js**作为配置文件,是因为可以访问到**process.env.NODE_ENV**环境变量来区分是开发还是打包模式。 688 | 689 | ```js 690 | // babel.config.js 691 | module.exports = { 692 | // 执行顺序由右往左,所以先处理ts,再处理jsx,最后再试一下babel转换为低版本语法 693 | "presets": [ 694 | [ 695 | "@babel/preset-env", 696 | { 697 | // 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc 698 | // "targets": { 699 | // "chrome": 35, 700 | // "ie": 9 701 | // }, 702 | "useBuiltIns": "usage", // 根据配置的浏览器兼容,以及代码中使用到的api进行引入polyfill按需添加 703 | "corejs": 3 // 配置使用core-js使用的版本 704 | } 705 | ], 706 | "@babel/preset-react", 707 | "@babel/preset-typescript" 708 | ] 709 | } 710 | ``` 711 | 712 | 移除**webpack.base.js**中**babel-loader**的**options**配置 713 | 714 | ```js 715 | // webpack.base.js 716 | module.exports = { 717 | // ... 718 | module: { 719 | rules: [ 720 | { 721 | test: /.(ts|tsx)$/, 722 | use: 'babel-loader' 723 | }, 724 | // 如果node_moduels中也有要处理的语法,可以把js|jsx文件配置加上 725 | // { 726 | // test: /.(js|jsx)$/, 727 | // use: 'babel-loader' 728 | // } 729 | // ... 730 | ] 731 | } 732 | } 733 | ``` 734 | 735 | 736 | ### 4.6 babel处理js非标准语法 737 | 738 | 现在**react**主流开发都是函数组件和**react-hooks**,但有时也会用类组件,可以用装饰器简化代码。 739 | 740 | 新增**src/components/Class.tsx**组件, 在**App.tsx**中引入该组件使用 741 | 742 | ```tsx 743 | import React, { PureComponent } from "react"; 744 | 745 | // 装饰器为,组件添加age属性 746 | function addAge(Target: Function) { 747 | Target.prototype.age = 111 748 | } 749 | // 使用装饰圈 750 | @addAge 751 | class Class extends PureComponent { 752 | 753 | age?: number 754 | 755 | render() { 756 | return ( 757 |

我是类组件---{this.age}

758 | ) 759 | } 760 | } 761 | 762 | export default Class 763 | ``` 764 | 765 | 需要开启一下**ts**装饰器支持,修改**tsconfig.json**文件 766 | 767 | ```js 768 | // tsconfig.json 769 | { 770 | "compilerOptions": { 771 | // ... 772 | // 开启装饰器使用 773 | "experimentalDecorators": true 774 | } 775 | } 776 | ``` 777 | 778 | 上面Class组件代码中使用了装饰器,目前**js**标准语法是不支持的,现在运行或者打包会报错,不识别装饰器语法,需要借助**babel-loader**插件,安装依赖 779 | 780 | ```sh 781 | npm i @babel/plugin-proposal-decorators -D 782 | ``` 783 | 784 | 在**babel.config.js**中添加插件 785 | 786 | ```js 787 | module.exports = { 788 | // ... 789 | "plugins": [ 790 | ["@babel/plugin-proposal-decorators", { "legacy": true }] 791 | ] 792 | } 793 | ``` 794 | 795 | 现在项目就支持装饰器了。 796 | 797 | ### 4.7 复制public文件夹 798 | 799 | 一般**public**文件夹都会放一些静态资源,可以直接根据绝对路径引入,比如**图片**,**css**,**js**文件等,不需要**webpack**进行解析,只需要打包的时候把**public**下内容复制到构建出口文件夹中,可以借助[copy-webpack-plugin](https://www.npmjs.com/package/copy-webpack-plugin)插件,安装依赖 800 | 801 | ```sh 802 | npm i copy-webpack-plugin -D 803 | ``` 804 | 805 | 开发环境已经在**devServer**中配置了**static**托管了**public**文件夹,在开发环境使用绝对路径可以访问到**public**下的文件,但打包构建时不做处理会访问不到,所以现在需要在打包配置文件**webpack.prod.js**中新增**copy**插件配置。 806 | 807 | ```js 808 | // webpack.prod.js 809 | // .. 810 | const path = require('path') 811 | const CopyPlugin = require('copy-webpack-plugin'); 812 | module.exports = merge(baseConfig, { 813 | mode: 'production', 814 | plugins: [ 815 | // 复制文件插件 816 | new CopyPlugin({ 817 | patterns: [ 818 | { 819 | from: path.resolve(__dirname, '../public'), // 复制public下文件 820 | to: path.resolve(__dirname, '../dist'), // 复制到dist目录中 821 | filter: source => { 822 | return !source.includes('index.html') // 忽略index.html 823 | } 824 | }, 825 | ], 826 | }), 827 | ] 828 | }) 829 | ``` 830 | 831 | 在上面的配置中,忽略了**index.html**,因为**html-webpack-plugin**会以**public**下的**index.html**为模板生成一个**index.html**到**dist**文件下,所以不需要再复制该文件了。 832 | 833 | 测试一下,在**public**中新增一个[**favicon.ico**](https://guojiongwei.top/favicon.ico)图标文件,在**index.html**中引入 834 | 835 | ```html 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | webpack5-react-ts 845 | 846 | 847 | 848 |
849 | 850 | 851 | ``` 852 | 853 | 再执行**npm run build:dev**打包,就可以看到**public**下的**favicon.ico**图标文件被复制到**dist**文件中了。 854 | 855 | ![微信截图_20220608102540.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/66dc9b07a234485eb6435b7a6a03bf39~tplv-k3u1fbpfcp-watermark.image?) 856 | 857 | ### 4.8 处理图片文件 858 | 859 | 对于图片文件,**webpack4**使用**file-loader**和**url-loader**来处理的,但**webpack5**不使用这两个**loader**了,而是采用自带的[**asset-module**](https://webpack.js.org/guides/asset-modules/#root)来处理 860 | 861 | 修改**webpack.base.js**,添加图片解析配置 862 | 863 | ```js 864 | module.exports = { 865 | module: { 866 | rules: [ 867 | // ... 868 | { 869 | test:/.(png|jpg|jpeg|gif|svg)$/, // 匹配图片文件 870 | type: "asset", // type选择asset 871 | parser: { 872 | dataUrlCondition: { 873 | maxSize: 10 * 1024, // 小于10kb转base64位 874 | } 875 | }, 876 | generator:{ 877 | filename:'static/images/[name][ext]', // 文件输出目录和命名 878 | }, 879 | }, 880 | ] 881 | } 882 | } 883 | ``` 884 | 885 | 测试一下,准备一张小于[10kb](https://github.com/guojiongwei/webpack5-react-ts/blob/main/src/assets/imgs/5kb.png)的图片和大于[10kb](https://github.com/guojiongwei/webpack5-react-ts/blob/main/src/assets/imgs/22kb.png)的图片,放在**src/assets/imgs**目录下, 修改**App.tsx**: 886 | 887 | ```js 888 | import React from 'react' 889 | import smallImg from './assets/imgs/5kb.png' 890 | import bigImg from './assets/imgs/22kb.png' 891 | import './app.css' 892 | import './app.less' 893 | 894 | function App() { 895 | return ( 896 | <> 897 | 小于10kb的图片 898 | 大于于10kb的图片 899 | 900 | ) 901 | } 902 | export default App 903 | ``` 904 | > 这个时候在引入图片的地方会报:**找不到模块“./assets/imgs/22kb.png”或其相应的类型声明**,需要添加一个图片的声明文件 905 | 906 | 新增**src/images.d.ts**文件,添加内容 907 | 908 | 909 | ```js 910 | declare module '*.svg' 911 | declare module '*.png' 912 | declare module '*.jpg' 913 | declare module '*.jpeg' 914 | declare module '*.gif' 915 | declare module '*.bmp' 916 | declare module '*.tiff' 917 | declare module '*.less' 918 | declare module '*.css' 919 | ``` 920 | 添加图片声明文件后,就可以正常引入图片了, 然后执行**npm run build:dev**打包,借助**serve -s dist**查看效果,可以看到可以正常解析图片了,并且小于**10kb**的图片被转成了**base64**位格式的。 921 | 922 | ![微信截图_20220608102550.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e3f819540e6e428cadad50a36b1e768e~tplv-k3u1fbpfcp-watermark.image?) 923 | 924 | **css**中的背景图片一样也可以解析,修改**app.tsx**。 925 | 926 | ```tsx 927 | import React from 'react' 928 | import smallImg from './assets/imgs/5kb.png' 929 | import bigImg from './assets/imgs/22kb.png' 930 | import './app.css' 931 | import './app.less' 932 | 933 | function App() { 934 | return ( 935 | <> 936 | 小于10kb的图片 937 | 大于于10kb的图片 938 |
{/* 小图片背景容器 */} 939 |
{/* 大图片背景容器 */} 940 | 941 | ) 942 | } 943 | export default App 944 | ``` 945 | 修改**app.less** 946 | 947 | ```less 948 | // app.less 949 | #root { 950 | .smallImg { 951 | width: 69px; 952 | height: 75px; 953 | background: url('./assets/imgs/5kb.png') no-repeat; 954 | } 955 | .bigImg { 956 | width: 232px; 957 | height: 154px; 958 | background: url('./assets/imgs/22kb.png') no-repeat; 959 | } 960 | } 961 | ``` 962 | 963 | 可以看到背景图片也一样可以识别,小于**10kb**转为**base64**位。 964 | 965 | ![微信截图_20220608102560.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f399c5523fb24913bf30be5d5c2399f5~tplv-k3u1fbpfcp-watermark.image?) 966 | 967 | ### 4.9 处理字体和媒体文件 968 | 969 | 字体文件和媒体文件这两种资源处理方式和处理图片是一样的,只需要把匹配的路径和打包后放置的路径修改一下就可以了。修改**webpack.base.js**文件: 970 | 971 | ```js 972 | // webpack.base.js 973 | module.exports = { 974 | module: { 975 | rules: [ 976 | // ... 977 | { 978 | test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件 979 | type: "asset", // type选择asset 980 | parser: { 981 | dataUrlCondition: { 982 | maxSize: 10 * 1024, // 小于10kb转base64位 983 | } 984 | }, 985 | generator:{ 986 | filename:'static/fonts/[name][ext]', // 文件输出目录和命名 987 | }, 988 | }, 989 | { 990 | test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件 991 | type: "asset", // type选择asset 992 | parser: { 993 | dataUrlCondition: { 994 | maxSize: 10 * 1024, // 小于10kb转base64位 995 | } 996 | }, 997 | generator:{ 998 | filename:'static/media/[name][ext]', // 文件输出目录和命名 999 | }, 1000 | }, 1001 | ] 1002 | } 1003 | } 1004 | ``` 1005 | 1006 | ## 五. 配置react模块热更新 1007 | 1008 | 热更新上面已经在**devServer**中配置**hot**为**true**, 在**webpack4**中,还需要在插件中添加了**HotModuleReplacementPlugin**,在**webpack5**中,只要**devServer.hot**为**true**了,该插件就已经内置了。 1009 | 1010 | 现在开发模式下修改**css**和**less**文件,页面样式可以在不刷新浏览器的情况实时生效,因为此时样式都在**style**标签里面,**style-loader**做了替换样式的热替换功能。但是修改**App.tsx**,浏览器会自动刷新后再显示修改后的内容,但我们想要的不是刷新浏览器,而是在不需要刷新浏览器的前提下模块热更新,并且能够保留**react**组件的状态。 1011 | 1012 | 可以借助[@pmmmwh/react-refresh-webpack-plugin](https://www.npmjs.com/package/@pmmmwh/react-refresh-webpack-plugin)插件来实现,该插件又依赖于[react-refresh](https://www.npmjs.com/package/react-refresh), 安装依赖: 1013 | 1014 | ```sh 1015 | npm i @pmmmwh/react-refresh-webpack-plugin react-refresh -D 1016 | ``` 1017 | 1018 | 配置**react**热更新插件,修改**webpack.dev.js** 1019 | 1020 | ```js 1021 | // webpack.dev.js 1022 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); 1023 | 1024 | module.exports = merge(baseConfig, { 1025 | // ... 1026 | plugins: [ 1027 | new ReactRefreshWebpackPlugin(), // 添加热更新插件 1028 | ] 1029 | }) 1030 | ``` 1031 | 1032 | 为**babel-loader**配置**react-refesh**刷新插件,修改**babel.config.js**文件 1033 | 1034 | ```js 1035 | const isDEV = process.env.NODE_ENV === 'development' // 是否是开发模式 1036 | module.exports = { 1037 | // ... 1038 | "plugins": [ 1039 | isDEV && require.resolve('react-refresh/babel'), // 如果是开发模式,就启动react热更新插件 1040 | // ... 1041 | ].filter(Boolean) // 过滤空值 1042 | } 1043 | ``` 1044 | 1045 | 测试一下,修改**App.tsx**代码 1046 | 1047 | ```tsx 1048 | import React, { useState } from 'react' 1049 | 1050 | function App() { 1051 | const [ count, setCounts ] = useState('') 1052 | const onChange = (e: any) => { 1053 | setCounts(e.target.value) 1054 | } 1055 | return ( 1056 | <> 1057 |

webpack5+react+ts

1058 |

受控组件

1059 | 1060 |
1061 |

非受控组件

1062 | 1063 | 1064 | ) 1065 | } 1066 | export default App 1067 | ``` 1068 | 1069 | 在两个输入框分别输入内容后,修改**App.tsx**中**h2**标签的文本,会发现在不刷新浏览器的情况下,页面内容进行了热更新,并且**react**组件状态也会保留。 1070 | 1071 | 1072 | ![微信截图_20220608103100.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f22a13be3020484fb9a3f975aec4a7ee~tplv-k3u1fbpfcp-watermark.image?) 1073 | 1074 | 1075 | ![微信截图_20220608103103.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/227ff0a5871c4387a3b10f0f276b7500~tplv-k3u1fbpfcp-watermark.image?) 1076 | 1077 | > 新增或者删除页面**hooks**时,热更新时组件状态不会保留。 1078 | 1079 | ## 六. 优化构建速度 1080 | 1081 | ### 6.1 构建耗时分析 1082 | 1083 | 当进行优化的时候,肯定要先知道时间都花费在哪些步骤上了,而[speed-measure-webpack-plugin](https://link.juejin.cn/?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fspeed-measure-webpack-plugin)插件可以帮我们做到,安装依赖: 1084 | 1085 | ```sh 1086 | npm i speed-measure-webpack-plugin -D 1087 | ``` 1088 | 1089 | 使用的时候为了不影响到正常的开发/打包模式,我们选择新建一个配置文件,新增**webpack**构建分析配置文件**build/webpack.analy.js** 1090 | 1091 | ```js 1092 | const prodConfig = require('./webpack.prod.js') // 引入打包配置 1093 | const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); // 引入webpack打包速度分析插件 1094 | const smp = new SpeedMeasurePlugin(); // 实例化分析插件 1095 | const { merge } = require('webpack-merge') // 引入合并webpack配置方法 1096 | 1097 | // 使用smp.wrap方法,把生产环境配置传进去,由于后面可能会加分析配置,所以先留出合并空位 1098 | module.exports = smp.wrap(merge(prodConfig, { 1099 | 1100 | })) 1101 | ``` 1102 | 1103 | 修改**package.json**添加启动**webpack**打包分析脚本命令,在**scripts**新增: 1104 | 1105 | ```js 1106 | { 1107 | // ... 1108 | "scripts": { 1109 | // ... 1110 | "build:analy": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.analy.js" 1111 | } 1112 | // ... 1113 | } 1114 | ``` 1115 | 1116 | 执行**npm run build:analy**命令 1117 | 1118 | ![微信截图_20220615110031.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/52ed97e61f94427c8bf3ed833a3957f0~tplv-k3u1fbpfcp-watermark.image?) 1119 | 1120 | 可以在图中看到各**plugin**和**loader**的耗时时间,现在因为项目内容比较少,所以耗时都比较少,在真正的项目中可以通过这个来分析打包时间花费在什么地方,然后来针对性的优化。 1121 | 1122 | ### 6.2 开启持久化存储缓存 1123 | 1124 | 在**webpack5**之前做缓存是使用**babel-loader**缓存解决**js**的解析结果,**cache-loader**缓存**css**等资源的解析结果,还有模块缓存插件**hard-source-webpack-plugin**,配置好缓存后第二次打包,通过对文件做哈希对比来验证文件前后是否一致,如果一致则采用上一次的缓存,可以极大地节省时间。 1125 | 1126 | **webpack5** 较于 **webpack4**,新增了持久化缓存、改进缓存算法等优化,通过配置 [webpack 持久化缓存](https%3A%2F%2Fwebpack.docschina.org%2Fconfiguration%2Fcache%2F%23root),来缓存生成的 **webpack** 模块和 **chunk**,改善下一次打包的构建速度,可提速 **90%** 左右,配置也简单,修改**webpack.base.js** 1127 | 1128 | ```js 1129 | // webpack.base.js 1130 | // ... 1131 | module.exports = { 1132 | // ... 1133 | cache: { 1134 | type: 'filesystem', // 使用文件缓存 1135 | }, 1136 | } 1137 | ``` 1138 | 1139 | 当前文章代码的测试结果 1140 | 1141 | | 模式 | 第一次耗时 | 第二次耗时 | 1142 | | ------------ | ---------- | ---------- | 1143 | | 启动开发模式 | 2869毫秒 | 687毫秒 | 1144 | | 启动打包模式 | 5455毫秒 | 552毫秒 | 1145 | 1146 | 通过开启**webpack5**持久化存储缓存,再次打包的时间提升了**90%**。 1147 | 1148 | ![微信截图_20220615163590.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/28e2db78055a4a6c8154fe371ad56a88~tplv-k3u1fbpfcp-watermark.image?) 1149 | 1150 | 缓存的存储位置在**node_modules/.cache/webpack**,里面又区分了**development**和**production**缓存 1151 | 1152 | ![微信截图_20220615163601.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2137439811c64a9382c431aff8e5b8fe~tplv-k3u1fbpfcp-watermark.image?) 1153 | 1154 | ### 6.3 开启多线程loader 1155 | 1156 | **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**的,安装依赖 1157 | 1158 | ```sh 1159 | npm i thread-loader -D 1160 | ``` 1161 | 1162 | 使用时,需将此 **loader** 放置在其他 **loader** 之前。放置在此 **loader** 之后的 **loader** 会在一个独立的 **worker** 池中运行。 1163 | 1164 | 修改**webpack.base.js** 1165 | 1166 | ```js 1167 | // webpack.base.js 1168 | module.exports = { 1169 | // ... 1170 | module: { 1171 | rules: [ 1172 | { 1173 | test: /.(ts|tsx)$/, 1174 | use: ['thread-loader', 'babel-loader'] 1175 | } 1176 | ] 1177 | } 1178 | } 1179 | ``` 1180 | 1181 | 由于**thread-loader**不支持抽离css插件**MiniCssExtractPlugin.loader**(下面会讲),所以这里只配置了多进程解析**js**,开启多线程也是需要启动时间,大约**600ms**左右,所以适合规模比较大的项目。 1182 | 1183 | ### 6.4 配置alias别名 1184 | 1185 | **webpack**支持设置别名**alias**,设置别名可以让后续引用的地方减少路径的复杂度。 1186 | 1187 | 修改**webpack.base.js** 1188 | 1189 | ```js 1190 | module.export = { 1191 | // ... 1192 | resolve: { 1193 | // ... 1194 | alias: { 1195 | '@': path.join(__dirname, '../src') 1196 | } 1197 | } 1198 | } 1199 | ``` 1200 | 1201 | 修改**tsconfig.json**,添加**baseUrl**和**paths** 1202 | 1203 | ```js 1204 | { 1205 | "compilerOptions": { 1206 | // ... 1207 | "baseUrl": ".", 1208 | "paths": { 1209 | "@/*": [ 1210 | "src/*" 1211 | ] 1212 | } 1213 | } 1214 | } 1215 | ``` 1216 | 1217 | 配置修改完成后,在项目中使用 **@/xxx.xx**,就会指向项目中**src/xxx.xx,**在**js/ts**文件和**css**文件中都可以用。 1218 | 1219 | **src/App.tsx**可以修改为 1220 | 1221 | ```tsx 1222 | import React from 'react' 1223 | import smallImg from '@/assets/imgs/5kb.png' 1224 | import bigImg from '@/assets/imgs/22kb.png' 1225 | import '@/app.css' 1226 | import '@/app.less' 1227 | 1228 | function App() { 1229 | return ( 1230 | <> 1231 | 小于10kb的图片 1232 | 大于于10kb的图片 1233 |
{/* 小图片背景容器 */} 1234 |
{/* 大图片背景容器 */} 1235 | 1236 | ) 1237 | } 1238 | export default App 1239 | ``` 1240 | **src/app.less**可以修改为 1241 | 1242 | ```less 1243 | // app.less 1244 | #root { 1245 | .smallImg { 1246 | width: 69px; 1247 | height: 75px; 1248 | background: url('@/assets/imgs/5kb.png') no-repeat; 1249 | } 1250 | .bigImg { 1251 | width: 232px; 1252 | height: 154px; 1253 | background: url('@/assets/imgs/22kb.png') no-repeat; 1254 | } 1255 | } 1256 | ``` 1257 | ### 6.5 缩小loader作用范围 1258 | 1259 | 一般第三库都是已经处理好的,不需要再次使用**loader**去解析,可以按照实际情况合理配置**loader**的作用范围,来减少不必要的**loader**解析,节省时间,通过使用 **include**和**exclude** 两个配置项,可以实现这个功能,常见的例如: 1260 | 1261 | - **include**:只解析该选项配置的模块 1262 | - **exclude**:不解该选项配置的模块,优先级更高 1263 | 1264 | 修改**webpack.base.js** 1265 | 1266 | ```js 1267 | // webpack.base.js 1268 | const path = require('path') 1269 | module.exports = { 1270 | // ... 1271 | module: { 1272 | rules: [ 1273 | { 1274 | include: [path.resolve(__dirname, '../src')], 只对项目src文件的ts,tsx进行loader解析 1275 | test: /.(ts|tsx)$/, 1276 | use: ['thread-loader', 'babel-loader'] 1277 | } 1278 | ] 1279 | } 1280 | } 1281 | ``` 1282 | 1283 | 其他**loader**也是相同的配置方式,如果除**src**文件外也还有需要解析的,就把对应的目录地址加上就可以了,比如需要引入**antd**的**css**,可以把**antd**的文件目录路径添加解析**css**规则到**include**里面。 1284 | 1285 | ### 6.6 精确使用loader 1286 | 1287 | **loader**在**webpack**构建过程中使用的位置是在**webpack**构建模块依赖关系引入新文件时,会根据文件后缀来倒序遍历**rules**数组,如果文件后缀和**test**正则匹配到了,就会使用该**rule**中配置的**loader**依次对文件源代码进行处理,最终拿到处理后的**sourceCode**结果,可以通过避免使用无用的**loader**解析来提升构建速度,比如使用**less-loader**解析**css**文件。 1288 | 1289 | 可以拆分上面配置的**less**和**css**, 避免让**less-loader**再去解析**css**文件 1290 | 1291 | ```js 1292 | // webpack.base.js 1293 | // ... 1294 | module.exports = { 1295 | module: { 1296 | // ... 1297 | rules: [ 1298 | // ... 1299 | { 1300 | test: /.css$/, //匹配所有的 css 文件 1301 | include: [path.resolve(__dirname, '../src')], 1302 | use: [ 1303 | 'style-loader', 1304 | 'css-loader', 1305 | 'postcss-loader' 1306 | ] 1307 | }, 1308 | { 1309 | test: /.less$/, //匹配所有的 less 文件 1310 | include: [path.resolve(__dirname, '../src')], 1311 | use: [ 1312 | 'style-loader', 1313 | 'css-loader', 1314 | 'postcss-loader', 1315 | 'less-loader' 1316 | ] 1317 | }, 1318 | ] 1319 | } 1320 | } 1321 | ``` 1322 | 1323 | **ts**和**tsx**也是如此,**ts**里面是不能写**jsx**语法的,所以可以尽可能避免使用 **@babel/preset-react**对 **.ts** 文件语法做处理。 1324 | 1325 | ### 6.7 缩小模块搜索范围 1326 | 1327 | **node**里面模块有三种 1328 | 1329 | - **node**核心模块 1330 | - **node_modules**模块 1331 | - 自定义文件模块 1332 | 1333 | 使用**require**和**import**引入模块时如果有准确的相对或者绝对路径,就会去按路径查询,如果引入的模块没有路径,会优先查询**node**核心模块,如果没有找到会去当前目录下**node_modules**中寻找,如果没有找到会查从父级文件夹查找**node_modules**,一直查到系统**node**全局模块。 1334 | 1335 | 这样会有两个问题,一个是当前项目没有安装某个依赖,但是上一级目录下**node_modules**或者全局模块有安装,就也会引入成功,但是部署到服务器时可能就会找不到造成报错,另一个问题就是一级一级查询比较消耗时间。可以告诉**webpack**搜索目录范围,来规避这两个问题。 1336 | 1337 | 修改**webpack.base.js** 1338 | 1339 | ```js 1340 | // webpack.base.js 1341 | const path = require('path') 1342 | module.exports = { 1343 | // ... 1344 | resolve: { 1345 | // ... 1346 | modules: [path.resolve(__dirname, '../node_modules')], // 查找第三方模块只在本项目的node_modules中查找 1347 | }, 1348 | } 1349 | ``` 1350 | 1351 | ### 6.8 devtool 配置 1352 | 1353 | 开发过程中或者打包后的代码都是**webpack**处理后的代码,如果进行调试肯定希望看到源代码,而不是编译后的代码, [source map](http://blog.teamtreehouse.com/introduction-source-maps)就是用来做源码映射的,不同的映射模式会明显影响到构建和重新构建的速度, [**devtool**](https://webpack.js.org/configuration/devtool/)选项就是**webpack**提供的选择源码映射方式的配置。 1354 | 1355 | **devtool**的命名规则为 **^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$** 1356 | 1357 | | 关键字 | 描述 | 1358 | | --------- | -------------------------------------------------------- | 1359 | | inline | 代码内通过 dataUrl 形式引入 SourceMap | 1360 | | hidden | 生成 SourceMap 文件,但不使用 | 1361 | | eval | `eval(...)` 形式执行代码,通过 dataUrl 形式引入 SourceMap | 1362 | | nosources | 不生成 SourceMap | 1363 | | cheap | 只需要定位到行信息,不需要列信息 | 1364 | | module | 展示源代码中的错误位置 | 1365 | 1366 | 开发环境推荐:**eval-cheap-module-source-map** 1367 | 1368 | - 本地开发首次打包慢点没关系,因为 **eval** 缓存的原因, 热更新会很快 1369 | - 开发中,我们每行代码不会写的太长,只需要定位到行就行,所以加上 **cheap** 1370 | - 我们希望能够找到源代码的错误,而不是打包后的,所以需要加上 **module** 1371 | 1372 | 修改**webpack.dev.js** 1373 | 1374 | ```js 1375 | // webpack.dev.js 1376 | module.exports = { 1377 | // ... 1378 | devtool: 'eval-cheap-module-source-map' 1379 | } 1380 | ``` 1381 | 1382 | 打包环境推荐:**none**(就是不配置**devtool**选项了,不是配置**devtool**: '**none**') 1383 | 1384 | 1385 | ```js 1386 | // webpack.prod.js 1387 | module.exports = { 1388 | // ... 1389 | // devtool: '', // 不用配置devtool此项 1390 | } 1391 | ``` 1392 | 1393 | - **none**话调试只能看到编译后的代码,也不会泄露源代码,打包速度也会比较快。 1394 | - 只是不方便线上排查问题, 但一般都可以根据报错信息在本地环境很快找出问题所在。 1395 | 1396 | ### 6.9 其他优化配置 1397 | 1398 | 除了上面的配置外,**webpack**还提供了其他的一些优化方式,本次搭建没有使用到,所以只简单罗列下 1399 | 1400 | - [**externals**](https://www.webpackjs.com/configuration/externals/): 外包拓展,打包时会忽略配置的依赖,会从上下文中寻找对应变量 1401 | - [**module.noParse**](https://www.webpackjs.com/configuration/module/#module-noparse): 匹配到设置的模块,将不进行依赖解析,适合**jquery**,**boostrap**这类不依赖外部模块的包 1402 | - [**ignorePlugin**](https://webpack.js.org/plugins/ignore-plugin/#root): 可以使用正则忽略一部分文件,常在使用多语言的包时可以把非中文语言包过滤掉 1403 | 1404 | ## 七. 优化构建结果文件 1405 | 1406 | ### 7.1 webpack包分析工具 1407 | 1408 | [webpack-bundle-analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer)是分析**webpack**打包后文件的插件,使用交互式可缩放树形图可视化 **webpack** 输出文件的大小。通过该插件可以对打包后的文件进行观察和分析,可以方便我们对不完美的地方针对性的优化,安装依赖: 1409 | 1410 | ```sh 1411 | npm install webpack-bundle-analyzer -D 1412 | ``` 1413 | 1414 | 修改**webpack.analy.js** 1415 | 1416 | ```js 1417 | // webpack.analy.js 1418 | const prodConfig = require('./webpack.prod.js') 1419 | const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); 1420 | const smp = new SpeedMeasurePlugin(); 1421 | const { merge } = require('webpack-merge') 1422 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') // 引入分析打包结果插件 1423 | module.exports = smp.wrap(merge(prodConfig, { 1424 | plugins: [ 1425 | new BundleAnalyzerPlugin() // 配置分析打包结果插件 1426 | ] 1427 | })) 1428 | ``` 1429 | 1430 | 配置好后,执行**npm run build:analy**命令,打包完成后浏览器会自动打开窗口,可以看到打包文件的分析结果页面,可以看到各个文件所占的资源大小。 1431 | 1432 | ![微信截图_20220616153950.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/18a854a3ce594292a2faf5d329d7095a~tplv-k3u1fbpfcp-watermark.image?) 1433 | 1434 | ### 7.2 抽取css样式文件 1435 | 1436 | 在开发环境我们希望**css**嵌入在**style**标签里面,方便样式热替换,但打包时我们希望把**css**单独抽离出来,方便配置缓存策略。而插件[mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin)就是来帮我们做这件事的,安装依赖: 1437 | 1438 | ```sh 1439 | npm i mini-css-extract-plugin -D 1440 | ``` 1441 | 1442 | 修改**webpack.base.js**, 根据环境变量设置开发环境使用**style-looader**,打包模式抽离**css** 1443 | 1444 | ```js 1445 | // webpack.base.js 1446 | // ... 1447 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 1448 | const isDev = process.env.NODE_ENV === 'development' // 是否是开发模式 1449 | module.exports = { 1450 | // ... 1451 | module: { 1452 | rules: [ 1453 | // ... 1454 | { 1455 | test: /.css$/, //匹配所有的 css 文件 1456 | include: [path.resolve(__dirname, '../src')], 1457 | use: [ 1458 | isDev ? 'style-loader' : MiniCssExtractPlugin.loader, // 开发环境使用style-looader,打包模式抽离css 1459 | 'css-loader', 1460 | 'postcss-loader' 1461 | ] 1462 | }, 1463 | { 1464 | test: /.less$/, //匹配所有的 less 文件 1465 | include: [path.resolve(__dirname, '../src')], 1466 | use: [ 1467 | isDev ? 'style-loader' : MiniCssExtractPlugin.loader, // 开发环境使用style-looader,打包模式抽离css 1468 | 'css-loader', 1469 | 'postcss-loader', 1470 | 'less-loader' 1471 | ] 1472 | }, 1473 | ] 1474 | }, 1475 | // ... 1476 | } 1477 | ``` 1478 | 1479 | 再修改**webpack.prod.js**, 打包时添加抽离css插件 1480 | 1481 | ```js 1482 | // webpack.prod.js 1483 | // ... 1484 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 1485 | module.exports = merge(baseConfig, { 1486 | mode: 'production', 1487 | plugins: [ 1488 | // ... 1489 | // 抽离css插件 1490 | new MiniCssExtractPlugin({ 1491 | filename: 'static/css/[name].css' // 抽离css的输出目录和名称 1492 | }), 1493 | ] 1494 | }) 1495 | ``` 1496 | 1497 | 配置完成后,在开发模式**css**会嵌入到**style**标签里面,方便样式热替换,打包时会把**css**抽离成单独的**css**文件。 1498 | 1499 | ### 7.3 压缩css文件 1500 | 1501 | 上面配置了打包时把**css**抽离为单独**css**文件的配置,打开打包后的文件查看,可以看到默认**css**是没有压缩的,需要手动配置一下压缩**css**的插件。 1502 | 1503 | ![微信截图_20220616153959.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb0c5dee0fe044e39402eb6a152c6fa2~tplv-k3u1fbpfcp-watermark.image?) 1504 | 1505 | 可以借助[css-minimizer-webpack-plugin](https://link.juejin.cn/?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fcss-minimizer-webpack-plugin)来压缩css,安装依赖 1506 | 1507 | ```sh 1508 | npm i css-minimizer-webpack-plugin -D 1509 | ``` 1510 | 1511 | 修改**webpack.prod.js**文件, 需要在优化项[optimization](https://webpack.js.org/configuration/optimization/)下的[minimizer](https://webpack.js.org/configuration/optimization/#optimizationminimizer)属性中配置 1512 | 1513 | ```js 1514 | // webpack.prod.js 1515 | // ... 1516 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') 1517 | module.exports = { 1518 | // ... 1519 | optimization: { 1520 | minimizer: [ 1521 | new CssMinimizerPlugin(), // 压缩css 1522 | ], 1523 | }, 1524 | } 1525 | ``` 1526 | 1527 | 再次执行打包就可以看到**css**已经被压缩了。 1528 | 1529 | ### 7.4 压缩js文件 1530 | 1531 | 设置**mode**为**production**时,**webpack**会使用内置插件[terser-webpack-plugin](https://www.npmjs.com/package/terser-webpack-plugin)压缩**js**文件,该插件默认支持多线程压缩,但是上面配置**optimization.minimizer**压缩**css**后,**js**压缩就失效了,需要手动再添加一下,**webpack**内部安装了该插件,由于**pnpm**解决了幽灵依赖问题,如果用的**pnpm**的话,需要手动再安装一下依赖。 1532 | 1533 | ```sh 1534 | npm i terser-webpack-plugin -D 1535 | ``` 1536 | 1537 | 修改**webpack.prod.js**文件 1538 | 1539 | ```js 1540 | // ... 1541 | const TerserPlugin = require('terser-webpack-plugin') 1542 | module.exports = { 1543 | // ... 1544 | optimization: { 1545 | minimizer: [ 1546 | // ... 1547 | new TerserPlugin({ // 压缩js 1548 | parallel: true, // 开启多线程压缩 1549 | terserOptions: { 1550 | compress: { 1551 | pure_funcs: ["console.log"] // 删除console.log 1552 | } 1553 | } 1554 | }), 1555 | ], 1556 | }, 1557 | } 1558 | ``` 1559 | 1560 | 配置完成后再打包,**css**和**js**就都可以被压缩了。 1561 | 1562 | ### 7.5 合理配置打包文件hash 1563 | 1564 | 项目维护的时候,一般只会修改一部分代码,可以合理配置文件缓存,来提升前端加载页面速度和减少服务器压力,而**hash**就是浏览器缓存策略很重要的一部分。**webpack**打包的**hash**分三种: 1565 | 1566 | - **hash**:跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的**hash**值都会更改,并且全部文件都共用相同的**hash**值 1567 | - **chunkhash**:不同的入口文件进行依赖文件解析、构建对应的**chunk**,生成对应的哈希值,文件本身修改或者依赖文件修改,**chunkhash**值会变化 1568 | - **contenthash**:每个文件自己单独的 **hash** 值,文件的改动只会影响自身的 **hash** 值 1569 | 1570 | **hash**是在输出文件时配置的,格式是**filename: "[name].\[chunkhash:8][ext]"**,**[xx]** 格式是**webpack**提供的占位符, **:8**是生成**hash**的长度。 1571 | 1572 | | 占位符 | 解释 | 1573 | | ----------- | -------------------------- | 1574 | | ext | 文件后缀名 | 1575 | | name | 文件名 | 1576 | | path | 文件相对路径 | 1577 | | folder | 文件所在文件夹 | 1578 | | hash | 每次构建生成的唯一 hash 值 | 1579 | | chunkhash | 根据 chunk 生成 hash 值 | 1580 | | contenthash | 根据文件内容生成hash 值 | 1581 | 1582 | 因为**js**我们在生产环境里会把一些公共库和程序入口文件区分开,单独打包构建,采用**chunkhash**的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响,可以继续使用浏览器缓存,所以**js**适合使用**chunkhash**。 1583 | 1584 | **css**和图片资源媒体资源一般都是单独存在的,可以采用**contenthash**,只有文件本身变化后会生成新**hash**值。 1585 | 1586 | 修改**webpack.base.js**,把**js**输出的文件名称格式加上**chunkhash**,把**css**和图片媒体资源输出格式加上**contenthash** 1587 | 1588 | ```js 1589 | // webpack.base.js 1590 | // ... 1591 | module.exports = { 1592 | // 打包文件出口 1593 | output: { 1594 | filename: 'static/js/[name].[chunkhash:8].js', // // 加上[chunkhash:8] 1595 | // ... 1596 | }, 1597 | module: { 1598 | rules: [ 1599 | { 1600 | test:/.(png|jpg|jpeg|gif|svg)$/, // 匹配图片文件 1601 | // ... 1602 | generator:{ 1603 | filename:'static/images/[name].[contenthash:8][ext]' // 加上[contenthash:8] 1604 | }, 1605 | }, 1606 | { 1607 | test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体文件 1608 | // ... 1609 | generator:{ 1610 | filename:'static/fonts/[name].[contenthash:8][ext]', // 加上[contenthash:8] 1611 | }, 1612 | }, 1613 | { 1614 | test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件 1615 | // ... 1616 | generator:{ 1617 | filename:'static/media/[name].[contenthash:8][ext]', // 加上[contenthash:8] 1618 | }, 1619 | }, 1620 | ] 1621 | }, 1622 | // ... 1623 | } 1624 | ``` 1625 | 1626 | 再修改**webpack.prod.js**,修改抽离**css**文件名称格式 1627 | 1628 | ```js 1629 | // webpack.prod.js 1630 | // ... 1631 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 1632 | module.exports = merge(baseConfig, { 1633 | mode: 'production', 1634 | plugins: [ 1635 | // 抽离css插件 1636 | new MiniCssExtractPlugin({ 1637 | filename: 'static/css/[name].[contenthash:8].css' // 加上[contenthash:8] 1638 | }), 1639 | // ... 1640 | ], 1641 | // ... 1642 | }) 1643 | ``` 1644 | 再次打包就可以看到文件后面的**hash**了 1645 | 1646 | ### 7.6 代码分割第三方包和公共模块 1647 | 1648 | 一般第三方包的代码变化频率比较小,可以单独把**node_modules**中的代码单独打包, 当第三包代码没变化时,对应**chunkhash**值也不会变化,可以有效利用浏览器缓存,还有公共的模块也可以提取出来,避免重复打包加大代码整体体积, **webpack**提供了代码分隔功能, 需要我们手动在优化项[optimization](https://webpack.js.org/configuration/optimization/)中手动配置下代码分隔[splitChunks](https://webpack.js.org/configuration/optimization/#optimizationsplitchunks)规则。 1649 | 1650 | 修改**webpack.prod.js** 1651 | 1652 | ```js 1653 | module.exports = { 1654 | // ... 1655 | optimization: { 1656 | // ... 1657 | splitChunks: { // 分隔代码 1658 | cacheGroups: { 1659 | vendors: { // 提取node_modules代码 1660 | test: /node_modules/, // 只匹配node_modules里面的模块 1661 | name: 'vendors', // 提取文件命名为vendors,js后缀和chunkhash会自动加 1662 | minChunks: 1, // 只要使用一次就提取出来 1663 | chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的 1664 | minSize: 0, // 提取代码体积大于0就提取出来 1665 | priority: 1, // 提取优先级为1 1666 | }, 1667 | commons: { // 提取页面公共代码 1668 | name: 'commons', // 提取文件命名为commons 1669 | minChunks: 2, // 只要使用两次就提取出来 1670 | chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的 1671 | minSize: 0, // 提取代码体积大于0就提取出来 1672 | } 1673 | } 1674 | } 1675 | } 1676 | } 1677 | ``` 1678 | 1679 | 配置完成后执行打包,可以看到**node_modules**里面的模块被抽离到**verdors.ec725ef1.js**中,业务代码在**main.9a6bf38a.js**中。 1680 | 1681 | ![微信截图_20220616180505.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ed4de313a00f412a9706686c52208177~tplv-k3u1fbpfcp-watermark.image?) 1682 | 1683 | 测试一下,此时**verdors.js**的**chunkhash**是**ec725ef1**,**main.js**文件的**chunkhash**是**9a6bf38a**,改动一下**App.tsx**,再次打包,可以看到下图**main.js**的**chunkhash**值变化了,但是**vendors.js**的**chunkhash**还是原先的,这样发版后,浏览器就可以继续使用缓存中的**verdors.ec725ef1.js**,只需要重新请求**main.js**就可以了。 1684 | 1685 | ![微信截图_20220617102854.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d046a6e0d0f044e88aec3ec9b60e8ada~tplv-k3u1fbpfcp-watermark.image?) 1686 | 1687 | ### 7.7 tree-shaking清理未引用js 1688 | 1689 | [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**功能以此来标记未引入代码然后移除掉,测试一下。 1690 | 1691 | 在**src/components**目录下新增**Demo1**,**Demo2**两个组件 1692 | 1693 | ```tsx 1694 | // src/components/Demo1.tsx 1695 | import React from "react"; 1696 | function Demo1() { 1697 | return

我是Demo1组件

1698 | } 1699 | export default Demo1 1700 | 1701 | // src/components/Demo2.tsx 1702 | import React from "react"; 1703 | function Demo2() { 1704 | return

我是Demo2组件

1705 | } 1706 | export default Demo2 1707 | ``` 1708 | 1709 | 再在**src/components**目录下新增**index.ts**, 把**Demo1**和**Demo2**组件引入进来再暴露出去 1710 | 1711 | ```ts 1712 | // src/components/index.ts 1713 | export { default as Demo1 } from './Demo1' 1714 | export { default as Demo2 } from './Demo2' 1715 | ``` 1716 | 1717 | 在**App.tsx**中引入两个组件,但只使用**Demo1**组件 1718 | 1719 | ```tsx 1720 | // ... 1721 | import { Demo1, Demo2 } from '@/components' 1722 | 1723 | function App() { 1724 | return 1725 | } 1726 | export default App 1727 | ``` 1728 | 1729 | 执行打包,可以看到在**main.js**中搜索**Demo**,只搜索到了**Demo1**, 代表**Demo2**组件被**tree-shaking**移除掉了。 1730 | 1731 | ![微信截图_20220617111640.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a42b846a8b40483fb117653bc8a0691f~tplv-k3u1fbpfcp-watermark.image?) 1732 | 1733 | ### 7..8 tree-shaking清理未使用css 1734 | 1735 | **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**还有标签名称, 安装依赖: 1736 | 1737 | ```sh 1738 | npm i purgecss-webpack-plugin glob-all -D 1739 | ``` 1740 | 1741 | 修改**webpack.prod.js** 1742 | 1743 | ```js 1744 | // webpack.prod.js 1745 | // ... 1746 | const globAll = require('glob-all') 1747 | const PurgeCSSPlugin = require('purgecss-webpack-plugin') 1748 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 1749 | module.exports = { 1750 | // ... 1751 | plugins: [ 1752 | // 抽离css插件 1753 | new MiniCssExtractPlugin({ 1754 | filename: 'static/css/[name].[contenthash:8].css' 1755 | }), 1756 | // 清理无用css 1757 | new PurgeCSSPlugin({ 1758 | // 检测src下所有tsx文件和public下index.html中使用的类名和id和标签名称 1759 | // 只打包这些文件中用到的样式 1760 | paths: globAll.sync([ 1761 | `${path.join(__dirname, '../src')}/**/*.tsx`, 1762 | path.join(__dirname, '../public/index.html') 1763 | ]), 1764 | }), 1765 | ] 1766 | } 1767 | ``` 1768 | 1769 | 测试一下,用上面配置解析图片文件代码拿过来,修改**App.tsx** 1770 | 1771 | ```tsx 1772 | import React from 'react' 1773 | import './app.css' 1774 | import './app.less' 1775 | 1776 | function App() { 1777 | return ( 1778 | <> 1779 |
1780 |
1781 | 1782 | ) 1783 | } 1784 | export default App 1785 | ``` 1786 | 1787 | **App.tsx**中有两个**div**,类名分别是**smallImg**和**bigImg**,当前**app.less**代码为 1788 | 1789 | ```less 1790 | #root { 1791 | .smallImg { 1792 | width: 69px; 1793 | height: 75px; 1794 | background: url('./assets/imgs/5kb.png') no-repeat; 1795 | } 1796 | .bigImg { 1797 | width: 232px; 1798 | height: 154px; 1799 | background: url('./assets/imgs/22kb.png') no-repeat; 1800 | } 1801 | } 1802 | ``` 1803 | 1804 | 此时先执行一下打包,查看**main.css** 1805 | 1806 | ![微信截图_20220617141338.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/782a308293414ba381ea515f29c5db95~tplv-k3u1fbpfcp-watermark.image?) 1807 | 1808 | 因为页面中中有**h2**标签, **smallImg**和**bigImg**类名,所以打包后的**css**也有,此时修改一下**app.less**中的 **.smallImg**为 **.smallImg1**,此时 **.smallImg1**就是无用样式了,因为没有页面没有类名为 **.smallImg1**的节点,再打包后查看 **main.css** 1809 | 1810 | ![微信截图_20220617141901.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fbcd882dc21944d58a3e30bf1816efea~tplv-k3u1fbpfcp-watermark.image?) 1811 | 1812 | 可以看到**main.css**已经没有 **.smallImg1**类名的样式了,做到了删除无用**css**的功能。 1813 | 1814 | 但是**purgecss-webpack-plugin**插件不是全能的,由于项目业务代码的复杂,插件不能百分百识别哪些样式用到了,哪些没用到,所以请不要寄希望于它能够百分百完美解决你的问题,这个是不现实的。 1815 | 1816 | 插件本身也提供了一些白名单**safelist**属性,符合配置规则选择器都不会被删除掉,比如使用了组件库[antd](https://ant.design/), **purgecss-webpack-plugin**插件检测**src**文件下**tsx**文件中使用的类名和**id**时,是检测不到在**src**中使用**antd**组件的类名的,打包的时候就会把**antd**的类名都给过滤掉,可以配置一下安全选择列表,避免删除**antd**组件库的前缀**ant**。 1817 | 1818 | ```js 1819 | new PurgeCSSPlugin({ 1820 | // ... 1821 | safelist: { 1822 | standard: [/^ant-/], // 过滤以ant-开头的类名,哪怕没用到也不删除 1823 | } 1824 | }) 1825 | ``` 1826 | 1827 | ### 7.9 资源懒加载 1828 | 1829 | 像**react**,**vue**等单页应用打包默认会打包到一个**js**文件中,虽然使用代码分割可以把**node_modules**模块和**公共模块**分离,但页面初始加载还是会把整个项目的代码下载下来,其实只需要公共资源和当前页面的资源就可以了,其他页面资源可以等使用到的时候再加载,可以有效提升首屏加载速度。 1830 | 1831 | **webpack**默认支持资源懒加载,只需要引入资源使用**import**语法来引入资源,**webpack**打包的时候就会自动打包为单独的资源文件,等使用到的时候动态加载。 1832 | 1833 | 以懒加载组件和**css**为例,新建懒加载组件**src/components/LazyDemo.tsx** 1834 | 1835 | ```tsx 1836 | import React from "react"; 1837 | 1838 | function LazyDemo() { 1839 | return

我是懒加载组件组件

1840 | } 1841 | 1842 | export default LazyDemo 1843 | ``` 1844 | 1845 | 修改**App.tsx** 1846 | 1847 | ```tsx 1848 | import React, { lazy, Suspense, useState } from 'react' 1849 | const LazyDemo = lazy(() => import('@/components/LazyDemo')) // 使用import语法配合react的Lazy动态引入资源 1850 | 1851 | function App() { 1852 | const [ show, setShow ] = useState(false) 1853 | 1854 | // 点击事件中动态引入css, 设置show为true 1855 | const onClick = () => { 1856 | import('./app.css') 1857 | setShow(true) 1858 | } 1859 | return ( 1860 | <> 1861 |

展示

1862 | {/* show为true时加载LazyDemo组件 */} 1863 | { show && } 1864 | 1865 | ) 1866 | } 1867 | export default App 1868 | ``` 1869 | 1870 | 点击展示文字时,才会动态加载**app.css**和**LazyDemo**组件的资源。 1871 | 1872 | 1873 | ![微信截图_20220617151624.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf0829c6b29743f1a558ed0b1405f30a~tplv-k3u1fbpfcp-watermark.image?) 1874 | 1875 | ### 7.10 资源预加载 1876 | 1877 | 上面配置了资源懒加载后,虽然提升了首屏渲染速度,但是加载到资源的时候会有一个去请求资源的延时,如果资源比较大会出现延迟卡顿现象,可以借助**link**标签的**rel**属性**prefetch**与**preload**,**link**标签除了加载**css**之外也可以加载**js**资源,设置**rel**属性可以规定**link**提前加载资源,但是加载资源后不执行,等用到了再执行。 1878 | 1879 | **rel的属性值** 1880 | 1881 | - **preload**是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源。 1882 | - **prefetch**是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源,会在空闲时加载。 1883 | 1884 | 对于当前页面很有必要的资源使用 **preload** ,对于可能在将来的页面中使用的资源使用 **prefetch**。 1885 | 1886 | **webpack v4.6.0+** 增加了对[预获取和预加载](https://webpack.docschina.org/guides/code-splitting/#prefetchingpreloading-modules)的支持,使用方式也比较简单,在**import**引入动态资源时使用**webpack**的魔法注释 1887 | 1888 | ```js 1889 | // 单个目标 1890 | import( 1891 | /* webpackChunkName: "my-chunk-name" */ // 资源打包后的文件chunkname 1892 | /* webpackPrefetch: true */ // 开启prefetch预获取 1893 | /* webpackPreload: true */ // 开启preload预获取 1894 | './module' 1895 | ); 1896 | ``` 1897 | 1898 | 测试一下,在**src/components**目录下新建**PreloadDemo.tsx**, **PreFetchDemo.tsx** 1899 | 1900 | ```tsx 1901 | // src/components/PreloadDemo.tsx 1902 | import React from "react"; 1903 | function PreloadDemo() { 1904 | return

我是PreloadDemo组件

1905 | } 1906 | export default PreloadDemo 1907 | 1908 | // src/components/PreFetchDemo.tsx 1909 | import React from "react"; 1910 | function PreFetchDemo() { 1911 | return

我是PreFetchDemo组件

1912 | } 1913 | export default PreFetchDemo 1914 | ``` 1915 | 1916 | 修改**App.tsx** 1917 | 1918 | ```tsx 1919 | import React, { lazy, Suspense, useState } from 'react' 1920 | 1921 | // prefetch 1922 | const PreFetchDemo = lazy(() => import( 1923 | /* webpackChunkName: "PreFetchDemo" */ 1924 | /*webpackPrefetch: true*/ 1925 | '@/components/PreFetchDemo' 1926 | )) 1927 | // preload 1928 | const PreloadDemo = lazy(() => import( 1929 | /* webpackChunkName: "PreloadDemo" */ 1930 | /*webpackPreload: true*/ 1931 | '@/components/PreloadDemo' 1932 | )) 1933 | 1934 | function App() { 1935 | const [ show, setShow ] = useState(false) 1936 | 1937 | const onClick = () => { 1938 | setShow(true) 1939 | } 1940 | return ( 1941 | <> 1942 |

展示

1943 | {/* show为true时加载组件 */} 1944 | { show && ( 1945 | <> 1946 | 1947 | 1948 | 1949 | ) } 1950 | 1951 | ) 1952 | } 1953 | export default App 1954 | ``` 1955 | 1956 | 然后打包后查看效果,页面初始化时预加载了**PreFetchDemo.js**组件资源,但是不执行里面的代码,等点击展示按钮后从预加载的资源中直接取出来执行,不用再从服务器请求,节省了很多时间。 1957 | 1958 | ![微信截图_20220617173416.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc0b1038ce1743efbe1570c7834337ce~tplv-k3u1fbpfcp-watermark.image?) 1959 | 1960 | > 在测试时发现只有**js**资源设置**prefetch**模式才能触发资源预加载,**preload**模式触发不了,**css**和图片等资源不管设置**prefetch**还是**preload**都不能触发,不知道是哪里没配置好。 1961 | 1962 | ### 7.11 打包时生成gzip文件 1963 | 1964 | 前端代码在浏览器运行,需要从服务器把**html**,**css**,**js**资源下载执行,下载的资源体积越小,页面加载速度就会越快。一般会采用**gzip**压缩,现在大部分浏览器和服务器都支持**gzip**,可以有效减少静态资源文件大小,压缩率在 **70%** 左右。 1965 | 1966 | **nginx**可以配置**gzip: on**来开启压缩,但是只在**nginx**层面开启,会在每次请求资源时都对资源进行压缩,压缩文件会需要时间和占用服务器**cpu**资源,更好的方式是前端在打包的时候直接生成**gzip**资源,服务器接收到请求,可以直接把对应压缩好的**gzip**文件返回给浏览器,节省时间和**cpu**。 1967 | 1968 | **webpack**可以借助[compression-webpack-plugin](https://www.npmjs.com/package/compression-webpack-plugin) 插件在打包时生成 **gzip** 文章,安装依赖 1969 | 1970 | ```sh 1971 | npm i compression-webpack-plugin -D 1972 | ``` 1973 | 1974 | 添加配置,修改**webpack.prod.js** 1975 | 1976 | ```js 1977 | const glob = require('glob') 1978 | const CompressionPlugin = require('compression-webpack-plugin') 1979 | module.exports = { 1980 | // ... 1981 | plugins: [ 1982 | // ... 1983 | new CompressionPlugin({ 1984 | test: /.(js|css)$/, // 只生成css,js压缩文件 1985 | filename: '[path][base].gz', // 文件命名 1986 | algorithm: 'gzip', // 压缩格式,默认是gzip 1987 | test: /.(js|css)$/, // 只生成css,js压缩文件 1988 | threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k 1989 | minRatio: 0.8 // 压缩率,默认值是 0.8 1990 | }) 1991 | ] 1992 | } 1993 | ``` 1994 | 1995 | 配置完成后再打包,可以看到打包后js的目录下多了一个 **.gz** 结尾的文件 1996 | 1997 | ![微信截图_20220620105008.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/57d3e316c25847ae83897b6044712c86~tplv-k3u1fbpfcp-watermark.image?) 1998 | 1999 | 因为只有**verdors.js**的大小超过了**10k**, 所以只有它生成了**gzip**压缩文件,借助**serve -s dist**启动**dist**,查看**verdors.js**加载情况 2000 | 2001 | ![微信截图_20220620105520.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb621247c02f4e81be85b104d85c6c98~tplv-k3u1fbpfcp-watermark.image?) 2002 | 2003 | 可以看到**verdors.js**的原始大小是**182kb**, 使用**gzip**压缩后的文件只剩下了**60.4kb**,减少了**70%** 的大小,可以极大提升页面加载速度。 2004 | 2005 | ## 八. 总结 2006 | 2007 | 到目前为止已经使用**webpack5**把**react18+ts**的开发环境配置完成,并且配置比较完善的保留组件状态的**热更新**,以及常见的**优化构建速度**和**构建结果**的配置,完整代码已上传到[webpack5-react-ts](https://github.com/guojiongwei/webpack5-react-ts.git) 2008 | 。还有细节需要优化,比如把容易改变的配置单独写个**config.js**来配置,输出文件路径封装。这篇文章只是配置,如果想学好**webpack**,还需要学习**webpack**的构建原理以及**loader**和**plugin**的实现机制。 2009 | 2010 | 本文是总结自己在工作中使用**webpack5**搭建**react+ts**构建环境中使用到的配置, 肯定也很多没有做好的地方,后续有好的使用技巧和配置也会继续更新记录。 2011 | 2012 | 附上上面安装依赖的版本 2013 | 2014 | ```json 2015 | "devDependencies": { 2016 | "@babel/core": "^7.18.2", 2017 | "@babel/plugin-proposal-decorators": "^7.18.2", 2018 | "@babel/plugin-transform-runtime": "^7.18.5", 2019 | "@babel/preset-env": "^7.18.2", 2020 | "@babel/preset-react": "^7.17.12", 2021 | "@babel/preset-typescript": "^7.17.12", 2022 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", 2023 | "@types/react": "^18.0.12", 2024 | "@types/react-dom": "^18.0.5", 2025 | "autoprefixer": "^10.4.7", 2026 | "babel-loader": "^8.2.5", 2027 | "compression-webpack-plugin": "^10.0.0", 2028 | "copy-webpack-plugin": "^11.0.0", 2029 | "core-js": "^3.23.0", 2030 | "cross-env": "^7.0.3", 2031 | "css-loader": "^6.7.1", 2032 | "css-minimizer-webpack-plugin": "^4.0.0", 2033 | "html-webpack-plugin": "^5.5.0", 2034 | "less": "^4.1.3", 2035 | "less-loader": "^11.0.0", 2036 | "mini-css-extract-plugin": "^2.6.1", 2037 | "postcss": "^8.4.14", 2038 | "postcss-loader": "^7.0.0", 2039 | "purgecss-webpack-plugin": "^4.1.3", 2040 | "react-refresh": "^0.14.0", 2041 | "speed-measure-webpack-plugin": "^1.5.0", 2042 | "style-loader": "^3.3.1", 2043 | "thread-loader": "^3.0.4", 2044 | "typescript": "^4.7.3", 2045 | "webpack": "^5.73.0", 2046 | "webpack-bundle-analyzer": "^4.5.0", 2047 | "webpack-cli": "^4.9.2", 2048 | "webpack-dev-server": "^4.9.1", 2049 | "webpack-merge": "^5.8.0" 2050 | }, 2051 | "dependencies": { 2052 | "react": "^18.1.0", 2053 | "react-dom": "^18.1.0" 2054 | } 2055 | ``` 2056 | 2057 | ## 参考 2058 | 2059 | 1. [webpack官网](https://www.webpackjs.com/) 2060 | 2. [babel官网](https://www.babeljs.cn/) 2061 | 3. [【万字】透过分析 webpack 面试题,构建 webpack5.x 知识体系](https://juejin.cn/post/7023242274876162084) 2062 | 4. [Babel 那些事儿](https://juejin.cn/post/6992371845349507108) 2063 | 5. [阔别两年,webpack 5 正式发布了!](https://juejin.cn/post/6882663278712094727) 2064 | 6. [webpack从入门到进阶](https://www.bilibili.com/video/BV1Pf4y157Ni?p=1) 2065 | --------------------------------------------------------------------------------