Next.js & Module Federation

Next 12Next 13Next 14SSR (Pages Router) App Router

Demo Reference

在此处查看示例项目列表: Next.js SSR

设置环境

在开始之前,你需要安装 Node.js,并确保你的 Node.js 版本 >= 16。我们推荐使用 Node.js 20 的 LTS 版本。

你可以使用以下命令检查当前使用的 Node.js 版本:

node -v

如果你当前环境中没有安装 Node.js,或者安装的版本太低,你可以使用 nvmfnm 来安装所需的版本。

以下是通过 nvm 安装 Node.js 20 LTS 版本的例子:

# Install the long-term support version of Node.js 20
nvm install 20 --lts

# Make the newly installed Node.js 20 as the default version
nvm alias default 20

# Switch to the newly installed Node.js 20
nvm use 20

步骤 1: 设置 Next.js 应用

创建 Next.js 项目

你可以使用 create-next-app 创建 Next.js 项目。只需执行以下命令:

npm
yarn
pnpm
bun
npx create-next-app@latest

创建 App 1

npx create-next-app@latest

"What is your project named?":
> mfe1

"Would you like to use App Router?":
> No

创建 App 2

npx create-next-app@latest

"What is your project named?":
> mfe2

"Would you like to use App Router?":
> No

安装

cd mfe1
pnpm add @module-federation/nextjs-mf webpack -D
pnpm i
cd mfe2
pnpm add @module-federation/nextjs-mf webpack -D
pnpm i

现有项目

npm
yarn
pnpm
bun
npm i @module-federation/nextjs-mf webpack -D
next.config.mjs
import { NextFederationPlugin } from '@module-federation/nextjs-mf';

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  webpack(config,options ){
    config.plugins.push(
      new NextFederationPlugin({
        name: 'mfe1',
        filename: 'static/chunks/remoteEntry.js',
        remotes: {
          mfe2: `http://localhost:3001/static/${options.isServer ? 'ssr' : 'chunks'}/remoteEntry.js`,
        },
        shared: {},
        extraOptions: {
          exposePages: true,
          enableImageLoaderFix: true,
          enableUrlLoaderFix: true,
        },
      })
    )
    return config
  }
};

export default nextConfig;

步骤 2: 覆盖 Webpack

设置本地 Webpack 环境

“本地 Webpack”意味着你必须将 webpack 作为依赖项安装,接下来将不会使用其捆绑的 webpack 副本,该副本无法使用,因为它不会导出所有 Webpack 内部结构

shell
cross-env NEXT_PRIVATE_LOCAL_WEBPACK=true next dev
# or
cross-env NEXT_PRIVATE_LOCAL_WEBPACK=true next build

.env 也可以设置,但及时设置 NEXT_PRIVATE_LOCAL_WEBPACK 可能不可靠。

.env
NEXT_PRIVATE_LOCAL_WEBPACK=true

确保已经手动安装过 Webpack

必须安装Webpack,否则构建会抛出MODULE_NOT_FOUND错误

npm
yarn
pnpm
bun
npm i webpack -D

步骤 3: 实现 SSR

增加服务生命周期

为了确保 Next.js 创建服务器运行时,"_document" 必须实现 "getInitialProps " 或 "getServerSideProps"

如果没有服务器生命周期方法,接下来将尝试它认为是静态的“SSG”页面。

如果没有服务器运行时,就没有服务器来呈现远程更新

_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return initialProps;
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

将重新验证添加到热重载节点

为了处理远程模块中的更新,使用“revalidate”函数。这是因为 Webpack 使用块缓存并且不会获取已加载的块,并且热服务器不会自动识别更新。

实施重新验证有两种主要方法:

  • 渲染阻塞
  • 重新验证时陈旧
Render Blocking
Stale While Revalidate

建议大多数用例使用此实现,因为它可以确保服务器和客户端始终同步,从而有助于避免水合错误。通过在渲染之前阻止并检查更新,你可以保证你的应用程序始终是最新的,而不会对用户体验产生负面影响。

怎么运行的:

  • 在渲染页面之前,服务器检查是否有任何可用更新。
  • 如果有可用更新,它会在响应客户端请求之前继续进行热模块更换 (HMR)。
  • 此方法确保所有用户都能收到最新版本的应用程序,而不会遇到服务器呈现的内容和客户端呈现的内容之间不一致的情况。

实施示例:

_document
import { revalidate } from '@module-federation/nextjs-mf/utils';
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    if (ctx?.pathname && !ctx?.pathname?.endsWith('_error')) {
      await revalidate().then((shouldUpdate) => {
        if (shouldUpdate) {
          console.log('Hot Module Replacement (HMR) activated', shouldUpdate);
        }
      });
    }

    const initialProps = await Document.getInitialProps(ctx);
    return initialProps;
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

热更新

热更新尝试在服务端渲染(SSR)期间“刷新”使用中的分块,以便将 <script> 标签发送到浏览器。

_document
import Document, { Html, Head, Main, NextScript } from 'next/document';
import {
  revalidate,
  FlushedChunks,
  flushChunks,
} from '@module-federation/nextjs-mf/utils';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    if (ctx.pathname) {
      if (!ctx.pathname.endsWith('_error')) {
        await revalidate().then((shouldUpdate) => {
          if (shouldUpdate) {
            console.log('should HMR', shouldUpdate);
          }
        });
      }
    }

    const initialProps = await Document.getInitialProps(ctx);

    const chunks = await flushChunks();

    return {
      ...initialProps,
      chunks,
    };
  }

  render() {
    return (
      <Html>
        <Head>
          <FlushedChunks chunks={this.props.chunks} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

步骤 4: 创建并导出

现在,创建一个从“MFE2”公开的组件

创建按钮组件

在“MFE2”中,在 src 目录中创建一个名为“Button.js”的新文件,其中包含以下内容:

import React from 'react';

const Button = () => (
  <button>MFE2 Button</button>
);

export default Button;

配置 next MFE2

更新构建配置以公开模块

next.config.mjs
const nextConfig = {
  reactStrictMode: true,
  webpack(config, options) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'mfe2',
        filename: 'static/chunks/remoteEntry.js',
        exposes: {
          "./Button": './component/Button.js',
        },
        shared: {},
        extraOptions: {
          exposePages: true,
          enableImageLoaderFix: true,
          enableUrlLoaderFix: true,
        },
      })
    )
    return config
  }
};

步骤 5: 消费远程模块

在 "MFE1" 中使用 "MFE2" 公开的模块

导入模块

在“MFE1”的其中一页中导入“MFE2”

index.js
import React from 'react';
import Button from 'mfe2/Button'; // federated import

const Index = () => {
  return (
    <div>
      <h1>MFE1</h1>
      <Button />
    </div>
  );
}

将服务器生命周期方法添加到页面

接下来还将尝试没有某些数据生命周期的“SSG”页面。

确保将其添加到页面文件中。

export const getServerSideProps = async () => {
  return {
    props: {}
  }
}
// or
Index.getInitialProps = async ()=> {
  return {}
}

export default Index;

配置 Next in MFE1

相应地更新“remotes”字段

next.config.mjs
import { NextFederationPlugin } from '@module-federation/nextjs-mf';

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  webpack(config,options ){
    config.plugins.push(
      new NextFederationPlugin({
        name: 'mfe1',
        filename: 'static/chunks/remoteEntry.js',
        remotes: {
          mfe2: `http://localhost:3001/static/${options.isServer ? 'ssr' : 'chunks'}/remoteEntry.js`,
        },
        shared: {},
        extraOptions: {
          exposePages: true,
          enableImageLoaderFix: true,
          enableUrlLoaderFix: true,
        },
      })
    )
    return config
  }
};

export default nextConfig;