前言

本文初衷:平时的项目大多用create-react-app开发,从开发到打包都只是在敲命令而见不到webpack.config.js配置文件,简直是面向黑箱编程,遇到问题之后即使解决了也不知道怎么回事,这种感觉非常不好。

学习 webpack 的重要性不言而喻,即使市面上已经有如此众多的成熟脚手架,比如普通项目可以用 CRA,SPA 管理系统可以用 antdpro,打包组件库可以用 tsdx 等等,但如果不懂这些打包工具的原理甚至基础用法,总有一天你会遇到奇葩问题而不知道如何解决。

本文将以问题导向的形式,在实际搭建过程中逐个剖析webpack重要配置,深浅适宜,整体内容较基础,适合初入坑 webpack 的小伙伴们参考。

本示例 webpack 版本为5.xwebpack-cli版本为4.x

话不多说,马上开始吧~

正文

1.项目初始化

1
2
3
4
mkdir webapck-ts-react
cd webapck-ts-react
yarn init
yarn add webpack webpack-cli -D

空项目中初始化为以下结构:

image.png

🤔 问题1:webpack是什么?

👉 展开查看答案 - webpack是一个打包工具;将符合`ES Module`和`CommonJS`模块化规范的工程文件打包成一个静态资源(可部署到服务器)

image.png

一张图讲清楚webpack的作用

此时直接执行npx webpack命令试试看吧:

1
webpack

神奇的事情发生了,会发现多出了个文件夹dist,里面是打包编译好的文件main.js,如果我们没有在webpack.config.js中配置任何内容,则默认按照相应出入口进行打包,默认命令类似:

1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js

const path = require('path')

module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}

再次命令行执行npx webpack结果一样,验证默认配置就是上面这样的~

配置项中entry为入口,可以配置为相对路径;
output为出口,path属性必须设置为绝对路径;

为什么输出路径要求绝对路径?

以上差异原因在于项目中入口一般可以确定为本项目中,但是出口理论上可以是磁盘上任意值,所以output的path必须为绝对路径。

outputfilename在单入口项目中可以写任意固定值,在多入口项目中不能写固定值,[name]为变量占位符表示不固定的值;

🤔 问题2:为什么要npx webpack而不是直接webpack?

👉 展开查看答案 webpack打包命令默认有两种方式:全局和本地(局部); 如果直接执行webpack则用的是全局webpack编译,结果一样的嗷; 如果使用npx webpack则会在当前项目中寻找webpack指令执行,查找路径为/node_modules/bin/webpack

image.png

🤔 问题3:全局有webpack命令不就够了吗?为啥本地还要安装webpack?

👉 展开查看答案 全局安装的都是固定版本(比如最新的5.x),有些年代久远的项目需要需要使用更早期的webpack版本(比如4.x),为了防止版本冲突,所以开发中一般都是用项目本地版本

不过每次都要npx webpack未免太麻烦了,所以我们可以在package.json中做如下配置:

1
2
3
4
5
6
// package.json
...
"scripts": {
"build": "webpack"
},
...

之后直接执行yarn build就和执行npx webpack效果一样啦~

2.处理图片loader

接着我们发挥下webpack模块化打包的特性,新建一个模块专门在页面上加载图片:

1
2
3
4
5
6
7
8
// src/loadImg.js

import Img from './images/picture.jpg'

const Image = document.createElement('img')
Image.src = Img

document.body.appendChild(Image)

index.js中引入

1
2
3
4
5
6
7
require('./loadImg')

function sum (a, b) {
return a + b
}

console.log(sum(1, 2))

执行yarn build发现报错:

image.png

提示得很清楚啦,由于webpack默认只认识.js.json文件,对于图片文件的识别是需要借助loader的;

🤔 问题4:loader是什么?

👉 展开查看答案 webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。

在webpack4.x版本中处理图片需要用到file-loader,url-loader或raw-loader,但是在webpack5.x中不需要了,对于图片和字体文件等,可以通过type: asset声明直接处理文件。

这里我们采用5.x的方式处理图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const path = require('path')

module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
+ module: {
+ rules: [
+ {
+ test: /\.(png|jpg|jpeg|gif|webp)$/,
+ type: 'asset'
+ }
+ ]
+ }
}

打包成功:

image.png

新建HTML文件,引入打包后的main.js文件测试,注意script标签一定要加defer属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dist/index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script defer src="./main.js"></script>
</head>
<body>

</body>
</html>

🤔 机智如你已经发现了,直接在dist文件夹中新建额外文件的操作不对劲吧,别急,后面会有plugin帮我们自动处理的。

打开dist/index.html预览,一切正常:

image.png

3.处理css文件loader

让我们新建一个css文件

1
2
3
4
5
6
// src/css/index.css

body {
background-color: burlywood;
color: blueviolet;
}

引入

1
2
3
4
5
6
7
8
9
src/index.js
require('./loadImg')
+ import './css/index.css'

function sum (a, b) {
return a + b
}

console.log(sum(1, 2))

不出所料,还是同样内容的报错:缺少合适的loader,因为上面我们已经知道了,webpack默认只能识别js文件和JSON文件,其他格式文件都需要loader帮助识别处理。
安装处理css的loader

1
yarn add style-loader css-loader -D

配置文件中指定.css文件的解析所用loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
module.exports = {
...
module: {
rules: [
{
test: /\.(png|jpg|jpeg|gif|webp)$/,
type: 'asset'
},
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader']
+ }
]
}
}

再次yarn build,无报错而且样式生效

image.png

🤔 问题5:css-loader我猜是解析css的,那么style-loader是干啥的?

👉 展开查看答案 css-loader仅能识别并打包css文件,而style-loader将打包出来的css样式插入到HTML的head中,使其在页面上生效

4.打包模式mode

接下来解决打包模式警告问题:

image.png

只需要在webpack.config.js中指定mode配置项即可

image.png

mode参数有两种:developmentproduction,默认为production,这两种模式各有一套默认配置:

Mode: development

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// webpack.development.config.js

module.exports = {
mode: 'development'
devtool: 'eval',
cache: true,
performance: {
hints: false
},
output: {
pathinfo: true
},
optimization: {
moduleIds: 'named',
chunkIds: 'named',
mangleExports: false,
nodeEnv: 'development',
flagIncludedChunks: false,
occurrenceOrder: false,
concatenateModules: false,
splitChunks: {
hidePathInfo: false,
minSize: 10000,
maxAsyncRequests: Infinity,
maxInitialRequests: Infinity,
},
emitOnErrors: true,
checkWasmTypes: false,
minimize: false,
removeAvailableModules: false
},
plugins: [
new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
]
}

Mode: production

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// webpack.production.config.js
module.exports = {
mode: 'production',
performance: {
hints: 'warning'
},
output: {
pathinfo: false
},
optimization: {
moduleIds: 'deterministic',
chunkIds: 'deterministic',
mangleExports: 'deterministic',
nodeEnv: 'production',
flagIncludedChunks: true,
occurrenceOrder: true,
concatenateModules: true,
splitChunks: {
hidePathInfo: true,
minSize: 30000,
maxAsyncRequests: 5,
maxInitialRequests: 3,
},
emitOnErrors: false,
checkWasmTypes: true,
minimize: true,
},
plugins: [
new TerserPlugin(/* ... */),
new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.NoEmitOnErrorsPlugin()
]
}

5.借助babel打包react项目

当前的webpack配置已经能够打包js和css以及图片文件了,接下来我们让它支持react项目的打包;
众所周知,打包react项目的核心工作就是转化其jsx语法,这就不得不提到babel了。

🤔 问题6:什么是babel?

👉 展开查看答案 Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。除此之外还能为你做的事情有:
  1. 语法转换

  2. 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过 @babel/polyfill 模块)

  3. 源码转换 (codemods)

babel的使用方法:
一个核心包@babel/core必须安装的,其余功能可以通过配置插件plugins或预设presets实现,这里我们要转化jsx语法,可以直接使用@babel/preset-react这个预设(预设就是一堆插件的合集方案),考虑到在webpack中使用babel,所以还要用到babel-loader

1
yanr add @babel/core @babel/preset-react babel-loader -D

当然react和react-dom也需要安装到生产依赖中

1
yarn add react react-dom

新建index.jsx文件写入react代码

1
2
3
4
5
6
// src/index.jsx

import React from 'react'
import ReactDOM from 'react-dom'

ReactDOM.render(<div>React组件测试</div>, document.getElementById('root'))

配置文件中更改打包入口并增加jsx解析规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// webpack.config.js

const path = require('path')

module.exports = {
- // entry: './src/inde.jsx',
+ entry: './src/index.jsx',
mode: 'development',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.(png|jpg|jpeg|gif|webp)$/,
type: 'asset'
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader']
},
+ {
+ test: /\.jsx?/,
+ use: [
+ {
+ loader: 'babel-loader',
+ options: {
+ presets: ['@babel/preset-react']
+ }
+ }
+ ]
+ }
]
}
}

执行yarn build打包;
更改dist/index.html文件新增id为root的节点

1
2
3
4
5
6
...
</head>
<body>
<div id="root"></div>
</body>
</html>

可以发现编译成功:

image.png

6.配置plugin

(1)html-webpack-plugin

之前的操作中我们多次手动修改dist文件夹下的内容,这种操作肯定是不被允许的,所以我们需要配置模板,借助html-webpack-plugin自动生成这个测试用的HTML文件

1
yarn add html-webpack-plugin -D

src目录先新建index.html模板文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webpack搭建TS版React开发环境</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

修改配置项,增加插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

module.exports = {
entry: './src/index.jsx',
mode: 'development',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
module: {
...
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}

此时yarn build打包,发现dist文件夹下已经自动生成了模板文件,并且自动引入了main.js打包文件

(2)clean-webpack-plugin

见名知意,这个插件作用很简单,就是在每次打包生成新的打包文件之前自动删除所有老的打包文件

1
yarn add clean-webpak-plugin -D
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// webapck.config.js

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
...

module.exports = {
entry: './src/index.jsx',
mode: 'development',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[chuankhash].[name].js'
},
optimization: {
splitChunks: {
chunks: 'all'
}
},
module: {
...
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new CleanWebpackPlugin()
]
}

7.支持TS版React项目编译

如何让webpack支持ts呢,其实这个问题和如何支持jsx语法一样性质,对于代码转化工作都是要loader去做。

以下提供两种方案用来支持React组件的TS写法,无论哪种都要先在本地安装typescript

1
yarn add typescript -D

生成tsconfig.json配置文件

1
yarn tsc --init

方案一:babel-loader的@babel/preset-typescript

一种方法就是沿用babel-loader,通过增加预设preset来支持ts解析:

1
yarn add @babel/preset-typescript -D

src/index.jsx改名为src/index.tsx,同时打包配置文件中的entry也要改为entry: './src/index.tsx'
babel-loader的presets数组增加预设:@babel/preset-typescript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// webpack.config.js

const path = require('path')

module.exports = {
entry: './src/index.tsx',
mode: 'development',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[chuankhash].[name].js'
},
module: {
rules: [
....
{
test: /\.tsx?/,
use: [
{
loader: 'babel-loader',
options: {
+ presets: ['@babel/preset-react', '@babel/preset-typescript']
}
}
]
}
]
},
}

执行yarn build可以成功打包;

但是这种方案下,很多typescript语法是不被支持的,比如我们新建一个Comp组件故意写出错误的类型定义:

1
2
3
4
5
6
7
8
9
10
const Comp = () => {
const list: number[] = ['1', 'abc']
let peekValue: string
peekValue = list.pop()
return (<>
<div>这是COMP组件{peekValue}</div>
</>)
}

export default Comp

执行yarn build可以看到:

image.png

说明这种方案虽然能够打包TS,但是无法在打包过程中对TS错误语法进行校验,如果既想打包又想校验怎么办呢?这是就要用到另一个loader了:

方案二:ts-loader

1
yarn add ts-loader -D

配置文件中移除@babel/preset-typescript预设并增加ts-loader后执行打包:

image.png

可以看到一下子出了16个error,可见ts-loader能在打包过程中对不符合规则的ts语法做校验的。

解决报错的过程分别为

  1. tsconfig.json配置”jsx”: “react”

  2. yarn add @types/react @types/react-dom

  3. 解决具体语法报错

8.优化开发体验webpack-dev-server

目前每次重新打包之后都要手动查看HTML文件变更,太不“自动化”了,其实webpack允许我们开启一个本地服务监听打包过程自动更新页面,而且还能热更新。

1
yarn add webpack-dev-server -D

开启打包服务,在4.x版本中需要修改命令为:webpack-dev-server
而在5.x版本中只要:webpack serve

1
2
3
4
5
6
7
8
9
10
// package.json

{
...
"scripts": {
"build": "webpack",
"dev": "webpack serve"
},
...
}

此时执行yarn dev即可观察到已经开启打包监听,devSer的具体配置项如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack.config.js

module.exports = {
...
devServer: {
contentBase: path.join(__dirname, "dist"), // * 服务启动根目录(除了main.js所在目录之外的静态服务目录)
compress: true, // * 为每个静态文件开启 gzip compression
open: true, // * 是否自动打开浏览器,默认false不打开
port: 8081, // * 自定义服务端口,默认为8080
hot: true, // * 是否开启模块热更新,默认为false不开启
proxy: { // * 本地正向代理(常用于非同源请求)
"/api": {
target: "http://localhost:3000",
pathRewrite: {
"^/api": "",
},
},
},
},
...
}

那么至此,一个ts版的react开发环境就搭建好了,剩下一些自定义配置完全根据各自公司项目需要了,比如我们项目习惯用sass module模式开发。

9.支持sass module开发模式

安装sass-loader和node-sass

1
yarn add sass-loader node-sass -D
1
2
3
4
5
6
7
8
9
10
11
12
// src/comp.module.scss

.wrap {
.head {
font-size: 20px;
color: blueviolet;
}
.body {
font-size: 14px;
color: yellowgreen;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/Comp.tsx

import React from 'react'
import styles from './comp.module.scss'

const Comp = () => {
const list: string[] = ['1', 'abc']
let peekValue: string
peekValue = list.pop() as string
return (<div className={styles.wrap}>
<div className={styles.head}>这是COMP组件</div>
<div className={styles.body}>测试使用</div>
</div>)
}

export default Comp

配置文件中增加一个解析规则:

image.png

为了配合一下TS,还要新建个类型声明文件

1
2
3
4
5
6
7
8
9
10
11
12
// typed-css.d.ts

// scss模块声明
declare module '*.scss' {
const content: {[key: string]: any}
export = content
}
// less模块声明
declare module '*.less' {
const content: { [key: string]: any }
export default content
}

10.实现react模块热替换(HMR)

1
yarn add @pmmmwh/react-refresh-webpack-plugin react-refresh -D
1
2
3
4
5
6
7
8
9
10
11
12
// webpack.config.js

const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
...
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
+ new ReactRefreshPlugin()
],
...

11.配置路径别名

一定要照着下面的配

1
2
3
4
5
6
7
8
9
10
// webpack.config.js
module.exports = {
...
resolve: {
extensions: [".js", ".json", ".ts", ".tsx"],
alias: {
'@': path.resolve(__dirname, './src')
}
...
}
1
2
3
4
5
6
7
8
9
10
11
// tsconfig.json

{
"compilerOptions": {

"baseUrl": "./src",
"paths": {
"@compoents": ["./components/*"],
"@/*": ["./*"],
},
}

image.png

image.png

结语

至此,一款工作中能用的TS版React开发环境已经搭建完毕~

现已具备功能:

  • typescript语法
  • sass module
  • 模块热替换
  • 路径别名
  • 解析图片和CSS
  • source-map
    后期可支持项:
  • 第三方包优化,treeshaking,cdn等
  • 生产环境配置文件分离
  • 生产环境包体积和chunkname优化

文中项目源码:webpack-ts-react-lead