前言
在平时工作中,为减少开发成本,一般都会使用脚手架来进行开发,比如 create-react-app
。脚手架都会帮我们配置好了 webpack,但如果想自己搭建 webpack 项目要怎么做呢?这边文章将介绍如何使用 webpack 5 来搭建 react 项目,项目地址在文末。
一、简单聊下 Webpack
1.1 Webpack 的好处
试想在不使用任何打包工具的情况下,我们很难在项目去使用 es6+
新语法,TypeScript
即使是新的浏览器,也不支持,更别说在项目中使用 React
、Vue
了。打包工具能帮我们解决这些问题,打包工具有很多,比如 Webpack、Vite、Snowpack、Rspack 等,这里介绍 Webapck,毕竟生态圈大。
Webpack 是一个 JavaScript 应用程序的静态模块打包工具。 它可以帮我们分析项目结构,将模块打包,最终得以在浏览器中直接使用。 Webpack 有哪些好处呢?
开发环境:
- 新特性&新语法: 像
ESNext
新特性,.less、.ts、tsx/jsx、.vue 等浏览器无法识别的格式文件d都能在开发中使用。Webpack 的 Loader 机制能帮助进行转换。 - 模块化: 在 Webpack 中,一切皆为模块, 我们可以使用模块化编程,把复杂的程序细化为小的模块文件。
- 模块热替换(HMR): 提供模块热替换功能, 在修改代码后,不需要重新加载整个页面,只需要替换修改的模块,从而提高开发效率。
- Source Map: 提供了 Source Map 功能,可以将编译后的代码映射回原始源代码,从而方便我们进行调试
生产环境:
- 性能优化:可以压缩代码,合并文件,从而减少网络请求。
- 代码分割:可以进行代码分割,实现按需加载或者并行加载,从而减少页面加载时间,提高用户体验。
- 缓存优化:可以根据文件内容生成 hash 值,从而实现缓存优化,减少网络请求和服务器负载。
1.2 Webpack 的基本概念
这里我们先简单熟悉下 Webpack 基本概念,下面搭建项目时都要用到。
- entry: 使用哪个模块来作为构建的起始入口。
- output: 最终打包后的文件放在哪里,以及如何命名这些文件。
- loader: 是处理文件的转换器,用于对模块源码进行转换,webpack 只能识别 js、json 文件,像 css 、ts 、jsx等文件都需要通过 loader 进行转换。
- plugin: 是一种可扩展的机制,可以打包过程中添加额外的功能。比如打包优化,资源管理,注入环境变量等。
- mode: 对于不同的环境,我们往往需要不同的配置,通过设置
mode
参数来选择环境。
二、搭建 React 项目
上面简单介绍了 webpack,接下来开始搭建我们的项目。
2.1 项目初始化
我们使用 pnpm 来初始化一个项目(8.x 版本需要 node 在 16 + ),为什么选用 pnpm ,可以看下包管理工具 —— 更推荐的 pnpm。
mkdir create-react
cd create-react
pnpm init --y
git init
2.2 安装配置 react & TypeScript
引入 react
、react-dom
和对应的类型包 @types/react 、@types/react-dom。这里使用的版本是18.2.0。
pnpm add react react-dom
pnpm add -D @types/react @types/react-dom
然后配置 TypeScript
pnpm add typescript -D
有了 TypeScript,就可以直接通过 tsc 命令生成一个 tsconfig.json
的配置文件
tsc --init
可以按照所需手动修改 ts 的配置文件。
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"module": "ESNext",
"moduleResolution": "node",
"rootDir": "./src",
"baseUrl": ".",
"paths": {
"common/*": [
"src/common/*"
],
"@/*": [
"src/*"
]
},
"strict": true,
"sourceMap": true,
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"importHelpers": true,
"noUnusedLocals": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src"]
}
接着我们创建 src 目录,在根目录创建 index.tsx
,在 src 下创建 App.tsx 。
// index.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom/client'
import App from './src/App'
const root = ReactDOM.createRoot(document.getElementById('app')!)
// v18 的新方法
root.render(<App />)
// App.tsx
import * as React from 'react'
const App: React.FC = () => {
return <div>hello 小柒</div>
}
export default App
前置准备已经做好, 接下来我们来一步一步的使用 webpack 打包 React 项目。
2.3 Webpack 相关
(1) 安装 Webpack
- webpack 、webpack-cli :打包必备。
- webpack-dev-server: 一个提供热更新的开发服务器,对开发阶段友好。
- webpack-merge: 用来合并配置文件。
pnpm add webpack webpack-cli webpack-dev-server webpack-merge -D
(2) 配置 Webpack 文件
Webpack 默认读取的是 webpack.config.js
文件,但在实际开发中我们需要将生产环境和开发环境分开。我们先来整理下配置文件的目录结构,在 scripts
目录下创建三个配置文件。
修改下 package.json 中的 scripts 配置,用来简化命令。
"scripts": {
"dev": "cross-env NODE_ENV=development webpack serve -c scripts/webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack -c scripts/webpack.prod.js"
},
我们使用 NODE_ENV = production
来设置环境变量,为了在不同的平台上都能使用,我们使用 cross-env 来兼容,这样在不同环境下也能正确获取环境变量。
pnpm add cross-env -D
我们将一些通用的配置写在 webpack.base.js
文件里。
const path = require('path')
module.exports = {
entry: path.resolve(__dirname, '../src/index.tsx'),
output: {
path: path.resolve(__dirname, '../dist'), // 打包后的代码放在dist目录下
filename: '[name].[hash:8].js', // 打包的文件名
},
}
在 webpack.dev.js
文件和 webpack.prod.js
中引入通用配置。
// webpack.dev.js
const { merge } = require('webpack-merge')
const base = require('./webpack.base.js')
module.exports = merge(base, {
mode: 'development', // 开发模式
devServer: {
open: true, // 编译完自动打开浏览器
port: 8080,
},
})
// webpack.prod.js
const { merge } = require('webpack-merge')
const base = require('./webpack.base.js')
module.exports = merge(base, {
mode: 'producton', // 生产模式
})
到这里环境基本搭建好了,接下来我们就一步一步的来完善配置。
(3) 配置 babel
由于 webpack 只能识别js、json 文件, 无法识别 jsx/tsx 文件,此时如果我们尝试启动项目肯定会报错。如何让 webpack 能识别呢?此时我们就需要使用 babel-loader
来转换代码,babel-loader 可以让 webpack 在构建的时候借助 Babel 对JS代码进行转译。
注意💡: Babel 是一个 JavaScript 编译器。主要用于将高版本的JavaScript代码转为向后兼容的JS代码,从而能让我们的代码运行在更低版本的浏览器或者其他的环境中。
babel-loader
的转码功能依赖 Babel 的核心转码包 @babel/core,如果要转义 React 文件还需要引入 @babel/preset-react
这个预设; 对于 ts 我们除了可以使用 ts-loader 外,也可以使用 @babel-preset-typescript
来编译 ts 代码;在实际项目中,考虑到浏览器的兼容性问题,我们都会设置目标浏览器来转换我们的代码,这时候就需要使用到 @babel/preset-env
这个预设。这里就不过多的介绍 babel 配置 ,接下来我们来安装上述提到的关于 babel 的依赖包并进行 webpack 的配置。
pnpm add -D babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript
我们在 webpack.base.js
文件添加下面配置,我们将 js|ts、jsx|tsx 文件都交给 babel-loader 来处理,并配置对应的 presets,这些 presets 会从右向左执行。
{
...
resolve: {
// 配置 extensions 来告诉 webpack 在没有书写后缀时,以什么样的顺序去寻找文件
extensions: ['.mjs','.js', '.json', '.jsx', '.ts', '.tsx'], // 如果项目中只有 tsx 或 ts 可以将其写在最前面
},
module: {
rules: [
{
test: /.(jsx?)|(tsx?)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
targets: 'iOS 9, Android 4.4, last 2 versions, > 0.2%, not dead', // 根据项目去配置
useBuiltIns: 'usage', // 会根据配置的目标环境找出需要的polyfill进行部分引入
corejs: 3, // 使用 core-js@3 版本
},
],
['@babel/preset-typescript'],
['@babel/preset-react'],
],
},
},
},
],
},
...
}
运行 pnpm run build
,打包后会生成 dist 目录,可以看到打包后的 js 文件。
此时如果想要在浏览器中访问,我们需要手动在 dist 目录下添加 html 文件,并引入打包好的 js 文件:
<!DOCTYPE html>
<html lang="zh-cn">
<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.7a68ecf3.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
在浏览器中打开 html 文件,即可访问。
如果想项目启动或打包时自动生成 html 文件 ,要怎么做呢?我们可以借助 html-webpack-plugin 插件来帮忙自动生成 html文件,先在根目录创建一个模板 index.html
文件。
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no, minimum-scale=1, maximum-scale=1, minimal-ui, viewport-fit=cover"
/>
<title></title>
</head>
<body>
<div id="app"></div>
</body>
</html>
安装 html-webpack-plugin
:
pnpm add -D html-webpack-plugin
在 webpack.base.js
文件中添加以下配置:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../index.html'), // 使用自定义模板
}),
],
// ...
}
pnpm run build
一下,dist 目录下已经自动生成了 index.html 文件,并引入了打包的 js 文件。
在 pnpm run dev
启动开发时,我们也能在 localhost:8080 中看到页面了。
注意💡:在使用 dev-server 启动时,它会读取 Webpack 的配置文件(默认是 webpack.config.js),然后将文件打包到内存中(我们看不到 dist 文件夹的生成,Webpack 会打包到硬盘上),用默认地址打开时默认显示index.html 的内容,如果没有index.html 文件,则显示目录。
(4) css 配置
项目中样式的引用那是必不可少的,这里使用 less 来语法来举例。在 src下创建 index.less 文件,在 App.tsx 中引入。此时启动项目,控制台一定会报错。
// index.less
@color: red;
.wrapper {
display: flex;
color: @color;
}
// App.tsx
import * as React from 'react'
import './index.less'
const App: React.FC = () => {
return <div className="wrapper">hello 小柒</div>
}
export default App
less 文件可以使用 less-loader 将 less 编译为 css,一般情况下我们还会使用 Postcss 来处理 CSS,在 Webpack 中我们可以使用 postcss-loader
来处理 css。
注意💡: PostCSS
本身是一个工具,有了它我们可以使用 JavaScript 代码来处理 CSS。它将 CSS 解析成抽象语法树 AST, 将 AST 交给插件来处理并得到结果。PostCSS 的插件体系很强大,提供很多插件,比如:autoprefixer
用来添加浏览器前缀、 cssnano
用来压缩 CSS、 postcss-preset-env
用来根据目标浏览器生成 CSS的 polyfill等等。当然我们也可以实现自己的 PostCSS 插件。
处理过的 css 可以使用 css-loader 来解析成 js ,我们来看看 css-loader 解析之后的内容是什么。
打印出来是一个数组,第二个元素是我们想要的 css 样式。
css-loader 只能帮我们将 css 解析成 js,但不能挂载到元素上。如果想让 css 生效,我们要手动挂载。
这样就达到了我们想要的效果,不过这么写未免有点憨憨。
想要自动挂载样式,style-loader 可以帮我们实现,它负责将 css 样式通过 style 标签插入到 DOM 中。下面是通过 style-loader 实现样式挂载,自动添加 style 标签到head 中。
通过上述,可以来配置我们的 webpack.dev.js 文件了。第一步还是先安装所需要的依赖包。
// less-loader 默认是11版本过高会报错。
// 两种方法:1、要么指定低版本的 less-loader@^6.2.0 2、同时安装 less 和 postcss
pnpm add -D style-loader css-loader postcss-loader less-loader@^6.2.0 postcss-preset-env
// 或者
pnpm add -D style-loader css-loader postcss postcss-loader less less-loader postcss-preset-env
第二步配置 webpack.dev.js
文件。这样我们就可以在代码中正常使用 less 啦~。
// webpack.dev.js
module: {
rules: [
// ...
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
[['postcss-preset-env', {}]]
},
},
},
'less-loader',
],
// 排除 node_modules 目录
exclude: /node_modules/,
},
],
},
我们可以发现,css 样式都打包到最终中的 js 文件了,如果项目比较复杂,css 都打包在js文件,js的体积就会越来越大。在生产环境下,我们肯定希望打包出来的文件体积越小越好,在生产环境下,我们一般是用 MiniCssExtractPlugin 代替 style-loader
,来将打包后的 js 文件的css提取出来,单独创建一个 css 文件,使用 link 的方式引入。除了分离 css文件减小 js 体积, 还可以使用 CssMinimizerWebpackPlugin 优化、压缩来 CSS 体积。 看看 webpack.prod.js
文件中的配置:
// webpack.prod.js
// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = merge(base, {
// ...
module: {
rules: [
{
test: /\.(css|less)$/,
use: [
MiniCssExtractPlugin.loader, // 使用 MiniCssExtractPlugin.loader 代替 style-loader
'css-loader',
{
loader: 'postcss-loader',
options: {
// 它可以帮助我们将一些现代的 CSS 特性,转成大多数浏览器认识的 CSS,并且会根据目标浏览器或运行时环境添加所需的 polyfill;
// 也包括会自动帮助我们添加 autoprefixer
postcssOptions: {
plugins: [['postcss-preset-env', {}]],
},
},
},
'less-loader',
],
// 排除 node_modules 目录
exclude: /node_modules/,
},
],
},
optimization: {
minimizer: [
// 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释
// `...`,
new CssMinimizerPlugin({
// 默认开启
// parallel true: // 多进程并发执行,提升构建速度 。 运行时默认的并发数:os.cpus().length - 1
}),
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'assets/css/[hash:8].css', // 将css单独提测出来放在assets/css 下
}),
],
})
打包后的效果如下:
(5) 图片&字体
先来看看图片的配置,创建文件夹 assets/images
,引入图片 coffee.jpg。
我们在 App.tsx
中引入张图,这里使用 @ 符号来声明路径,需要在 base 文件中进行alias 属性的配置,同时还要对 ts 类型声明进行声明,否则 ts 类型会报错。
此时再编译,会报错,webpack 无法识别图片,需要使用loader 去解析图片资源。
对于图片的处理,在 webpack 5 之前,我们可以使用 file-loader
和 url-loader
,这两个 loader 都可以帮我们解析图片资源。
注意💡
file-loader
:不仅仅可以处理图片资源,本质是处理文件导入地址并替换成其访问地址,并把文件输出到相应位置,音视频等资源也可以使用它。url-loader
:file-loader 的升级版,包含 file-loader 的全部功能,并且能够根据配置将符合配置的文件转换成 Base64 方式引入,将小体积的图片 Base64 引入项目可以减少 http 请求,也是一个前端常用的优化方式。
我们先来看看 webpack 5 之前如何配置。先使用 file-loader
来处理图片资源,可以看到图片是一个可访问的地址。
再使用url-loader
看看效果,在不设置 limit 限制时,会转换成 Base64 格式引入。
当我们配置了 limit 时(这里设置比较小,项目中应合理配置),图片超过这个限制就会使用file-loader
去处理图片,表现如上。一般情况下我们使用url-loader
来处理。在webpack.base.js
中配置:
// webpack.base.js
resolve: {
//...
alias: {
'@': path.resolve(__dirname, '../src'),
},
},
module: {
rules: [
//...
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
use: [
// {
// loader: 'file-loader',
// },
{
loader: 'url-loader',
options: {
limit: 2000,
// //限制打包图片的大小:
// //如果大于或等于2000Byte,则按照相应的文件名和路径打包图片;如果小于2000Byte,则将图片转成base64格式的字符串。
// name: 'img/[name].[hash:8].[ext]',
// //img:图片打包的文件夹;
// //[name].[ext]:设定图片按照本来的文件名和扩展名打包,不用进行额外编码
// //[hash:8]:一个项目中如果两个文件夹中的图片重名,打包图片就会被覆盖,加上hash值的前八位作为图片名,可以避免重名。
},
},
],
},
]
}
了解图片的配置方式后,我们同样可以使用 url-loader 去处理字体。一般数字类型都用din,这里我们引入一个ttf字体,使用@font-face 来定义字体名。
接着在 webpack.base.js 文件中配置下。
// webpack.base.js
module: {
rules: [
// ...
{
test: /\.(eot|ttf|woff|woff2)$/i,
use: [
{
loader: 'url-loader',
},
],
},
}
]
}
这样我们就能使用 din 字体了,编译运行,字体生效~。
Webpack 5 中的 asset module 其实已经帮我们处理了,可以直接使用。资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。
这里我们使用 asset 类型,并配置 parser.dataUrlCondition.maxSize
属性,小于 maxSize 的会被打包成 base64,否则会被打包到目录,以url的形式引入。话不多说直接上配置。
module: {
rules: [
// ...
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 25 * 1024, // 25kb
},
},
generator: {
filename: 'assets/imgs/[name].[hash:8][ext]',
},
},
{
test: /\.(eot|ttf|woff|woff2)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 25 * 1024, // 25kb
},
},
generator: {
filename: 'assets/fonts/[name].[hash:8][ext]',
},
},
]
}
build 一下,图片和字体都被分别打包再 imgs 和 fonts下了。
三、小结
项目上需要用到的基本上都配置完了,当然这只是一个简单的项目配置,随着项目的复杂度变高,webpack 的配置也不同。webpack 5 其实已经内置了很多新的特性,比如内置的 tree-shaking
、内置静态资源构建能力、持久化缓存等,让开发体验更友好了。感兴趣的小伙伴也可以试着尝试下哦~。
项目地址: 手把手教你搭建 Webpack 5 + React 项目