Next.js Multi Zones 实战指南:用 rewrites 把多个 Next.js 应用聚合成单一域名
【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js
Multi Zones 是 Next.js 官方向微前端演进的一种落地方式:把原本运行在同一个域名下的巨型应用,拆解为多个彼此独立、各自负责一批 URL 路径的小型 Next.js 应用(zone),再由其中一个入口应用借助rewrites把不属于自己的路径转发给对应的 zone。本文以仓库中的 examples/with-zones 官方示例(含home与blog两个 zone)为骨架,讲解 Multi Zones 的拆分解耦思路、rewrites+assetPrefix/basePath的实现细节、本地联调方法、部署到单一域名下多个 Vercel 项目的完整流程,以及用单元测试预先验证 rewrite 路由逻辑的工程实践。
Multi Zones 是什么:从"一个大应用"到"一批 zone"
Multi Zones 把"部署在同一个域名下的一个大型 Next.js 应用"重新组织为一组更小的、按路径分工的应用:每个应用被称为一个 zone,各自只服务一组 URL 路径,并独立安装依赖、独立开发、独立构建与独立部署。它的适用场景很典型——当某个应用中存在一批与其余页面几乎无关的页面集合时,将这些页面挪到独立 zone,就能:
- 缩小主应用的体积,让构建(build)时间显著下降;
- 移除只在某一个 zone 才需要、却长期拖累主应用的代码;
- 各 zone 可以按自身节奏升级、扩容,团队边界与发布边界随之清晰。
Multi Zones 应用之所以能被用户感知为"同一个网站",是因为由其中一个 zone 充当入口:入口应用的next.config.js用rewrites特性 把某些请求路径代理转发到其他 zone。前提约束是:同一个域名下的所有 URL 路径必须在所有 zone 之间全局唯一,否则路由归属会产生冲突。
示例拆解:home与blog两个 zone 的分工
本示例由两个完全独立的 Next.js 应用组成,目录结构如下:
- examples/with-zones/home:主应用 zone,负责
/、/about等全部未指派给 blog 的路径;它把指向 blog 的请求通过rewrites转发出去。 - examples/with-zones/blog:子应用 zone,专门负责
/blog与/blog/*路径,通过assetPrefix保证自身资源与 home 互不冲突。
两个应用都有自己的package.json(版本号各写各的、依赖独立安装),各自的页面代码互不感知。看具体的文件布局:
- home 的路由:
/对应 home/app/page.tsx,/about对应 home/app/about/page.tsx,公共头部由 home/components/Header.tsx 提供; - blog 的路由:页面被刻意放在
blog/app/blog/子目录中,于是路由天然带/blog前缀——blog/app/blog/page.tsx 是博客列表页,blog/app/blog/post/[id]/page.tsx 是动态文章详情页,blog/app/blog/layout.tsx 定义了 zone 自己的根布局。
在示例页面之间你还能看到一条关键约束的体现:home 首页用普通<a href="/blog">跳往 blog,而 blog 内部用next/link维护/blog、/blog/post/1等相对路径。因为 blog 将assetPrefix设为/blog-static,其页面对外链接的前缀依然是/blog而非/blog-static。
核心机制:入口 zone 用rewrites做请求代理
Multi Zones 的"路由汇聚"完全发生在入口应用 home 的 next.config.js 中:
const { BLOG_URL } = process.env; /** @type {import('next').NextConfig} */ const nextConfig = { async rewrites() { return [ { source: "/blog", destination: `${BLOG_URL}/blog`, }, { source: "/blog/:path+", destination: `${BLOG_URL}/blog/:path+`, }, { source: "/blog-static/_next/:path+", destination: `${BLOG_URL}/blog-static/_next/:path+`, }, ]; }, }; module.exports = nextConfig;这段配置体现了 Multi Zones 入口的完整职责:
- 业务路径转发:
/blog与/blog/:path+被整体透传给 blog zone。:path+是 Next.js rewrite 的通配参数语法,含义是"匹配一个或多个路径段",因此/blog/post/1、/blog/whatever都能命中第二条规则,并把捕获到的路径段原样拼到目标地址上。 - 静态资源转发:
/blog-static/_next/:path+被单独转发。浏览器访问被代理回来的 blog 页面时,页面 HTML 里引用的_next/static脚本与样式会请求/blog-static/_next/...,这条 rewrite 确保这些资源请求也被送回 blog zone,由它自己处理。若不转发静态资源,blog 页面即使成功返回 HTML,也无法在 home 域名下正确加载样式与 JS。 - 目标地址来自环境变量:
destination拼接的是process.env.BLOG_URL,也就是说 home 通过环境变量得知"blog zone 部署在哪里"。这正是本地与生产可复用的关键:同一份配置,改变BLOG_URL的值即可指向本机 dev server 或线上部署。
需要说明的细节:当rewrites()返回一个普通数组时,默认规则语义等同于afterFiles(先查文件系统/页面,再命中 rewrite);Next.js 也允许返回{ beforeFiles, afterFiles, fallback }结构来显式控制三个阶段。对本示例来说,/blog、/blog-static/...不会与 home 自身页面撞路径,因此简单数组形式已足够清晰。单元测试(见下文)在合并规则时兼容了两种返回形态,这从侧面印证了两种写法都被框架支持。
Zone 资源隔离:assetPrefix与basePath怎么选
本示例的实际配置:assetPrefix+ 目录前缀
两个 zone 部署在同一域名后,浏览器看到的资源 URL 也必须互不冲突。blog zone 的 next.config.js 是这样做的:
/** @type {import('next').NextConfig} */ const nextConfig = { assetPrefix: "/blog-static", }; module.exports = nextConfig;配合把页面放在app/blog/子目录下,blog zone 用一套组合拳完成了隔离:目录结构提供路由前缀/blog,而assetPrefix把 Next.js 生成页面里引用的静态资源路径整体加上了/blog-static前缀(如/blog-static/_next/static/chunks/xxx.js)。home 的第三条 rewrite 再把/blog-static/_next/:path+回发给 blog,形成闭环。home 自己没有assetPrefix,它产出的_next资源就保持在默认路径,与 blog 的/blog-static前缀天然错开。
basePath的作用与边界
官方文档强调basePath会自动给应用内所有页面加上统一前缀,包括相对链接。这意味着如果你把basePath设为/blog,那么/会被自动解析为/blog、/about会被解析为/blog/about。
正因为basePath对"应用内全部页面"生效,它只适合"整个应用所有页面共享同一个前缀"的情形。如果很多页面并不共享相同的前缀——例如/home和/blog同处一个 zone——那么更合适的选择是assetPrefix:它只给 Next.js 生成的静态资源(_next/static等)加上独特前缀,不影响任何页面路由本身。这也解释了为何本示例的 blog zone 采用assetPrefix并把路由交给目录层级解决,而不是依赖basePath去"顺带改"所有链接——对于 zone 间资源去重、页面路径各自独立维护的场景,assetPrefix是更精准的隔离手段。
本地运行两个 zone
与单一应用不同,Multi Zones 意味着"多个 Next.js 应用叠加在一个站点上",因此每个 app 都拥有自己的依赖并独立运行。
用脚手架快速复刻示例
在仓库里,该示例的入口在 examples/with-zones(根package.json仅为占位,实际依赖都在home/与blog/两个子目录中)。如果要从零开始体验 Multi Zones,可借助官方示例引导(命令将生成独立的with-zones-app项目目录):
npx create-next-app --example with-zones with-zones-appyarn create next-app --example with-zones with-zones-apppnpm create next-app --example with-zones with-zones-app分别启动 home 与 blog
先在仓库根目录启动/homezone(对应端口 3000):
cd home npm install && npm run dev # or cd home yarn && yarn dev # or cd home pnpm install && pnpm dev启动成功后 home 应用即运行在 http://localhost:3000。再打开一个新的终端启动/blogzone——注意 blog/package.json 的dev脚本显式带了端口参数next dev -p 4000,这是为了让两个应用能在本机同时监听而不冲突:
cd blog npm install && npm run dev # or cd blog yarn && yarn dev # or cd blog pnpm install && pnpm devblog 应用应运行在 http://localhost:4000/blog。
本地联调的一点提醒
home 的rewrites依赖环境变量BLOG_URL。示例 README 在本地阶段建议直接访问各 zone 自己的地址(如4000/blog)来独立验证功能;如果希望在本机也走"单一域名 + 代理"的完整链路(即访问localhost:3000/blog由 home 转发到 blog),则可以仿照下文部署章节的做法,为 home 配置BLOG_URL=http://localhost:4000后再访问 home 域名下对应路径,这与线上 Vercel 的转发行为保持一致。
上线前先验证:用 Jest 单测锁定 rewrite 逻辑
Multi Zones 的路由正确性直接决定站点可用性,而 home 仓库为此内置了一套针对next.config.js的单元测试,实现在 home/test/next-config.test.ts,配套的 Jest 配置见 home/jest.config.js(通过next/jest创建,测试环境为jsdom,匹配./**/*.test.{ts,tsx})。这套测试的思路非常值得借鉴:在真实部署之前,用 path-to-regexp 模拟 Next.js 的 source→destination 匹配过程(match()负责命中source,compile()负责把捕获的参数回填进destination),从而验证 rewrite 会不会按预期转发、会不会误伤 home 自身的路径。测试的核心断言如下:
- 非 blog 路径不被转发:
/、/blog-not、/blog2均返回undefined(即 home 自己处理); - blog 路径被正确转发到子 zone:
/blog→https://with-zones-blog.vercel.app/blog,/blog/post/1→https://with-zones-blog.vercel.app/blog/post/1; - blog 静态资源被转发到子 zone:
/blog-static/_next/static/chunks/chunk.css→https://with-zones-blog.vercel.app/blog-static/_next/static/chunks/chunk.css。
注意测试开头便设置了process.env.BLOG_URL = "https://with-zones-blog.vercel.app",说明在 CI 或部署流水线中,同样的测试可以换成任意环境值来校验目标。它同时兼容了rewrites()返回数组与返回{ beforeFiles, afterFiles }两种结构,也演示了对destination进行主机拆分、参数编译等边界处理。把这类"配置逻辑可测试化"的做法带到你自己的 Multi Zones 工程里,可以在每次调整转发规则后获得快速、确定性的反馈。
部署:把多个 zone 聚合到同一域名
Multi Zones 生产部署的核心诉求是"每个 zone 一个独立项目、但共享同一个域名"。下文以 Vercel 为例(示例 README 的部署章节即围绕 Vercel 展开):借助其monorepo(多仓库子目录)支持,为每个 app 各创建一个项目。
第一步:先部署 blog 子 zone
将示例推送到 Git 托管平台并导入 Vercel 后,在导入流程中不选择仓库根目录,而是选中blog目录作为项目源码目录(注意:按示例说明应不要从 home 开始,先部署 blog,因为 home 的 rewrite 目标依赖 blog 的线上地址)。
点击 Continue 完成导入。随后把 Vercel 分配给该项目(blog zone)的域名地址复制出来,写入home/.env并提交到仓库,形式如下:
# 将此 URL 替换为你的 blog 应用的地址 BLOG_URL="https://with-zones-blog.vercel.app"这一步相当于把 blog zone 的"线上坐标"注入 home,home 的rewrites()运行时读到的正是这个变量。
第二步:再部署 home 主 zone
对同一个仓库再次走一遍导入流程,这次选择home目录(home/.env 中已配置BLOG_URL,因此它知道要把/blog请求发往何处):
当 home 应用部署完成后,两个应用就应该能在同一个域名下协同工作:用户访问https://你的域名/blog时,home 通过rewrites把请求代理给独立的 blog 部署,而 blog 页面引用的/blog-static/_next/...资源也经同一条链路被送回 blog 处理。
之后的更新与回滚
仓库后续的任何提交都会同时触发与其关联的两个 Vercel 项目的部署(Vercel 依据 monorepo 识别出哪些子目录发生了变化),两个 zone 可各自拥有独立的部署与回滚记录,这正是 Multi Zones 相比单体应用在发布边界上的优势所在。
落地要点与常见误区
回顾整个示例,把 Multi Zones 落到自己项目时建议把握以下几点:
- 路径全局唯一是铁律。所有 zone 的路由加起来必须互不重叠,入口 zone 的 rewrite 规则要精确圈定"哪些前缀属于哪些子 zone",否则会出现请求误转或永久转发的路由黑洞。
- 选对资源隔离手段:整个应用共享同一前缀时用
basePath最省事;页面前缀不统一、只想隔离_next静态资源时用assetPrefix。示例中的 blog zone 正是"app/blog目录承担路由前缀 +assetPrefix: '/blog-static'承担资源前缀"的组合实现,且必须配套入口 zone 对/blog-static/_next/:path+的静态资源转发,缺一条链路页面就会"有 HTML、没样式"。 - 目标地址用环境变量驱动。
BLOG_URL这类配置把 home 与 blog 的耦合降到最低:本地指向localhost:4000、预发指向预发部署、线上指向线上域名,next.config.js一行不改。 - 把转发规则变成可测试的代码。参考 home/test/next-config.test.ts,用
path-to-regexp直接对配置做单测,能提前捕获"误改路由"这类回归,是低成本高回报的防护网。 - 多 zone 意味着每个 app 都独立安装依赖、独立监听端口与独立部署,本地联调时务必注意端口错开(如本示例 blog 用
next dev -p 4000),CI/CD 里则要按子目录分别构建。
【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考