# Webpack 搭建 React/TS 项目(一)
最近通过 npx create-react-app
创建了一个 react 的项目,但是查看 webpack 配置时有点慌。发现自己的项目似乎不太需要这么复杂的配置,所以打算自己搭建一个试试。
万事开头难,那先从从简单的 yarn init -y
开始。
# Webpack 的基础配置
# webpack 依赖安装
yarn add webpack webpack-cli webpack-dev-server webpack-merge -D
yarn add html-webpack-plugin -D
# 增加 webapck.config.js 文件
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const devServer = require("webpack-dev-server");
module.exports = {
mode: "development", // 后续会针对生产环境和开发环境做区分
entry: path.resolve(__dirname, "./src/index"),
output: {
path: path.resolve(__dirname, "./dist"),
filename: `[name].[fullhash].js`, // 这里的 fullhash 取代了原来的 hash
clean: true, // webapck@5.20 开始支持的,清理构建文件夹的配置,不再需要 clean-webpack-plugin 插件
},
devServer: {
port: 9527,
},
plugins: [
new HtmlWebpackPlugin({
title: "Webpack5和React18的搭配",
template: path.resolve(__dirname, "./public/index.html"),
}),
],
};
# 根据 webapck.config.js 文件,创建了以下文件:
- 根据入口文件 entry 配置,创建 src 目录及 index.js 文件
console.log("winter's coming, 凛冬将至~");
- 根据插件 html-webpack-plugin 创建了 public 文件夹及 一个空白的 index.html 文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 这个写法就能把 webpack.config.js 中htmlWebpackPlugin配置的title写入模板 -->
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
</body>
</html>
- 在 package.json 中配置开发环境启动指令
"scripts": {
"dev": "webpack serve --config webpack.config.js",
"build": "webpack"
},
经过上述一系列的配置后,就完成了一个基础的 webpack 构建服务。可以执行 yarn dev
启动 webpack-dev-server 服务,可以执行 yarn build
进行打包。
# 关于 webpack-dev-server
# 当前目录结构
.
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ └── index.js
├── webpack.config.js
└── yarn.lock
# package.json 文件信息
{
"name": "webpack5-react18",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "webpack serve --config webpack.config.js",
"build": "webpack"
},
"devDependencies": {
"html-webpack-plugin": "^5.5.3",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpack-merge": "^5.9.0"
}
}
# React 的加入
# react 依赖安装
yarn add react react-dom
# 更新 src/index.js
// index.js
import React from "react";
import ReactDom from "react-dom/client";
import App from "./App";
const root = ReactDom.createRoot(document.getElementById("app"));
root.render(<App />);
# 添加 src/App.js
import React from "react";
const App = () => {
return <div>我是一个简单的 React 页面</div>;
};
export default App;
再次执行 yarn dev
启动服务时,就不出意外的报错了。
ERROR in ./src/index.js 8:12
Module parse failed: Unexpected token (8:12)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| const root = ReactDom.createRoot(document.getElementById('app'))
|
> root.render(<App />)
webpack 5.88.2 compiled with 1 error in 18 ms
# ✨ 它需要一个 loader,一个能解决 React 语法模式的工具。 ✨
# 那么 Loader 是什么?
# 用 Loader 来解决 React 报错
在 webpack 中 loader 的配置在 module.rules
中。
rules 是一个包含多个 json 的数组形式。也就是说只要你想,可以配置 N 个 loader。
在配置 loader 时,首先需要知道自己需要用什么 loader 来解决问题。比如 react 的解决需要以下几个 loader。
react 需要的 loader
// webpack 配置的loader
yarn add babel-loader -D
// babel-loader 需要用到的插件
yarn add @babel/core @babel/preset-env @babel/preset-react -D
# webpack.config.js 配置更新
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
entry: path.resolve(__dirname, "./src/index.js"),
output: {
path: path.resolve(__dirname, "./dist"),
filename: `[name].[hash].js`,
},
devServer: {
port: 9528,
},
// 这里配置了 loader
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
title: "Webpack5和React18的搭配",
template: path.resolve(__dirname, "./public/index.html"),
inject: "body",
}),
],
};
# Babel 的出现
上面通过 loader 来解决 react 报错问题时,我们安装了四个依赖:babel-loader
、@babel/core
、 @babel/preset-env
和 @babel/preset-react
。
而 webpack.config.js 中的 loader 中配置了其中三个:babel-loader
、@babel/preset-env
和 @babel/preset-react
。
@babel/xxx
上述所有和 Babel 相关的模块都是以 @babel/ 的方式开头的,这是因为从版本 Babel@7 开始,所有的 Babel 模块都是作为独立的 npm 包发布的。这种模块化的设计能够让每种工具都针对特定使用情况进行设计。
babel-loader 作为 webpack 和 babel 之间沟通的桥梁,通过调用
babel/core
的 api 来告诉 webpack 如何应用提供的插件去调用相关的规则去编译 js。@babel/core 是 Babel 的核心功能,使用到了 Babel 就肯定会安装这个依赖。
@babel/preset-env 官方提供的一个预设,支持将最新的 JS 语法转换成目标环境支持的语法。
@babel/preset-react 官方提供的 react 预设,满足 Babel 转换 JSX 语法。
没有 @babel/preset-react 配置时会产生如下报错:
// 为什么要加入 @babel/preset-react
ERROR in ./src/index.js
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: /Users/hanzhizhen/code-study/webpack-study/webpack-react/src/index.js: Support for the experimental syntax 'jsx' isn't currently enabled (7:13):
5 | const root = ReactDom.createRoot(document.getElementById('app'))
6 |
> 7 | root.render(<App />)
| ^
8 |
Add @babel/preset-react (https://github.com/babel/babel/tree/main/packages/babel-preset-react) to the 'presets' section of your Babel config to enable transformation.
# 关于 babel
上述提到的依赖里有一个共同的单词 babel
,这个是什么东西呢? 简单了解一下。🤔️🤔️🤔️
# Performance 配置问题
完成上述配置后,我碰到一个警告报错,导致项目无法启动。报错信息如下;
WARNING
asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
main.d26c883b47e3cbb4cca6.js (262 KiB)
WARNING
entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
main (262 KiB)
main.d26c883b47e3cbb4cca6.js
WARNING
webpack performance recommendations:
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit https://webpack.js.org/guides/code-splitting/
这个问题主要参考 webpack 的 Performance 配置即可 (opens new window)
module.expoerts = {
mode: "",
entry: "",
output: {},
devServer: {},
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000,
},
module: {},
plugins: [],
};
# 项目中 CSS 配置
假如我们在项目中使用 less 语法来处理样式问题。
yarn add css-loader style-loader less less-loader -D
# 增加 app.less 文件
.app-page {
margin: 30px auto;
border: 3px solid #000;
border-radius: 4px 0 4px 0;
.test-less {
font-size: 16px;
font-weight: bold;
font-style: italic;
background-color: red;
}
}
# 改写 app.js 文件
import React from "react";
import "./App.less";
const App = () => {
return (
<div className="app-page">
我是一个越来越不简单的 React 页面
<p className="test-less">我来测试 less 的包裹写法</p>
</div>
);
};
export default App;
# 更新 webpack.config.js 配置
{
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
},
{
test: /\.(css|less)$/,
use: ['style-loader', 'css-loader', 'less-loader']
}
]
},
}
webpack 中的针对以 .css
和.less
结尾的文件,配置了三个 loader:
less-loader
把 less 文件转换成合法的 css 文件;css-loader
它可以解析 css 中的@import
和url()
标记的模块,并把它们转换成require()
语句,从而将他们引入到 Javascript 模块中。同时 css-loader 还可以生成 css 模块,这样就可以在 js 中直接引入 css 样式。但是需要注意的是,css-loader 必须配合 style-loader 一起使用。style-loader
把编译好的样式放到 style 标签中,同时嵌入 html 文件中。
最终页面的效果和源码结构如图,此时的样式是被 style 标签包裹着的:
# CSS 样式拆分
如何将 css 样式单独拆分到一个文件中,而不是直接嵌入到 style 中呢。
yarn add mini-css-extract-plugin -D
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
mode: "development", // 后续会针对生产环境和开发环境做区分
entry: path.resolve(__dirname, "./src/index"),
output: {
path: path.resolve(__dirname, "./dist"),
filename: `[name].[hash].js`,
clean: true, // webapck@5.20 开始支持的,清理构建文件夹的配置,不再需要 clean-webpack-plugin 插件
},
devServer: {
port: 9527,
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
},
{
test: /\.(css|less)$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
// 为 CSS 内的图像、文件等外部资源指定自定义公共路径。默认是指向 output.publicPath 目录下,就是 dist/xxx
// 设置 publicPath 后,css 的资源地址都会是 css/xxx
publicPath: "css/", // 这里只做举例展示
},
},
"css-loader",
"postcss-loader",
"less-loader",
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
title: "Webpack5和React18的搭配",
template: path.resolve(__dirname, "./public/index.html"),
}),
new MiniCssExtractPlugin({
// filename 可以通过配置成目录地址,来决定把css放到哪个目录下,这里需要配合 loader中设置的 publichPath 来处理
// filename 默认值是 [name].css
filename: "css/[name].css",
}),
],
};
通过插件的处理,css 被单独抽离到了一个 mian.css 文件中同时使用 link 标签引入处理。页面源码结构如下:
# CSS 打包压缩
当前的配置执行 yarn build
后 css 文件没有被压缩,看文档很多用的是插件 optimize-css-assets-webpack-plugin
。
不过 webpack 官方推荐了 css-minimizer-webpack-plugin
。
据说是在 source maps 和 assets 中使用查询字符串会更加准确,支持缓存和并发模式下运行。
module.exports = {
entry: "",
output: "",
resolve: "",
performance: "",
module: "",
plugins: "",
optimization: {
minimizer: [new CssMinimizerPlugin()],
},
};
# CSS 的 tree-shaking
webpack5 针对 js 的压缩默认是使用了插件 TerserWebpackPlugin
进行压缩 和 Tree Shaking。
但是 css 并没有做任何 tree-shaking 操作,很多老的项目会有很多冗余的 css 被打包到最终资源中。
从参考文档中看到了一个插件可以对 css 进行 tree-shaking,试了一下还不错。
yarn add purgecss-webpack-plugin glob -D
const { PurgeCSSPlugin } = require("purgecss-webpack-plugin");
module.exports = {
// ... 别的配置
plugins: [
// 别的插件
new PurgeCSSPlugin({
// 这里的后缀用的 .tsx是因为后面全都升级到了 ts 形式。别的文件可以自己更改
paths: glob.sync(path.resolve(__dirname, "src/**/*.tsx"), {
nodir: true,
}),
}),
],
};
# postcss 的配置
yarn add postcss-loader postcss-preset-env -D
项目开发时,有时候需要解决以下一些问题:
一些 css 样式为了兼容老版本的浏览器,需要添加内核前缀。比如
display: flex;
就有display: -webkit-box;
和display: -moz-box;
的多种方式。为了让 css 在编译时就能自动添加这些前缀,需要通过 postcss 进行处理。如果颜色值设置成
#12345678
这种八位模式时,其中每两位数字分别代表着r
、g
、b
、a
。 但是有些浏览器无法识别八位数字模式,可以通过 postcss 的插件postcss-preset-env
来做转化
# 更新 src/App.js
import React from "react";
import "./App.less";
const App = () => {
return (
<div className="app-page">
我是一个越来越不简单的 React 页面
<p className="test-less">我来测试 less 的包裹写法</p>
<div className="test-postcss">
<div>测试postcss-left</div>
<div>测试postcss-right</div>
</div>
</div>
);
};
export default App;
# 更新 src/App.less
.app-page {
margin: 30px auto;
border: 3px solid #000;
border-radius: 4px 0 4px 0;
.test-less {
font-size: 16px;
font-weight: bold;
font-style: italic;
background-color: red;
}
.test-postcss {
display: flex;
align-items: center;
justify-content: space-between;
background: #12345678;
div:nth-of-type(1) {
background: red;
}
div:nth-of-type(2) {
background: green;
}
}
}
# webpack.config.js 调整
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
}
}
},
{
test: /\.(css|less)$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
// 为 CSS 内的图像、文件等外部资源指定自定义公共路径。默认是指向 output.publicPath 目录下,就是 dist/xxx
// 设置 publicPath 后,css 的资源地址都会是 css/xxx
publicPath: "css/", // 这里只做举例展示
}
},
'css-loader',
'postcss-loader',
'less-loader'
]
}
]
},
# 添加文件 .browserslistrc
.browserslistrc
的文件必须添加后才能实现内核前缀的自动添加
> 1%
iOS >= 7
Android > 4.1
Firefox > 20
# 添加文件 postcss.config.js
module.exports = {
plugins: ["postcss-preset-env"],
};
之前为 css 样式添加内核前缀,都是使用了另一个插件autoprefixer
,这里直接使用 postcss-preset-env
进行了替代。因为这个插件有以下两个特点:
它可以帮助我们将一些现代的 CSS 特性,转成大多数浏览器认识的 CSS,并且会根据目标浏览器或运行时环境添加所需的 polyfill;
会自动帮助我们添加 autoprefixer(所以相当于已经内置了 autoprefixer)。我们查看
postcss-preset-env
的依赖时会发现是有 autoprefixer 的。
对相关的 dom 节点设置 flex 属性 和 八位数字的颜色样式后,运行项目可以看到 postcss 起了作用:
# 图片配置
由于现在公司有自己的图床平台,项目开发时的图片地址都是先上传图床,再直接使用地址在项目里直接使用的,所以图片的配置耗费了将近半天的时间。
这也说明了,只要平时足够卷,上班就有足够的 摸 🐟 时间 😂😂😂
图片的配置我尝试了两种:webpack5 之前的版本(url-loader 和 file-loader)和 webpack5 版本的
# 添加 src/assets 目录保存图片
.
├── package.json
├── postcss.config.js
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── app.less
│ ├── app.tsx
│ ├── assets
│ │ ├── 0-0.png
│ │ └── ai.png
│ └── index.tsx
├── webpack.config.js
└── yarn.lock
# 更新 app.js 文件
import "./app.less";
import aiImage from "./assets/ai.png";
const App = () => {
return (
<div className="app-page">
我是一个越来越不简单的 React 页面
<p className="test-less">我来测试 less 的包裹写法</p>
<div className="test-postcss">
<div>测试postcss-left</div>
<div>测试postcss-right</div>
</div>
<img src={aiImage} />
<div className="test-img"></div>
</div>
);
};
export default App;
# 更新 app.less 文件
.app-page {
margin: 30px auto;
border: 3px solid #000;
border-radius: 4px 0 4px 0;
.test-less {
font-size: 16px;
font-weight: bold;
font-style: italic;
background-color: red;
}
.test-postcss {
display: flex;
align-items: center;
justify-content: space-between;
background: #12345678;
div:nth-of-type(1) {
background: red;
}
div:nth-of-type(2) {
background: green;
}
}
.test-img {
height: 50px;
border: 1px solid #123456;
margin: 10px;
background: url(./assets/0-0.png);
background-size: auto 50px;
}
}
# file-loader 与 url-loader
# 一、区别
首先,在没有仔细看 file-loader 和 url-loader 文档的事实上,我吹两句:file-loader 和 url-loader 差不多。😅😅😅
至少在本次的配置中,用到的配置项【几乎】一样,不过产生的行为不同。
file-loader
它的作用是把通过 import 和 require 加载的文件解析成 url 替换到使用的地方,同时把解析好的资源文件(hash 名称类型的)输出到打包目录中。
也就是说,file-loader 打包完后,你用到的所有图片都会输出到打包目录中。
url-loader
它的特别之处在于,可以通过设置
limit
选项来确定一个文件大小的阈值(单位 kb)。大于这个阈值的文件打包行为和 file-loader 一样;小于这个阈值的文件地址会被转换成 base64 字符串替换文件使用的地方,同时文件不再输出到构建目录中。
也就是说,url-loader 打包后,构建目录中的文件并不是一一对应的,有些肯能不存在,因为它直接以 base64 字符串的形式被写入到 js 文件中。
这也说明了,url-loader 是 file-loader 的加强版。其实 url-loader 封装了 file-loader,安装完 url-loader 后就不再需要安装 file-loader 了。
file-loader 和 url-loader 在性能优化层面也很矛盾
当一个项目中有大量的图片时,通过 file-loader 打包后,图片的请求会增加 http 的请求量,但是主体的 bundle 文件会小一些。
而 url-loader 将小文件转换成 base64 后,会导致主 bundle 的体积增大(30%左右),这会导致宽带的浪费和加载时间的增加。
所以,合理的配置阈值很重要。
# 二、配置时的共同点
- 在配置的 rule 中添加 type: 'javascript/auto'
由于当前是 webpack5 版本,针对文件资源模块,可以认为是默认使用了资源模块类型(asset module type)进行处理。
如果还想使用 webpack5 之前的模式,通过 file-loader 或者 url-loader 进行处理,就会导致资源被处理两遍。
如下通过 file-loader 配置处理图片:
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
}
}
},
{
test: /\.(css|less)$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'less-loader'
]
},
{
test: /\.(jpe?g|png|gif)$/, // 针对这三种格式的文件使用file-loader处理
use: {
loader: "file-loader",
options: {
outputPath: "images/", // 定义图片输出的文件夹名(在output.path目录下定义一个 images 文件)
esModule: false,
},
},
// type: 'javascript/auto' // 这里暂时注释,演示问题
}
]
},
打包后,dist 目录中比预期多处一张图片。但其实 file-loader 配置时图片都应该打包到了 images/
目录下,多出来的这一张图片是 webpack5 的资源模块处理的得到的:
所以,如果是在 webpack5 版本下使用 file-loader 模式,需要配置 type 字段。
- options.esModule 设置为 false
默认配置是 esModule: true
关于 esModule 的配置,url-loader 中也有这一项。但真正有效果的是 file-loader,可以理解成这是 file-loader 的属性配置,url-loader 只是包装了参数的传递
file-loader 解释
By default, file-loader generates JS modules that use the ES modules syntax. There are some cases in which using ES modules is beneficial, like in the case of module concatenation and tree shaking.
意思是:默认情况下,file-loader 生成使用 ES 模块语法的 JS 模块。在某些情况下,使用 ES 模块是有益的,例如 module concatenation 和 tree shaking 的情况。
如果 webpack5 中不添加 esModule: false
:
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
}
}
},
{
test: /\.(css|less)$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'less-loader'
]
},
{
test: /\.(jpe?g|png|gif)$/, // 针对这三种格式的文件使用file-loader处理
use: {
loader: "file-loader",
options: {
// 定义打包后文件的名称;
// [name]:原文件名,[hash]:hash字符串(如果不定义名称,默认就以hash命名,[ext]:原文件的后缀名)
name: "[name]_[hash].[ext]",
outputPath: "images/", // 定义图片输出的文件夹名(在output.path目录下定义一个 images 文件)
// esModule: false // 这里暂时注释,演示问题
},
},
type: 'javascript/auto'
}
]
},
打包后的图片路径是这样的,具体为什么会打包成这样,没有再继续深究。
# file-loader 正确配置
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
}
}
},
{
test: /\.(css|less)$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'less-loader'
]
},
{
test: /\.(jpe?g|png|gif)$/, // 针对这三种格式的文件使用file-loader处理
use: {
loader: "file-loader",
options: {
// 定义打包后文件的名称;
// [name]:原文件名,[hash]:hash字符串(如果不定义名称,默认就以hash命名,[ext]:原文件的后缀名)
name: "[name]_[hash].[ext]",
outputPath: "images/", // 定义图片输出的文件夹名(在output.path目录下定义一个 images 文件)
esModule: false
},
},
type: 'javascript/auto'
}
]
},
执行 yarn build
之后的资源信息
# url-loader 正确配置
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript'
]
}
}
},
{
test: /\.(css|less)$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'less-loader'
]
},
{
test: /\.(jpg|png|gif)$/, // 针对这三种格式的文件使用url-loader处理
use: {
loader: 'url-loader',
options: {
// 定义打包后文件的名称;
// [name]:原文件名,[hash]:hash字符串(如果不定义名称,默认就以hash命名,[ext]:原文件的后缀名)
name: '[name]_[hash].[ext]',
outputPath: 'images/', // 定义图片输出的文件夹名(在output.path目录下的images文件中)
limit: 10 * 1024, // 大于10kb的图片会被打包在images文件夹里面,小于这个值的会被以base64的格式写在js文件中
esModule: false
}
},
type: 'javascript/auto'
},
]
},
相比于 file-loader
,url-loader
在执行 yarn build
之后的资源信息,可以发现带有 ai 开头的图片资源并不在 images 目录中。
因为它小于 10kb,直接以 base64 字符串的形式进行编码了。
注意图中 css 的样式引入模式,由于我们在 url-loader
的 options
配置中添加了 outputPath
的配置是 images/
。css 文件中就自动给图片的引入添加上了 images/
路径。
如果去掉 outputPath
的配置,css 的路径也会自动去掉 images/
路径,这里会对比下述 webpack5 的配置模式。
# webpack5 配置模式
webpack5 模式下,asset 模块默认也是指向构建目录(dist)下。
可以通过配置中 generator.filename
来指定文件的 目录和名称结构。
虽然官网也说可以在 output
中配置 assetModuleFilename
字段来指定。
虽然没有尝试,但是觉得这个配置应该对所有的没有再单独配置的资源都放到了指定的一个目录下。
但是考虑到一个项目中不仅有图片还有字体文件等,不可能放在一个分类的明显的文件夹中,比如都放在 images/
目录下。一般这种配置都会放在一个通用命名文件夹中,比如 assets
。
所以,细分构建文件夹的话,在单独的配置中设置 filename 最为合适。
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
mode: "development", // 后续会针对生产环境和开发环境做区分
entry: path.resolve(__dirname, "./src/index"),
output: {
path: path.resolve(__dirname, "./dist"),
filename: `[name].[hash].js`,
clean: true, // webapck@5.20 开始支持的,清理构建文件夹的配置,不再需要 clean-webpack-plugin 插件
},
devServer: {
port: 9527,
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
},
{
test: /\.(css|less)$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader",
"less-loader",
],
},
{
test: /\.(png|jpe?g|gif|webp)/i,
type: "asset",
generator: {
filename: "images/[hash][ext][query]", // 将图片资源输出到 output.path 中的 images目录下
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
title: "Webpack5和React18的搭配",
template: path.resolve(__dirname, "./public/index.html"),
inject: "body",
}),
new MiniCssExtractPlugin({
// filename 可以通过配置成目录地址,来决定把css
// filename 默认值是 [name].css
filename: "css/[name].css",
}),
],
};
# TS 配置
# 那些能编译 ts 的编译器
对于 ts 的配置在安装相关的依赖之前,需要先弄明白哪些编译器可以对 typescript 进行编译。
最常用的是 ts-loader
、babel
和 awesome-typescript-loader
。
ts-loader
ts-loader
提供了编译 TypeScript 代码的功能。在编译期间,可以对 TypeScript 代码进行更多的类型检查和错误检查,并将 TypeScript 代码转换为可运行的 JavaScript 代码。Babel
Babel@7
开始支持编译 TS,它需要搭配@babel/preset-typescript
插件进行编译 ts 到 js。另外需要注意的是,Babel 只是支持编译 ts,但是它并不能提供类型检查。所以,类型检查还需要结合
tsconfig.json
文件中的noEmit
配置进行处理。有了
awesome-typescript-loader
据说能加快项目的编译速度,更好的利用 babel 的转义和缓存,更适合与 Babel 结合。更多内容没了解过。
# ts-loader 示例
ts-loader
在内部调用 Typescript 的官方编译器 -tsc
。所以,ts-loader
和 tsc
共享 tsconfig.json
文件。
使用 ts-loader
的方式如下:
yarn add ts-loader typescript -D
然后在 webpack 的配置中针对 ts 进行规则配置即可:
module.exports = {
...
modules: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
},
{
test: /\.tsx?$/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true // 只做ts到js的转换,不做类型检查
}
}
}
]
}
}
# Babel 示例
Babel 对 TS 的编译态度是 删除 TS。
它一口气将全部的 TS 转化成常规的 JS,然后对这些 JS 按照目标版本编译成最终的 JS 文件。
它需要使用的预设 @babel/preset-typescript
是默认包含了 @babel/plugin-transform-typescript
插件的。我看到一些文档中也安装闭并配置了这个插件,但当前项目中没有配置,也没有发现什么问题。
yarn add typescript @babel/preset-typescript -D
# webpack 的配置文件
module.exports = {
module: {
rules: [
{
test: /.(js|tsx?)$/,
use: {
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript", // 它用来编译ts
],
},
},
},
],
},
};
# tsconfig.json 文件
{
"compilerOptions": {
"noImplicitAny": false,
"target": "ES5", // 代码编译到哪个js版本
"lib": ["dom", "DOM.Iterable", "ES6"], // 告诉ts编译时要用到的库文件
"allowJs": true, // 允许混合编译,就是允许同一个项目中允许 *.ts文件和*.js文件同时存在,最终都通过ts编译器一起打包成正常的js文件。
"esModuleInterop": true, // 允许使用commonJs的方式 import 引入文件
"jsx": "react", // 允许编译器支持react代码
"noEmit": false // 只做类型检查,不做编译文件输出
},
"include": ["src"] // 编译 src 文件夹
}
从上述配置中就可以看到,Babel 与 ts-loader 的编译流程对比会更加简洁:
使用 ts-loader 的编译流程是 TS > ts-loader 编译 > JS > Babel 编译 > 目标 JS
使用 Babel 就直接将 ts 和 当前的 js 一起编译成了目标的 JS 进行输出了。
所以,到底使用哪个来编译 TS,个人短浅的目光觉着大概率是 Babel。
毕竟,现在 React 和 Vue 项目中要说没有用到 Babel 那就是扯。所以,在有 babel 的基础上,基本就是使用一个插件来解决配置的问题了。
关键是:两种编译器不要混用,这是我看了好几个帖子中都见到的。
# tsconfig.json 文件
# webpack.config.js 配置
当前项目中的 ts 处理模式是 Babel + 插件处理,先看当前全部 webpack 配置:
module.exports = {
...
resolve: {
extensions: [".js", ".ts", ".tsx"],
},
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000,
},
module: {
rules: [
{
test: /.(js|tsx?)$/,
use: {
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
],
},
},
},
...
],
}
};
# 跟随问题配置 tsconfig.json
当前还没有添加 tsconfig.json
文件内容,来看看 App.tsx
文件的状况:
1. 首先是 React 的报错如下:
来看一下react.development.js
中模块的暴露方式:
这里说明,React 的代码是通过 exports.xx = xx
这种 Commonjs 模式导出的。正常我们倒入的模式都是 const {xx} = require('react')
如果要使用 import 的模式引入,需要这样写:
import * as React from "react";
const { useState, useEffect } = React;
而如果我们想使用 import React from 'react
的方式的话,就需要在 tsconfig.json 中配置上 "esModuleInterop": true
。
2. jsx
结构的报错
这个配置就是需要在 tsconfig.json 中提供对 jsx 的配置。
解决上面两个问题的基础配置如下:
{
"compilerOptions": {
"module": "esnext",
"target": "ES5", // 代码编译到哪个js版本
"lib": ["dom", "DOM.Iterable", "ES6"], // 告诉ts编译时要用到的库文件
"allowJs": true, // 允许混合编译,就是允许同一个项目中允许 *.ts文件和*.js文件同时存在,最终都通过ts编译器一起打包成正常的js文件。
"esModuleInterop": true, // 允许使用commonJs的方式 import 引入文件
"jsx": "react", // 允许编译器支持react代码
"noEmit": true, // 告知 Typescript 编译器,只做类型校验不输出 js 文件
"moduleResolution": "Node",
"baseUrl": "./src", // 编译文件的基础路径
"paths": {
// 设置别名时除了 webpack 中要配置,这里也要配置
"@src/*": ["./*"]
}
},
"include": ["src"]
}
这个配置之后, react 的引入可以直接使用 import React from 'react'
的模式了就。
3. module
字段
在代码中使用 import.meta
时出现了提示报错:
仅当 “--module” 选项为 “es2020”、“es2022”、“esnext”、“system”、“node16” 或 “nodenext” 时,才允许使用 “import.meta” 元属性。
关于 module 字段,它主要用于指定 TypeScript 代码的模块系统。
以下是 module 字段的一些常用值:
- commonjs: 使用 CommonJS 模块系统。这是 Node.js 默认的模块系统。
- amd: 使用 AMD(Asynchronous Module Definition)模块系统。AMD 是用于浏览器端的模块系统。
- umd: 使用 UMD(Universal Module Definition)模块系统。UMD 旨在同时支持 CommonJS 和 AMD。
- systemjs: 使用 SystemJS 模块系统。SystemJS 是一个在浏览器端实现模块加载的通用工具。
- es2020, es2022, esnext, es6, es7, es8, es9, es10, es11, es12, es13: 使用 ECMAScript 的不同版本作为模块系统。
- none: 无模块系统。所有的类型声明文件都将被导入为顶级命名空间。
- node16, nodenext: 使用 Node.js 的模块系统。
选择适当的 module 值取决于你的项目需求和目标环境。
例如,如果你正在开发一个针对浏览器端的应用,你可能会选择 amd、systemjs 或 umd。
如果你正在开发一个针对 Node.js 的应用,你可能会选择 commonjs、node16 或 nodenext。
如果你希望你的代码能在多个环境中运行,你可能需要选择一个兼容多种环境的模块系统,如 umd 或 systemjs。
4. moduleResolution
字段
改字段用于指定模块解析策略。它决定了当 TypeScript 编译器查找模块时,应如何解析相对和非相对模块的路径。
比如在未配置该字段时,引入 react-pdf
依赖时发生了这种报错:
Cannot find module 'react-pdf'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option?
这种报错的一般有三种情况:
- 没有安装
react-pdf
依赖 tsconfig.json
中没有正确的设置paths
和baseUrl
字段- 没有配置
moduleResolution
选项以支持 Node.js 风格的模块解析
那么什么是 Node.js 风格的模块解析呢?
# Node.js 风格的模块解析
Node.js 风格的模块解析是指 Node.js 环境中用于查找和加载模块的一套规则。
当你使用 require()函数来导入一个模块时,Node.js 会根据一套特定的算法来确定模块的位置,并将其代码加载到运行时环境中。
以下是 Node.js 模块解析的基本规则:
文件模块: 如果 require()的参数是一个相对路径(如./module)或一个绝对路径(如/path/to/module),Node.js 会尝试加载指定路径的文件。如果文件的后缀名被省略,Node.js 会按照.js、.json、.node 的顺序尝试加载文件,直到找到一个存在的文件。
核心模块: 如果 require()的参数是一个 Node.js 的核心模块名(如 fs、http),则 Node.js 会直接加载内置的核心模块。
node_modules 目录: 如果 require()的参数不是一个路径,也不是一个核心模块,Node.js 会从当前文件所在的目录开始,逐级向上查找 node_modules 目录,直到找到对应的模块或者到达文件系统的根目录。在 node_modules 目录中,Node.js 会查找一个与参数匹配的包目录,并尝试加载该目录下的 package.json 文件。如果 package.json 文件的 main 字段指定了一个入口文件,Node.js 会加载该文件;否则,Node.js 会尝试加载包目录下的 index.js 文件。
别名和符号链接: 在查找模块的过程中,Node.js 会解析文件系统中的别名(symbolic links),将别名指向的实际路径作为查找路径。
Node.js 风格的模块解析机制非常灵活,它允许开发者通过 node_modules 目录和 package.json 文件来组织和管理项目中的依赖关系。这种机制也被许多其他 JavaScript 环境(如 Browserify、Webpack 等)所采纳,成为 JavaScript 生态系统中广泛使用的模块加载标准。
在 TypeScript 中,通过配置 tsconfig.json 文件中的 moduleResolution 选项为 node,可以让 TypeScript 编译器使用 Node.js 风格的模块解析规则来解析模块。这对于在 Node.js 环境中运行 TypeScript 代码是非常重要的。
# 相关文件内容
# webpack.config.js
const path = require("path");
const glob = require("glob");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const { PurgeCSSPlugin } = require("purgecss-webpack-plugin");
module.exports = {
entry: path.resolve(__dirname, "src/index"),
output: {
path: path.resolve(__dirname, "./dist"),
filename: `[name].[fullhash].js`,
clean: true,
},
resolve: {
extensions: [".js", ".jsx", ".ts", ".tsx"],
},
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000,
},
module: {
rules: [
{
test: /.(js|tsx?)$/,
use: {
loader: "babel-loader",
options: {
presets: [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
],
},
},
},
{
test: /\.(css|less)$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader",
"less-loader",
],
},
{
test: /\.(png|jpg|gif|jpeg)$/,
type: "asset",
generator: {
filename: "images/[name][hash][ext]",
},
},
],
},
optimization: {
minimizer: [new CssMinimizerPlugin()],
},
plugins: [
new HtmlWebpackPlugin({
title: "Webpack5+React18的搭配",
template: path.resolve(__dirname, "./public/index.html"),
}),
new MiniCssExtractPlugin({
filename: "css/[name].css",
}),
// css tree-shaking
new PurgeCSSPlugin({
paths: glob.sync(path.resolve(__dirname, "src/**/*.tsx"), {
nodir: true,
}),
}),
],
};
# tsconfig.json
{
"compilerOptions": {
"target": "ES5", // 代码编译到哪个js版本
"lib": ["dom", "DOM.Iterable", "ES6"], // 告诉ts编译时要用到的库文件
"allowJs": true, // 允许混合编译,就是允许同一个项目中允许 *.ts文件和*.js文件同时存在,最终都通过ts编译器一起打包成正常的js文件。
"esModuleInterop": true, // 允许使用commonJs的方式 import 引入文件
"jsx": "react", // 允许编译器支持react代码
"noEmit": true // 告知 Typescript 编译器,只做类型校验不输出 js 文件
},
"include": ["src"]
}
# package.json
{
"name": "webpack5-react18-ts",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "webpack serve --config webpack.dev.config.js",
"build": "webpack --config webpack.prod.config.js"
},
"devDependencies": {
"@babel/core": "^7.23.6",
"@babel/preset-env": "^7.23.6",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@types/node": "^20.10.5",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"babel-loader": "^9.1.3",
"css-loader": "^6.8.1",
"css-minimizer-webpack-plugin": "^5.0.1",
"glob": "^10.3.10",
"html-webpack-plugin": "^5.5.4",
"less": "^4.2.0",
"less-loader": "^11.1.3",
"mini-css-extract-plugin": "^2.7.6",
"postcss-loader": "^7.3.3",
"postcss-preset-env": "^9.3.0",
"purgecss-webpack-plugin": "^5.0.0",
"style-loader": "^3.3.3",
"typescript": "^5.3.3",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpack-merge": "^5.10.0"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
# 参考文档
- webpack 从 0 开始搭建一个 react+ts 项目 (opens new window)
- 【万字】手把手搭建 Webpack5 + React18 + TS 脚手架 (opens new window)
- webpack 入门之 ts 处理(ts-loadr 和 babel-loader 的选择) (opens new window)
- TS 编译工具!从 ts-loader 到 Babel (opens new window)
- webpack 官网——资源模块 (opens new window)
- webpack 官网——CssMinimizerWebpackPlugin (opens new window)
- webpack 官网——TerserWebpackPlugin (opens new window)
- css 的 tree-shaking (opens new window)