在前一章中我们介绍了使用CRA创建React工程,实际上CRA屏蔽了很多工程配置的细节,一些工程配置修改起来可能不如我们自己手动搭建的方便使用,这里我们再手动搭建一个最基础的React工程,集成Webpack、Webpack DevServer、Babel、各种类型Loader、构建分析等必备的常用功能,以加深对相关工具链和依赖库的理解。
本篇笔记中的例子工程我们基于最新版本的React18、Webpack5来搭建。
下面是这个基础功能的目录结构,其中包括Webpack配置文件、构建输出目录、公共目录、源码目录以及工程描述文件,这些文件共同组成了一个最基础可用的React前端工程。
|_node_modules // npm包目录
|_config // 配置文件目录
|_webpack.js // 基础Webpack配置文件
|_webpack.development.js // 开发环境配置文件
|_webpack.test.js // 测试环境配置文件
|_webpack.production.js // 生产环境配置文件
|_webpack.analyze.js // 构建分析配置文件
|_dist // 构建输出
|_public // 入口HTML和其直接引用的静态文件
|_index.html // 入口HTML
|_global.css // 入口HTML直接引用的一个全局CSS文件
|_src // 源代码路径
|_index.jsx // 入口JavaScript文件
|_App.jsx // 根组件
|_package.json // npm工程描述文件
这里我们给出所有配置文件的源码以供参考。
package.json
{
"name": "demoreact",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"dev": "cross-env NODE_ENV=development APP_ENV=development webpack-dev-server --config config/webpack.development.js",
"build:test": "cross-env NODE_ENV=production APP_ENV=test webpack --config config/webpack.test.js",
"build:analyze": "cross-env NODE_ENV=production APP_ENV=production webpack --config config/webpack.analyze.js",
"build:prod": "cross-env NODE_ENV=production APP_ENV=production webpack --config config/webpack.production.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpack-merge": "^5.9.0",
"babel-loader": "^9.1.3",
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"@babel/preset-react": "^7.22.5",
"style-loader": "^3.3.3",
"css-loader": "^6.8.1",
"less": "^4.1.3",
"less-loader": "^11.1.3",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"react-refresh": "^0.14.0",
"html-webpack-plugin": "^5.5.3",
"copy-webpack-plugin": "^11.0.0",
"css-minimizer-webpack-plugin": "^5.0.1",
"speed-measure-webpack-plugin": "^1.5.0",
"webpack-bundle-analyzer": "^4.9.0",
"cross-env": "^7.0.3"
}
}
工程描述文件package.json
中,我们主要关注scripts
构建命令和依赖包。构建命令我们编写了4条:
npm run dev
本地通过devServer启动工程npm run build:test
以测试环境配置构建工程(配置基本同生产,后面不在重复介绍)npm run build:prod
以生产环境配置构建工程npm run build:analyze
执行构建分析插件依赖包这里我们详细介绍一下:
react
和react-dom
:不必多说,这些是React框架的本体webpack
、webpack-cli
、webpack-dev-server
:包括Webpack构建工具和开发环境用的调试服务器webpack-merge
:用于合并Webpack配置,是多环境开发拆分配置文件必备的工具babel-loader
、@babel/core
、@babel/preset-env
和@babel/preset-react
:Babel用于将高版本JavaScript语法和JSX语法转义到浏览器能够执行的JavaScript语法,这些工具通过babel-loader
集成到Webpack处理构建中的资源style-loader
、css-loader
、less
、less-loader
:用于加载CSS和Less样式资源@pmmmwh/react-refresh-webpack-plugin
、react-refresh
:这两个包是对Webpack原版HMR的补充,用于实现修改JSX后不刷新页面直接更新渲染结果,且保留组件状态,对开发效率十分有帮助html-webpack-plugin
:该插件用于将public/index.html
作为模板向其中插入Webpack打包后的资源,并将index.html
拷贝到构建输出路径dist
copy-webpack-plugin
:该插件用于将public
中的资源拷贝到构建输出路径dist
中,注意HtmlWebpackPlugin
默认会拷贝index.html
,该插件则用于拷贝index.html
直接引用的其它样式表、媒体资源、字体、第三方库等css-minimizer-webpack-plugin
:该插件用于压缩CSS文件speed-measure-webpack-plugin
:该插件用于分析构建速度,只在npm run build:analyze
命令中执行webpack-bundle-analyzer
:该插件用于分析构建结果,只在npm run build:analyze
命令中执行cross-env
:用于配置Node环境变量,这个大家都很熟悉,就不多介绍了config/webpack.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const CssMinimizerWebpackPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
entry: path.join(__dirname, "../src/index.jsx"),
output: {
filename: "[name].[chunkhash:8].js",
path: path.join(__dirname, "../dist"),
clean: true,
publicPath: "/",
},
resolve: {
alias: {
"@": path.join(__dirname, "../src"),
},
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: "/node_modules/",
use: {
loader: "babel-loader",
options: {
presets: [
["@babel/preset-react", { runtime: "automatic" }],
"@babel/preset-env",
],
plugins: [
process.env === "development" &&
require.resolve("react-refresh/babel"),
].filter(Boolean),
},
},
},
{
test: /\.(css|less)$/,
use: ["style-loader", "css-loader", "less-loader"],
},
{
test: /.(png|jpg|jpeg|gif|svg)$/,
type: "asset",
parser: {
dataUrlCondition: { maxSize: 10 * 1024 },
},
generator: {
filename: "static/images/[name][ext]",
},
},
{
test: /.(woff2?|eot|ttf|otf)$/,
type: "asset",
generator: {
filename: "static/fonts/[name][ext]",
},
},
{
test: /.(mp4|webm|ogg|mp3|wav|flac|aac)$/,
type: "asset",
generator: {
filename: "static/media/[name][ext]",
},
},
],
},
plugins: [
new webpack.DefinePlugin({
"process.env.APP_ENV": JSON.stringify(process.env.APP_ENV),
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
inject: true,
}),
],
optimization: {
minimize: true,
minimizer: [
new TerserWebpackPlugin({ extractComments: false }),
new CssMinimizerWebpackPlugin(),
],
splitChunks: {
cacheGroups: {
vendors: {
test: /node_modules/,
name: "vendors",
minChunks: 1,
chunks: "initial",
minSize: 0,
priority: 1,
},
commons: {
name: "commons",
minChunks: 2,
chunks: "initial",
minSize: 0,
},
},
},
},
cache: {
type: "filesystem",
},
};
webpack.js
是我们的基础Webpack配置,其它环境的Webpack配置都是通过merge
的方式合并到基础配置上的。
config/webpack.development.js
const path = require("path");
const { merge } = require("webpack-merge");
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
const baseConfig = require("./webpack.js");
module.exports = merge(baseConfig, {
mode: "development",
devtool: "source-map",
watch: true,
devServer: {
host: "localhost",
port: 3000,
open: true,
compress: false,
hot: true,
historyApiFallback: true,
static: {
directory: path.join(__dirname, "../public"),
},
},
plugins: [new ReactRefreshWebpackPlugin()],
});
webpack.development.js
是开发环境的Webpack配置,其中主要对devServer进行了配置。
config/webpack.production.js
const path = require("path");
const { merge } = require("webpack-merge");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const baseConfig = require("./webpack.js");
module.exports = merge(baseConfig, {
mode: "production",
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, "../public"),
to: path.resolve(__dirname, "../dist"),
filter: (resourcePath) => {
return !resourcePath.includes("index.html");
},
},
],
}),
],
});
webpack.production.js
是生产环境的Webpack配置,主要对构建进行了配置。config/webpack.test.js
内容基本和config/webpack.production.js
相同。
config/webpack.analyze.js
const { merge } = require("webpack-merge");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
const productionConfig = require("./webpack.production.js");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap(
merge(productionConfig, {
plugins: [new BundleAnalyzerPlugin()],
})
);
webpack.analyze.js
是用于构建分析的Webpack配置,这里主要用到了两个插件:speed-measure-webpack-plugin
和webpack-bundle-analyzer
,分别用于分析构建速度和构建结果。
public/index.html
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link href="global.css" type="text/css" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
</body>
</html>
index.html
是入口HTML文件,注意其中包含的<div id="root"></div>
是我们的React组件挂载根节点。
public/global.css
body {
margin: 0;
padding: 0;
}
ul {
margin: 0;
}
* {
box-sizing: border-box;
}
global.css
是我们自定义的样式表文件,在这里我们添加这样一个文件主要用于演示公共文件构建拷贝和CSS文件压缩的效果。
src/index.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "@/App.jsx";
const render = () => {
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
};
if (process.env.NODE_ENV === "development" && module.hot) {
module.hot.accept("@/App.jsx", render);
}
render();
index.jsx
是我们的入口源码文件,其中包含了挂载React组件和集成开发环境HMR功能的代码。
src/App.jsx
import Demo from "@/Demo/index.jsx";
const App = () => {
return (
<div>
<Demo />
</div>
);
};
export default App;
App.jsx
是我们自定义的React组件。
上面我们搭建的只是最简单最基础的React工程,真正开发中情况会更加的复杂。我们可能需要添加更多功能,比如react-router
、redux
、组件库等;我们面向的平台可能是Electron,我们渲染的方式可能是SSR等等。
实际上,我们只要弄懂Webpack等工具的基本使用方法,参考文档和各种教程去解决特定的问题即可,随着各种框架和工具版本的迭代配置项也可能发生变化,我们没必一上来就沉迷工程搭建,而最终反倒一行真正有用的代码都没写出来,那样就本末倒置了。