这篇文章和普通的 React 教程不同。
我们不从:
React API
↓
组件语法
↓
Hooks
开始。
而是直接进入一个完整的项目:
Browser
↓
React
↓
Redux
↓
React Router
↓
Express
↓
SSR
↓
Webpack
↓
Babel
↓
Development / Production
最终理解一个前端项目是如何从:
源码
↓
开发服务器
↓
构建
↓
服务端渲染
↓
浏览器加载
↓
Hydration
完整运行起来的。
这也是这篇文章真正想解决的问题:
前端工程化不是记住多少个 Webpack 配置,而是理解一个应用从源码到运行时经历了什么。
第一章:从一个完整项目理解前端工程化
1.1 项目到底解决什么问题?
原项目是一个完整的 ES6 React 项目模板,目标分为三个层次:
- 初级:了解 ES6 项目的基本配置
- 中级:完整掌握项目中的工程化内容
- 高级:能够掌握业务项目中的工程化配置,并进行定制化处理
原项目并不是一个单纯的 React Demo,而是把:
diff
客户端
+
服务端
+
SSR
+
状态管理
+
路由
+
构建
+
开发环境
+
生产环境
组合到了一起。
因此学习它时,不能只看某一个配置。
更应该理解:
为什么这样设计?
1.2 项目技术栈
原项目主要使用:
| 技术 | 作用 |
|---|---|
| React | 页面 UI |
| Redux | 状态管理 |
| React Router | 前端路由 |
| Connected React Router | Redux 与 Router 集成 |
| Express | Node.js 服务端 |
| TypeScript | 类型检查 |
| Webpack | 打包构建 |
| Babel | JavaScript 转换 |
| Axios | HTTP 请求 |
| React Helmet | 管理 HTML Head |
| Loadable Components | 组件懒加载 / Code Splitting |
| Webpack Dev Middleware | Express 集成 Webpack |
| Webpack Hot Middleware | HMR |
| Bundle Analyzer | Bundle 分析 |
| Morgan | 服务端日志 |
| Terser | JavaScript 压缩 |
| CSS Minimizer | CSS 压缩 |
| nodemon | Node 服务自动重启 |
这些技术的组合非常典型:
markdown
┌──────────────┐
│ React │
└──────┬───────┘
│
┌─────────────┼─────────────┐
↓ ↓ ↓
Redux Router Components
│
↓
Application
│
↓
Express / SSR
│
↓
Webpack
1.3 项目目录结构
原项目目录:
arduino
├─.husky
├─.vscode
├─public
│ ├─assets
│ └─server
├─src
│ ├─app
│ ├─client
│ ├─components
│ │ ├─ErrorBoundary
│ │ ├─Info
│ │ ├─List
│ │ └─Loading
│ ├─config
│ ├─pages
│ │ ├─Home
│ │ ├─NotFound
│ │ └─UserInfo
│ ├─routes
│ ├─server
│ ├─services
│ ├─static
│ ├─store
│ ├─theme
│ └─types
└─webpack
这个目录其实已经把整个应用拆成了几个明显的层次:
markdown
src/app
↓
应用入口
src/client
↓
浏览器入口
src/server
↓
SSR 服务端入口
src/pages
↓
页面
src/components
↓
通用组件
src/routes
↓
路由
src/services
↓
API 请求
src/store
↓
状态管理
src/types
↓
类型
webpack
↓
构建系统
这比单纯记忆:
Webpack 是打包工具
React 是 UI 框架
Redux 是状态管理
更有价值。
因为你开始看到:
一个真实项目是如何把这些工具组织起来的。
1.4 项目运行命令
原项目通过 cross-env 处理跨平台环境变量,并提供:
| 命令 | 作用 |
|---|---|
pnpm run dev |
localhost:3000 开发环境,支持 HMR |
pnpm run dev:build |
开发模式构建服务端代码 |
pnpm run start |
生产环境启动服务 |
pnpm run build |
构建客户端和服务端 |
pnpm run build:server |
构建服务端 |
pnpm run build:client |
构建客户端 |
pnpm run analyze:server |
分析服务端 Bundle |
pnpm run analyze:client |
分析客户端 Bundle |
这里已经可以看到一个很重要的工程概念:
markdown
Development
≠
Production
开发环境关注:
javascript
HMR
Source Map
快速反馈
生产环境关注:
Minification
Hash
Bundle
Caching
Performance
第二章:客户端应用是如何运行起来的
2.1 React 应用入口
原项目的应用入口:
javascript
// src/app/index.tsx
const App = ({ route }: Route): JSX.Element => (
<div className={styles.App}>
<Helmet {...config.APP} />
<Link to="/" className={styles.header}>
<img src={logo} alt="Logo" role="presentation" />
<h1>
<em>{config.APP.title}</em>
</h1>
</Link>
<hr />
{renderRoutes(route.routes)}
</div>
);
这里有几个值得注意的地方。
首先:
xml
<Helmet />
负责页面的:
bash
title
meta
link
script
等 Head 信息。
其次:
scss
renderRoutes(route.routes)
负责根据当前路由继续渲染子页面。
因此 App 本身并不是所有页面的集合,而更像:
css
App
├── Header
├── Helmet
└── Router
├── Home
├── UserInfo
└── NotFound
2.2 客户端入口
原项目客户端入口:
javascript
// src/client/index.tsx
const render = (Routes: RouteConfig[]) =>
ReactDOM.hydrate(
<Provider store={store}>
<ConnectedRouter {...props}>
{renderRoutes(Routes)}
</ConnectedRouter>
</Provider>,
document.getElementById('react-view'),
);
loadableReady(() => render(routes as RouteConfig[]));
这里是整个 SSR 项目的关键。
因为服务端已经生成了:
erlang
<div id="react-view">
...
</div>
浏览器加载 JavaScript 后,并不是重新从空 DOM 开始渲染。
而是:
arduino
Server HTML
↓
Browser
↓
React
↓
Hydration
让 React 接管已经存在的 HTML。
2.3 Hydration 到底是什么?
传统 CSR:
Browser
↓
下载 JS
↓
React 执行
↓
创建 DOM
SSR:
arduino
Server
↓
React Render
↓
HTML
↓
Browser
但此时浏览器拿到的 HTML 只是:
已经生成好的页面结构。
React 还没有真正接管它。
因此还需要:
Hydration
可以简单理解成:
让 React 将已有 HTML 与组件状态、事件处理等重新建立联系。
于是完整过程变成:
scss
Server
↓
renderToString()
↓
HTML
↓
Browser
↓
Hydration
↓
Interactive React App
这也是 SSR 与纯静态 HTML 最大的区别之一。
2.4 Error Boundary
原项目保留了一个完整的 Error Boundary:
typescript
class ErrorBoundary extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
error: null,
errorInfo: null,
};
}
componentDidCatch(
error: Error,
errorInfo: { componentStack: string }
): void {
this.setState({
error,
errorInfo,
});
}
render(): ReactNode {
const { children } = this.props;
const { errorInfo, error } = this.state;
return errorInfo ? (
<div data-testid="error-view">
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{error && error.toString()}
<br />
{errorInfo.componentStack}
</details>
</div>
) : (
children || null
);
}
}
它解决的是:
javascript
组件树
↓
运行时错误
↓
Error Boundary
↓
错误 UI
而不是让整个应用直接白屏。
需要注意:
Error Boundary 主要捕获 React 渲染过程中的错误,并不是所有 JavaScript 异常都能通过它捕获。
2.5 页面数据加载
Home 页面中:
ini
const Home: FC<Props> = (): JSX.Element => {
const dispatch = useDispatch();
const { readyStatus, items } = useSelector(
({ userList }: AppState) => userList,
shallowEqual
);
useEffect(() => {
dispatch(fetchUserListIfNeed());
}, [dispatch]);
// ...
};
客户端:
Component Mount
↓
useEffect
↓
dispatch
↓
API
↓
Redux
↓
UI
但是 SSR 不能等浏览器的 useEffect()。
所以原项目另外提供:
ini
export const loadData = (): AppThunk[] => [
fetchUserListIfNeed(),
];
这就是整个 SSR 数据预取的关键。
arduino
CSR
→ 浏览器执行 effect
SSR
→ Server 提前 loadData
2.6 路由配置
原项目:
yaml
export default [
{
component: App,
routes: [
{
path: '/',
exact: true,
component: AsyncHome,
loadData: loadHomeData,
},
{
path: '/UserInfo/:id',
component: AsyncUserInfo,
loadData: loadUserInfoData,
},
{
component: NotFound,
},
],
},
] as RouteConfig[];
这个设计非常重要。
路由不仅描述:
URL
↓
Component
还描述:
URL
↓
Component
↓
loadData
于是服务端可以:
请求 URL
↓
匹配 Route
↓
找到 loadData
↓
请求 API
↓
Redux Store
↓
React SSR
这就是:
Route-driven Data Fetching
第三章:Express + SSR 完整实战
3.1 Express 服务端入口
原项目使用 Express:
less
const app = express();
app.use(
helmet({
contentSecurityPolicy: false,
})
);
app.use(hpp());
app.use(compression());
app.use(
logger('dev', {
skip: (_, res) => res.statusCode < 400,
})
);
app.use(
favicon(
path.resolve(process.cwd(), 'public/logo.png')
)
);
app.use(
express.static(
path.resolve(process.cwd(), 'public')
)
);
if (__DEV__) {
devServer(app);
}
app.get('*', ssr);
整个请求链:
css
HTTP Request
↓
Express
↓
Security / Compression / Logger
↓
Static Assets
↓
SSR
↓
HTML
3.2 SSR 最核心的问题
假设用户访问:
bash
/user/100
服务端需要回答:
markdown
1. 这个 URL 对应哪个页面?
2. 页面需要什么数据?
3. 数据加载完成后如何生成 HTML?
4. 如何把 Redux 初始状态交给浏览器?
5. 如何让客户端继续接管?
因此 SSR 不是简单:
scss
renderToString(<App />);
而是一整个流程。
3.3 matchRoutes
原项目:
ini
const branch = matchRoutes(
routes,
req.path
);
然后:
javascript
const promises = branch.map(
({ route, match }) => {
if (route.loadData) {
return Promise.all(
route
.loadData({
params: match.params,
getState: store.getState,
req,
res,
})
.map((item: Action) =>
store.dispatch(item)
)
);
}
return Promise.resolve(null);
}
);
这个流程可以理解成:
scss
Request
↓
/UserInfo/100
↓
matchRoutes()
↓
找到 UserInfo Route
↓
loadData()
↓
Redux dispatch
↓
等待 API
也就是说:
服务端必须先知道"我要渲染哪个页面",才能知道"这个页面需要什么数据"。
3.4 SSR 完整流程
原项目的 SSR 代码核心结构:
ini
const { store } = createStore({
url: req.url,
});
const branch = matchRoutes(
routes,
req.path
);
先匹配路由:
vbscript
Request
↓
matchRoutes
然后:
scss
await loadBranchData();
等待页面数据。
之后:
ini
const App = extractor.collectChunks(
<Provider store={store}>
<StaticRouter
location={req.path}
context={staticContext}
>
{renderRoutes(routes)}
</StaticRouter>
</Provider>
);
然后:
ini
const initialState = store.getState();
const htmlContent =
renderToString(App);
最终:
css
Request
↓
matchRoutes
↓
loadData
↓
Redux Store
↓
renderToString
↓
HTML
↓
initialState
↓
Browser
↓
Hydration
3.5 renderToString
核心代码:
ini
const htmlContent = renderToString(App);
React 将:
React Component Tree
转换成:
arduino
HTML String
例如:
xml
<App />
↓
<div>
<h1>Hello</h1>
</div>
最终服务端将它塞进 HTML:
erlang
<div id="react-view">
...
</div>
3.6 Redux 初始状态注入
SSR 还有一个重要问题:
服务端请求过的数据:
arduino
Server Redux Store
浏览器端也需要知道。
原项目:
ini
const initialState = store.getState();
然后注入:
xml
<script>
window.__INITIAL_STATE__ = ...
</script>
于是:
arduino
Server Redux Store
↓
Serialize
↓
HTML
↓
Browser
↓
Client Redux Store
这样客户端就可以继续运行。
3.7 HTML 最终组装
原项目的 HTML 模板:
xml
const html = `
<!doctype html>
<html ${head.htmlAttributes.toString()}>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
${head.title.toString()}
${head.meta.toString()}
${head.link.toString()}
${extractor.getLinkTags()}
${extractor.getStyleTags()}
</head>
<body>
<div id="react-view">
${htmlContent}
</div>
<script>
window.__INITIAL_STATE__ =
${serialize(initialState)};
</script>
${extractor.getScriptTags()}
${head.script.toString()}
</body>
</html>
`;
于是整个 HTML:
css
HTML
├── Head
│ ├── title
│ ├── meta
│ ├── css
│ └── link
│
└── Body
├── SSR HTML
├── Initial State
└── JavaScript
最终发送给浏览器。
3.8 Hydration:SSR 最后一步
浏览器拿到:
xml
<div id="react-view">
<h1>...</h1>
</div>
同时拿到:
javascript
window.__INITIAL_STATE__
然后客户端:
scss
ReactDOM.hydrate(...)
完成接管。
最终:
css
SSR HTML
↓
First Paint
↓
Download JS
↓
Hydration
↓
Interactive
3.9 Redirect 与 404
SSR 还需要处理路由状态。
原项目:
lua
if (staticContext.url) {
res
.status(301)
.setHeader(
'Location',
staticContext.url
);
res.end();
return;
}
如果:
staticContext.url
存在,就说明服务端渲染过程中发生了重定向。
否则:
ini
res
.status(
staticContext.statusCode === '404'
? 404
: 200
)
.send(
renderHtml(
head,
extractor,
htmlContent,
initialState
)
);
因此 SSR 不只是:
css
生成 HTML
还需要正确处理:
200
301
404
等 HTTP 状态。
第四章:Webpack 如何把整个项目构建起来
4.1 为什么需要 Webpack?
浏览器最终需要的是:
JavaScript
CSS
Images
Fonts
但项目源码可能是:
.ts
.tsx
.scss
.css
.svg
.png
同时还有:
css
React
Redux
第三方依赖
Dynamic Import
Code Splitting
所以需要一个构建系统把:
css
Source Code
转换成:
Browser Assets
4.2 Webpack 基础配置
原项目公共配置:
css
const config = (
isWeb = false
): Configuration => ({
mode: isDev
? 'development'
: 'production',
stats: 'minimal',
context:
path.resolve(process.cwd()),
output: {
clean: true,
},
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
},
},
}),
],
},
plugins:
getPlugins(isWeb),
module: {
rules: [
{
test: /.(t|j)sx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /.css$/,
use: getStyleLoaders(isWeb),
},
{
test: /.(scss|sass)$/,
use:
getStyleLoaders(
isWeb,
true
),
},
{
test:
/.(woff2?|eot|ttf|otf)$/i,
type: 'asset',
},
{
test:
/.(png|svg|jpe?g|gif)$/i,
type: 'asset',
},
],
},
resolve: {
modules: [
'src',
'node_modules',
],
extensions: [
'.ts',
'.tsx',
'.js',
'.jsx',
'.json',
],
},
});
4.3 Loader
Webpack 最重要的概念之一:
Loader
Loader 负责:
将某种类型的资源转换成 Webpack 可以处理的模块。
例如:
TypeScript
↓
babel-loader
↓
JavaScript
CSS:
CSS
↓
css-loader
↓
PostCSS
↓
CSS Asset
SCSS:
SCSS
↓
sass-loader
↓
css-loader
↓
postcss-loader
4.4 Plugin
Loader 主要处理:
sql
Module
Plugin 可以参与:
整个构建生命周期
原项目使用:
WebpackProgressPlugin
WebpackManifestPlugin
LoadablePlugin
DefinePlugin
ForkTsCheckerWebpackPlugin
BundleAnalyzerPlugin
MiniCssExtractPlugin
HotModuleReplacementPlugin
ReactRefreshWebpackPlugin
例如:
php
new webpack.DefinePlugin({
__CLIENT__: isWeb,
__SERVER__: !isWeb,
__DEV__: isDev,
});
可以把构建环境信息注入代码:
scss
if (__DEV__) {
// development
}
4.5 CSS 处理
原项目:
ini
const getStyleLoaders = (
isWeb: boolean,
isSass?: boolean
) => {
let loaders = [
{
loader: 'css-loader',
options: {
importLoaders:
isSass ? 2 : 1,
modules: {
auto: true,
localIdentName:
isDev
? '[path][name]__[local]'
: '[hash:base64]',
exportOnlyLocals:
!isWeb,
},
},
},
{
loader: 'postcss-loader',
},
];
if (isWeb) {
loaders = [
MiniCssExtractPlugin.loader,
...loaders,
];
}
if (isSass) {
loaders = [
...loaders,
{
loader: 'sass-loader',
},
];
}
return loaders;
};
这里同时处理:
arduino
CSS
SCSS
CSS Modules
SSR CSS
Client CSS
这也是为什么工程化配置不能只看一个 Loader。
4.6 客户端构建
客户端入口:
css
const config: Configuration = {
devtool:
isDev &&
'eval-cheap-source-map',
entry: isDev
? [
'webpack-hot-middleware/client?reload=true',
'./src/client',
]
: './src/client',
output: {
filename: isDev
? '[name].js'
: '[name].[contenthash].js',
chunkFilename: isDev
? '[id].js'
: '[id].[contenthash].js',
path:
path.resolve(
process.cwd(),
'public/assets'
),
publicPath: '/assets/',
},
optimization: {
minimizer: [
new CssMinimizerPlugin(),
],
},
};
生产环境使用:
css
[name].[contenthash].js
这是为了:
利用浏览器缓存。
例如:
css
main.a8f2.js
代码没变化:
bash
hash 不变
浏览器可以继续使用缓存。
代码发生变化:
bash
hash 改变
浏览器重新请求。
4.7 服务端构建
服务端构建:
css
const config: Configuration = {
target: 'node',
devtool:
isDev
? 'inline-source-map'
: 'source-map',
entry: './src/server',
output: {
filename: 'index.js',
chunkFilename: '[id].js',
path:
path.resolve(
process.cwd(),
'public/server'
),
libraryTarget:
'commonjs2',
},
externals: [
'@loadable/component',
nodeExternals({
allowlist: [
/.(?!(?:jsx?|json)$).{1,5}$/i,
],
}),
],
};
注意这里:
arduino
Client
target: browser
而:
arduino
Server
target: node
因此原项目实际上存在:
arduino
Source
│
┌────────┴────────┐
↓ ↓
Client Server
↓ ↓
Browser Node
↓ ↓
public/assets public/server
这就是:
双端构建。
4.8 Code Splitting
项目使用:
bash
@loadable/component
例如:
javascript
const AsyncHome =
loadable(
() => import('./pages/Home')
);
这样:
Home
就不一定需要和主 Bundle 一起加载。
可以形成:
arduino
main.js
chunk-home.js
chunk-user.js
浏览器根据需要加载。
4.9 SSR 下的 Code Splitting
普通 CSR:
sql
Dynamic Import
↓
Browser
↓
Load Chunk
但 SSR 更复杂。
因为服务端生成 HTML 时,需要知道:
当前页面到底需要哪些 JS / CSS Chunk?
因此项目使用:
ChunkExtractor
以及:
scss
extractor.collectChunks(...)
服务端:
css
Route
↓
Component
↓
Loadable
↓
Chunk
↓
ChunkExtractor
↓
HTML
然后:
scss
extractor.getScriptTags()
extractor.getLinkTags()
extractor.getStyleTags()
把真正需要的资源注入 HTML。
这就是 SSR Code Splitting 的关键。
4.10 HMR
开发环境:
vbnet
entry: [
'webpack-hot-middleware/client?reload=true',
'./src/client',
]
同时:
arduino
new webpack.HotModuleReplacementPlugin()
以及:
scss
require('webpack-hot-middleware')(...)
最终形成:
修改代码
↓
Webpack 重新编译
↓
HMR
↓
浏览器收到更新
↓
页面局部更新
这就是:
Hot Module Replacement
4.11 Webpack Dev Middleware
原项目把 Webpack 和 Express 组合起来:
php
const compiler =
webpack(webpackConfig);
const instance =
require(
'webpack-dev-middleware'
)(
compiler,
{
headers: {
'Access-Control-Allow-Origin':
'*',
},
serverSideRender: true,
}
);
app.use(instance);
这样:
markdown
Express
+
Webpack
就可以组成一个开发服务器。
第五章:从 2023 项目实战重新理解 2026 前端工程化
前面的项目必须保留。
但到了 2026 年,我们不能把这个项目直接当成:
"今天新项目应该照着这样搭。"
更准确的定位应该是:
这是一个非常完整的传统 React SSR 工程案例。
它的价值不是让你今天复制一份 Webpack 配置,而是帮助你理解现代前端工具链是怎么演化过来的。
5.1 哪些东西今天仍然值得学习?
非常多。
第一:客户端 / 服务端边界
你仍然需要理解:
arduino
Browser
vs
Server
尤其是:
SSR
Hydration
Data Fetching
Serialization
这些概念在现代 React 中仍然重要。
第二:构建系统
虽然很多项目不再手写如此复杂的 Webpack 配置,但你仍然需要知道:
css
Entry
↓
Module
↓
Loader / Transform
↓
Plugin
↓
Chunk
↓
Asset
因为当构建出现问题时:
为什么这个文件没有被转换?
为什么这个依赖没有被打包?
为什么 Bundle 变大?
为什么 CSS 没有进入 SSR?
为什么服务端和客户端构建结果不一样?
这些问题最终都需要工程化知识。
第三:Code Splitting
Code Splitting 没有消失。
只是:
loadable-component
并不是今天唯一的实现方式。
现代前端依然非常关注:
sql
Initial JS
Chunk
Lazy Loading
Route Splitting
Caching
第四:状态与数据边界
原项目中的:
Redux
loadData
initialState
Hydration
其实是在解决:
服务端数据如何安全地进入客户端状态。
今天即使使用其他数据方案,这个问题仍然存在。
5.2 Webpack 到现代构建工具
2023 年学习这个项目时:
diff
Webpack
+
Babel
是非常典型的工程组合。
但到 2026 年,前端构建工具已经明显变化。
现代项目可能使用:
diff
Vite
+
Rolldown
+
Oxc
或者:
Webpack
继续承担复杂工程场景。
Vite 8 已经切换到 Rolldown 作为核心 Bundler,并继续推进 Rust-based toolchain。(vite.dev)
所以今天学习 Webpack,应该建立这样的认知:
Webpack
不是前端工程化本身
Webpack
只是工程化体系中的一个实现。
5.3 Babel 到今天的位置
原项目使用:
babel-loader
来处理:
TS
JS
JSX
TSX
2026 年仍然需要理解 Babel 的:
css
Parser
↓
AST
↓
Plugin
↓
Preset
↓
Transform
但现代项目不一定所有代码都由 Babel 完成。
现在还会看到:
SWC
esbuild
Oxc
等工具。
因此不要把:
ini
Babel
= JavaScript 编译器
理解得过于绝对。
更应该理解:
JavaScript / TypeScript 的源码转换,是一个独立于框架的工程能力。
5.4 传统 SSR 到现代 React Server Architecture
原项目的 SSR 是:
css
Express
↓
matchRoutes
↓
loadData
↓
Redux
↓
renderToString
↓
HTML
↓
Hydration
这是非常经典的 SSR 架构。
但现代 React 的服务端架构已经进一步发展。
React 官方现在提供:
arduino
Server Components
Server Rendering APIs
Streaming
Suspense
等能力。
因此今天理解 SSR,可以继续向:
renderToString
之后学习:
diff
Streaming SSR
+
Suspense
+
Server Components
React 官方目前仍将 Server Components、Server Rendering、Hydration 等作为现代 React 服务端架构的重要组成部分。(react.dev)
5.5 React 项目今天还需要 Redux 吗?
这个问题不应该简单回答:
需要
或者:
不需要
更合理的理解是:
arduino
UI State
Server State
Client State
Global State
Form State
URL State
并不是所有状态都应该进入 Redux。
原项目使用 Redux 是因为它需要处理:
diff
SSR
+
Initial State
+
Data Fetching
+
Client Hydration
今天如果项目使用:
arduino
React Server Components
+
Server State
+
其他数据层
架构可能完全不同。
所以学习 Redux 的价值,也应该从:
"我必须使用 Redux"
转变成:
理解集中式状态管理解决了什么问题。
一个完整请求到底发生了什么?
现在把整个项目串起来。
假设浏览器访问:
sql
GET /UserInfo/100
服务端阶段
scss
Browser
↓
HTTP Request
↓
Express
↓
SSR Middleware
↓
matchRoutes()
↓
UserInfo Route
↓
loadUserInfoData()
↓
API Request
↓
Redux Store
↓
renderToString()
↓
React HTML
↓
initialState
↓
ChunkExtractor
↓
HTML Template
浏览器阶段
sql
HTML
↓
First Paint
↓
CSS
↓
JavaScript
↓
Loadable Chunks
↓
React
↓
Redux Initial State
↓
Hydration
↓
Interactive
开发环境阶段
如果你修改:
UserInfo.tsx
则:
css
Source
↓
Webpack
↓
Babel / Transform
↓
HMR
↓
Browser
↓
React Refresh
生产环境阶段
最终:
css
Source
↓
Type Check
↓
Transform
↓
Webpack / Bundler
↓
Minification
↓
Code Splitting
↓
Hash
↓
Assets
↓
Server
↓
Browser
这才是这篇项目实战真正想让你掌握的东西。
2026 年学习这个项目,应该怎么学?
不要第一遍就试图背:
arduino
webpack.config.ts
正确的学习顺序应该是:
arduino
第一步
项目目录
↓
理解 Client / Server
然后:
第二步
React
↓
Redux
↓
Router
↓
页面
然后:
第三步
Express
↓
SSR
↓
matchRoutes
↓
loadData
↓
renderToString
↓
Hydration
然后:
第四步
Webpack
↓
Loader
↓
Plugin
↓
Entry
↓
Output
↓
Chunk
最后:
arduino
第五步
HMR
↓
Code Splitting
↓
Bundle Analysis
↓
Client / Server Dual Build
最后再把它和现代方案对照:
arduino
Webpack
↓
Vite / Rolldown
Babel
↓
SWC / Oxc / esbuild 等
renderToString
↓
Streaming SSR
传统 React SSR
↓
现代 Server Components / Server Rendering
Redux 全局状态
↓
按状态类型重新划分数据边界
总结
这套 ES6 React 项目最大的价值,不是:
让你记住一份 Webpack 配置。
而是让你完整走一遍:
css
源码
↓
React
↓
状态管理
↓
路由
↓
API
↓
Express
↓
SSR
↓
Webpack
↓
Bundle
↓
HTML
↓
Browser
↓
Hydration
你真正应该掌握的是:
1. 一个前端项目如何组织
css
src
client
server
pages
components
routes
store
services
webpack
2. 一个 SSR 应用如何工作
css
Request
↓
Route
↓
Data
↓
Store
↓
Render
↓
HTML
↓
Hydration
3. 一个构建系统如何工作
css
Source
↓
Parse / Transform
↓
Module
↓
Bundle
↓
Chunk
↓
Asset
4. 一个开发环境如何工作
css
Code Change
↓
Compile
↓
HMR
↓
Browser
5. 一个现代前端工程师应该如何看待旧技术
不是简单地:
Webpack 过时了
或者:
SSR 过时了
而是:
markdown
旧方案解决了什么问题?
↓
现代方案为什么改变?
↓
底层问题有没有改变?
↓
今天应该用什么方式解决?
这才是工程化学习真正有价值的地方。
技术会变化,但问题不会凭空消失。
Webpack 会变化,构建问题仍然存在。
Redux 的使用方式会变化,状态管理问题仍然存在。
SSR 的实现方式会变化,服务端渲染与客户端接管问题仍然存在。
工具会变化,但理解工具背后的运行机制,才是前端工程能力真正可以迁移的部分。