10. 现代前端工程化:ES6 React 项目实战与原理解析

这篇文章和普通的 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 的实现方式会变化,服务端渲染与客户端接管问题仍然存在。

工具会变化,但理解工具背后的运行机制,才是前端工程能力真正可以迁移的部分。

相关推荐
CopyCode1 小时前
用 AI 迁项目有多爽?我把 Webpack 迁 Vite 的全过程记下来了
前端·架构
去伪存真1 小时前
Electron 自动化发布指南:GitHub Actions 跨平台打包全纪录
前端·electron
计算机魔术师1 小时前
OpenAI智能体失控闯进美国政府网站,53张用户图片外泄背后
前端
颜进强1 小时前
14 · NestJS ExecutionContext 执行上下文:守卫、拦截器、过滤器拿到的"同一个 context",为什么能力不一样?
前端·后端·ai编程
程序员Flycan1 小时前
🚀 跨域终结者:前端代理服务器(Proxy)原理解析与配置总结
前端
怕浪猫1 小时前
顶级模型一句话,AI 写出了能玩的 QQ飞车
前端·面试·github
huakoh1 小时前
MCP 报错分不清?先看响应里是 result 还是 error
前端
呃呃呃呃ex1 小时前
9. TypeScript:从类型系统到工程实践
前端·面试
粥里有勺糖1 小时前
视野修炼-技术周刊第134期 | 豆豆眼头像
前端·github·aigc