一、先搞懂两个核心概念,别被专业词绕晕

很多人一听到服务端渲染、Webpack配置、模块联邦这些词就发懵,其实咱们拆成大白话讲,就跟家里做饭一样好理解。 先讲服务端渲染,简单说就是你点外卖,商家提前把饭做好(不是你自己在家现做),直接给你送过来,你拿到就能吃,不用等做饭的过程。对应到网站上,就是用户点一个页面,服务器先把这个页面的内容提前拼好,直接给用户浏览器发完整的页面,用户打开就能看到内容,不用等浏览器自己再拼接内容。 再讲Webpack配置不同入口,入口就相当于家里不同的做饭场景,比如一个入口是做早餐(PC端首页),一个入口是做午餐(移动端列表页),每个入口对应一套做饭的流程(代码打包规则)。公共代码提取就是把早餐和午餐都要用到的大米、油盐酱醋(比如公共的工具函数、公共样式)单独拿出来放一个公共调料包,不用每个场景都单独准备一套,省地方还方便。 模块联邦就更简单了,相当于你和邻居共用一个调料店,你家缺盐了直接去邻居家拿,不用自己买;邻居家缺酱油了也能直接来你家拿,两家的调料(代码模块)能互相借,不用每家都囤全。

二、为什么要在服务端渲染场景下做这些配置?

很多人会问,普通的网站打包不就行了,为啥要搞这么复杂?咱们结合实际的场景说,比如你做了一个电商网站,既要给电脑端用户看,也要给手机端用户看,电脑端和手机端的页面大部分逻辑是一样的,比如都要用到登录验证、购物车计算、公共的按钮样式,要是每个入口都把这些代码打包进去,最终打包出来的文件就会很大,用户打开页面的速度就会很慢。 尤其是服务端渲染的场景,服务器要提前把页面拼好,要是每个页面的代码都很大,服务器拼页面的速度就会变慢,用户等待的时间就会变长,体验就会变差。要是再加上多个项目共用一套公共代码,比如你公司同时做电商和社区两个网站,两个网站都要用到用户信息管理的代码,要是每个网站都单独打包这套代码,就会造成重复,浪费服务器的存储空间和用户的流量。 所以在服务端渲染场景下,把不同入口的公共代码提取出来,再用模块联邦实现代码共享,就相当于把重复的东西都省掉,让页面加载更快,服务器运行更顺畅,开发也更省心。

三、完整示例演示,手把手教你配置

3.1 明确示例技术栈

咱们的示例统一使用以下技术栈:Node.js 18、Webpack 5、React 18、React Router 6、Express(服务端渲染框架),所有配置和代码都围绕这套技术栈展开,方便大家跟着操作。

3.2 项目结构梳理

先给大家说清楚项目的结构,方便大家对应代码:

my-ssr-project/
├── src/
│   ├── client/          # 客户端代码
│   │   ├── pc-entry.js  # PC端入口
│   │   └── mobile-entry.js # 移动端入口
│   ├── server/          # 服务端代码
│   │   └── index.js     # 服务端渲染入口
│   ├── common/          # 公共代码
│   │   ├── utils.js     # 公共工具函数
│   │   └── style.css    # 公共样式
│   └── components/     # 公共组件
│       └── Button.js   # 公共按钮组件
├── webpack/             # Webpack配置文件夹
│   ├── client.config.js # 客户端Webpack配置
│   └── server.config.js # 服务端Webpack配置
└── package.json

3.3 公共代码提取配置

公共代码提取的核心是用Webpack的SplitChunksPlugin插件,相当于把所有入口都用到的公共代码单独拎出来打包成一个公共文件。咱们先看客户端的Webpack配置,也就是webpack/client.config.js,里面的SplitChunks配置就是用来提取公共代码的:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  // 定义两个入口,PC端和移动端
  entry: {
    pc: path.resolve(__dirname, '../src/client/pc-entry.js'),
    mobile: path.resolve(__dirname, '../src/client/mobile-entry.js'),
  },
  output: {
    // 打包后的文件输出路径
    path: path.resolve(__dirname, '../dist/client'),
    // 打包后的文件名,[name]对应入口名,[contenthash]是文件内容哈希,用来做缓存
    filename: '[name].[contenthash].js',
    // 公共文件的输出路径
    publicPath: '/client/',
  },
  module: {
    rules: [
      // 处理JSX文件
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
      // 处理CSS文件
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    // 生成PC端的HTML模板
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../src/client/pc-template.html'),
      filename: 'pc.html',
      chunks: ['pc', 'common'], // 引入PC端入口和公共代码
    }),
    // 生成移动端的HTML模板
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../src/client/mobile-template.html'),
      filename: 'mobile.html',
      chunks: ['mobile', 'common'], // 引入移动端入口和公共代码
    }),
  ],
  // 核心配置:提取公共代码
  optimization: {
    splitChunks: {
      chunks: 'all', // 所有入口的代码都参与拆分
      name: 'common', // 拆分出来的公共文件名叫common
      minSize: 0, // 只要有公共代码就拆分,不管大小
      minChunks: 2, // 只要被两个及以上入口引用就拆分
    },
  },
};

这里的minChunks:2就是说,只要PC端和移动端都用到的代码,就会被提取到common文件里,比如咱们的公共工具函数utils.js、公共样式style.css、公共按钮组件Button.js,都会被打包到common.js里,不用每个入口都单独打包一份。

3.4 模块联邦配置

模块联邦的核心是用Webpack的ModuleFederationPlugin插件,相当于把公共代码打包成一个可以被其他项目引用的“模块库”。咱们先修改客户端的Webpack配置,在plugins里加入模块联邦的配置:

// 上面的配置不变,只修改plugins部分
plugins: [
  // 模块联邦配置
  new ModuleFederationPlugin({
    name: 'commonLibrary', // 模块联邦的名称,相当于公共调料店的名字
    filename: 'common-library.js', // 模块联邦打包后的文件名
    exposes: { // 把哪些公共代码暴露出去,供其他项目引用
      './Button': '../src/components/Button.js', // 暴露公共按钮组件
      './utils': '../src/common/utils.js', // 暴露公共工具函数
    },
    shared: { // 共享的依赖,比如React、ReactDOM,不用每个项目都单独打包
      react: { singleton: true, eager: true },
      'react-dom': { singleton: true, eager: true },
    },
  }),
  // 下面的HtmlWebpackPlugin配置不变
  new HtmlWebpackPlugin({
    template: path.resolve(__dirname, '../src/client/pc-template.html'),
    filename: 'pc.html',
    chunks: ['pc', 'common'],
  }),
  new HtmlWebpackPlugin({
    template: path.resolve(__dirname, '../src/client/mobile-template.html'),
    filename: 'mobile.html',
    chunks: ['mobile', 'common'],
  }),
],

这里的exposes就是把咱们的公共代码打包成一个可以被其他项目引用的模块,比如其他项目想要用到这个公共按钮组件,直接引用commonLibrary/Button就可以了。shared是把React、ReactDOM这些公共依赖共享出去,避免其他项目重复打包,进一步减少代码体积。

3.5 服务端渲染的配置

服务端渲染的核心是Express框架,咱们看src/server/index.js的代码,服务端需要读取打包后的HTML文件,再把内容渲染给用户:

const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();

// 静态资源路径,用来加载打包后的JS、CSS文件
app.use('/client', express.static(path.resolve(__dirname, '../../dist/client')));

// PC端路由
app.get('/', (req, res) => {
  // 读取PC端的HTML模板
  const html = fs.readFileSync(path.resolve(__dirname, '../../dist/client/pc.html'), 'utf8');
  // 服务端渲染逻辑,比如拼接用户信息、购物车内容等
  const renderedHtml = html.replace('<!-- SSR_CONTENT -->', '<div>PC端首页内容</div>');
  res.send(renderedHtml);
});

// 移动端路由
app.get('/mobile', (req, res) => {
  // 读取移动端的HTML模板
  const html = fs.readFileSync(path.resolve(__dirname, '../../dist/client/mobile.html'), 'utf8');
  // 服务端渲染逻辑
  const renderedHtml = html.replace('<!-- SSR_CONTENT -->', '<div>移动端首页内容</div>');
  res.send(renderedHtml);
});

// 启动服务
app.listen(3000, () => {
  console.log('服务端渲染服务启动成功,访问地址:http://localhost:3000');
});

这里的服务端代码很简单,就是根据用户访问的路由,读取对应的HTML模板,再把内容渲染给用户。因为咱们已经把公共代码提取出来了,服务端渲染的时候,只需要加载一次公共代码,不用每个页面都加载一次,大大提升了服务端的渲染速度。

四、技术优缺点和注意事项

4.1 技术优点

首先是减少代码重复,不同入口的公共代码只打包一次,不用每个入口都单独打包,减少了代码体积,提升了页面加载速度。其次是提升开发效率,公共代码可以被多个项目共享,不用每个项目都重复开发同样的功能,比如公共按钮组件、公共工具函数,只需要开发一次,其他项目直接引用就可以。最后是优化服务端渲染性能,服务端只需要加载一次公共代码,不用每个页面都加载一次,提升了服务端的渲染速度,减少了服务器的压力。

4.2 技术缺点

首先是配置复杂度提升,Webpack的配置、公共代码提取、模块联邦的配置都需要一定的学习成本,新手可能会觉得配置很复杂。其次是依赖管理难度增加,模块联邦的共享依赖需要配置正确,要是配置错了,可能会出现依赖冲突的问题,比如不同项目用的React版本不一样,就会导致页面报错。最后是调试难度增加,公共代码被提取出来之后,要是出现问题,需要同时看多个入口的代码,调试起来比单个入口的项目要麻烦。

4.3 注意事项

首先是公共代码的拆分粒度要合适,不能拆得太细,也不能拆得太粗。拆得太细会导致公共文件太多,增加了HTTP请求的数量,反而会降低页面加载速度;拆得太粗会导致公共文件太大,加载时间变长。一般来说,只要是两个及以上入口都用到的代码,就可以拆分出来。其次是模块联邦的共享依赖要配置正确,共享的依赖必须是所有项目都用到的,而且版本要尽量一致,避免出现依赖冲突的问题。最后是服务端渲染的缓存要配置好,服务端渲染的页面可以做缓存,比如把常用的页面缓存起来,不用每次都重新渲染,进一步提升服务端的性能。

五、文章总结

服务端渲染场景下的Webpack配置不同入口的公共代码提取与模块联邦协同,本质上就是通过合理的配置,减少代码重复,提升开发效率和页面加载速度,优化服务端渲染的性能。咱们通过大白话的解释和完整的示例,把复杂的配置拆解成了一个个简单的步骤,新手也能跟着操作。 其实不管是公共代码提取还是模块联邦,核心都是“复用”,复用代码、复用资源,让开发更高效,让用户体验更好。只要掌握了核心的原理,再结合实际的项目需求,合理配置,就能发挥出这些技术的最大优势。