多活动前端项目的隔离改造:从共享构建到独立交付
一、为什么会有这篇文章
我是一名在游戏公司 C 端团队工作的前端开发工程师。每逢节假日,不同游戏的运营团队都会上线各自的运营活动,我们通常会在组内前辈留下的活动项目上继续开发。
记得有一年国庆期间,几款游戏同时有活动需要上线,组内的前端工程师几乎人手一个活动。那段时间,测试同学经常问:“我正在测的活动怎么突然 404 了,有人在更新代码吗?”前端同学在上线前也不得不反复确认:“你之前的活动还有改动吗?我是否需要先同步代码,再重新打包?”
这些问题暴露出了旧活动项目的核心矛盾:多个活动共享同一套构建产物和发布空间。任何人重新打包或上传资源,都可能覆盖其他活动的文件,影响测试进度;修改公共组件时,也需要在团队内频繁同步。因此,我们需要同时解决测试环境的资源冲突和线上环境的发布耦合。
旧方案的结构和问题可以通过一张架构图展示:

- 构建时会处理所有活动。
- 多个活动共享资源目录。
- 上线活动 B 可能影响活动 A。
- 无法针对单个活动独立回滚。
- 构建产物过大,会增加发布和用户加载成本。
二、明确需要隔离的三个层次
在多活动并行开发的场景中,测试页面突然出现 404,通常是因为其他开发者正在上传新产物,或者在未同步他人代码的情况下直接重新构建。活动上线时,还要额外确认其他活动的文案、规则和逻辑是否有变动。要从根本上解决这些问题,需要建立三层隔离:
- 代码与路由隔离:构建活动 A 时,不应将活动 B、C 的页面纳入当前构建的模块依赖图。
- 静态资源隔离:不同活动的 JavaScript、CSS 和图片应拥有独立的 URL 前缀和存储目录。
- 部署与发布隔离:发布或回滚活动 A 时,不需要替换活动 B 的文件,也不需要重新发布活动 B。
三层隔离分别解决不同问题:
- 路由隔离控制构建入口,减少无关代码。
- 资源隔离控制资源 URL 和存储目录,避免文件覆盖与缓存污染。
- 发布隔离控制 NGINX Location、部署目录和发布流程,支持独立上线与回滚。

三、整体方案:源码共享,活动独立交付
这套方案不需要将每个活动拆成独立的 Git 仓库。真正需要隔离的是路由入口、构建配置、资源命名空间、部署目录和发布流程。
src/activities 各活动的业务代码
src/shared 公共组件、API、Store 和工具函数
src/router/routes 各活动路由
src/router/routes/bundles
不同活动的构建入口
package.json 活动构建命令
vite.config.ts 将构建参数转换为路由和资源配置
NGINX 将 URL 映射到活动部署目录
整体方案保持源码层共享,在构建层和部署层实现活动隔离。

四、路由改造:从全量注册到构建时选择
旧项目会在统一入口中注册所有活动路由:
const actARoutes = [...]
const actBRoutes = [...]
const actCRoutes = [...]
export const routes = [
...actARoutes,
...actBRoutes,
...actCRoutes,
]
即使页面使用了动态 import(),只要所有活动路由都被统一入口引用,生产构建时 Rollup 仍会从入口出发分析完整模块依赖图,并为这些动态导入生成对应的 chunk。在开发环境中,Vite 基于浏览器的原生 ESM 按需转换模块,因此问题往往不明显;但当活动增长到数百个甚至更多时,生产构建的时间和产物体积都会持续上升。
新方案将活动路由与构建入口分开:
routes/
├── actARoutes.ts
├── actBRoutes.ts
├── actCRoutes.ts
└── bundles/
├── all.ts
├── act-a.ts
├── act-b.ts
└── act-c.ts
actARoutes.ts 记录活动 A 需要的页面,bundles 目录中的文件则负责声明本次构建允许哪些活动进入产物。然后通过 Vite Alias 将统一别名 @route-bundle 指向本次要构建的 Bundle。
例如,构建命令传入 ROUTE_BUNDLE=act-a 时,vite.config.ts 可以进行如下处理:
import { defineConfig } from 'vite'
import { resolve } from 'node:path'
const routeBundleFile = process.env.ROUTE_BUNDLE || 'default'
export default defineConfig({
resolve: {
alias: {
'@route-bundle': resolve(
__dirname,
`src/router/routes/bundles/${routeBundleFile}.ts`,
),
},
},
})
路由入口只依赖统一别名:
import { baseRoutes } from '...'
import { bundleRoutes } from '@route-bundle'
export const routes = [...baseRoutes, ...bundleRoutes]
这样,不同构建命令会将 @route-bundle 解析到不同的活动入口。未被当前 Bundle 引用的活动路由不会进入当前构建的模块依赖图,从而避免生成无关活动的 chunk。

五、Vite 改造:用构建参数描述一个活动
在 package.json 中,可以为每个活动声明独立的构建命令:
"build:new-year": "cross-env ROUTE_BUNDLE=new-year APP_TITLE=\"\u65b0\u5e74\u6d3b\u52a8\" CUSTOM_BUILD_PATH=/hymc_act/new-year/ CUSTOM_ROUTE_PATH=/hymc_act/new-year/ vite build --mode production"
执行该命令时,ROUTE_BUNDLE、APP_TITLE、CUSTOM_BUILD_PATH 和 CUSTOM_ROUTE_PATH 会被注入构建环境。其中:
| 参数 | 作用位置 | 职责 |
|---|---|---|
ROUTE_BUNDLE |
Vite Alias / Route Bundle | 控制本次构建包含哪些活动路由和页面。 |
CUSTOM_ROUTE_PATH |
Vue Router | 控制活动在应用内的路由基路径,需与 NGINX 入口路径协同。 |
CUSTOM_BUILD_PATH |
Vite base |
控制 index.html 中 JavaScript、CSS 和图片等构建资源的公共 URL 前缀。 |
APP_TITLE |
index.html |
控制活动页面标题。 |
ROUTE_BUNDLE 的值需要与 bundles 目录下的文件名保持一致,否则 Vite 无法通过别名找到对应的路由入口。
CUSTOM_ROUTE_PATH 和 CUSTOM_BUILD_PATH 可以相同,也可以不同。例如,用户通过 /activity/christmas/ 访问活动,静态资源却可以放在独立目录或 CDN 下:
CUSTOM_ROUTE_PATH=/activity/christmas/
CUSTOM_BUILD_PATH=https://cdn.example.com/activity-assets/christmas/
此时,NGINX 负责为 /activity/christmas/ 返回活动的 index.html,而该 HTML 中的构建资源会从 CDN 前缀加载。
Vite 中对应的核心配置为:
export default defineConfig({
base: basePath,
build: {
assetsDir: 'assets',
outDir: resolve(__dirname, 'dist', routeBundleFile),
},
})
base 控制构建产物中资源的公共 URL 前缀,assetsDir 控制 JavaScript、CSS 和图片等文件在产物内的子目录,outDir 才控制构建产物实际写入的本地目录。仅配置 base 不会自动得到活动专属的 dist 目录;如果不设置独立 outDir,就需要由部署脚本将当前 dist 复制到对应的活动目录。
当每个活动已经部署在独立目录中时,无需再通过活动名称多划分一层资源目录,将 assetsDir 固定为 assets 即可。假设 base 为 /hymc_act/act-a/,构建后的图片通常会被重命名并从类似 /hymc_act/act-a/assets/bg.[hash].png 的地址加载。
六、NGINX 改造:让活动拥有独立发布空间
Vite 负责构建资源并生成资源 URL,Vue Router 负责浏览器内部的页面匹配,而当请求真正到达服务器时,是 NGINX 决定应该返回哪个文件。因此,前端构建配置并不能单独完成活动隔离,还必须配合独立的服务器目录和 NGINX 映射。
下面的配置以 /hymc_act/<活动标识>/ 为统一路径规则:
location ~ ^/hymc_act/([^/]+)(?:/.*)?$ {
try_files $uri /hymc_act/$1/index.html =404;
}
例如,用户访问 https://act.example.com/hymc_act/act-a/ 时,正则捕获组 $1 的值为 act-a。NGINX 会先尝试返回实际请求的静态文件;若文件不存在,则回退到 /hymc_act/act-a/index.html,再由 Vue Router 接管应用内路由。如果活动入口文件也不存在,服务器最终返回 404。
这个回退逻辑还能解决 History 模式下的刷新 404 问题。首次进入活动时,服务器返回 index.html,之后由 Vue Router 在浏览器内完成跳转。当用户直接刷新某个子路由时,浏览器会向服务器请求当前完整路径。服务器上并没有对应的物理文件,因此需要 try_files 回退到该活动的 index.html,让 Vue Router 再次完成页面匹配。
注意:具体的 root、alias 或上游静态服务配置应与实际服务器目录保持一致。如果项目的资源目录不在当前 NGINX 根目录下,仅添加上述 location 仍然无法正确读取文件。
七、一次活动构建的完整过程
下面以新建 happy-huotui 活动为例,串联完整构建流程。
- 新增活动目录:在
src/activities下创建happy-huotui的页面、组件和资源目录。
- 创建活动路由和 Bundle:新增活动路由文件,再创建
bundles/happy-huotui.ts并导出对应路由。
- 添加构建命令:传入与 Bundle、路由、资源前缀相匹配的参数。
"build:happy-huotui": "cross-env ROUTE_BUNDLE=happy-huotui APP_TITLE=\"\u5f00\u5fc3\u706b\u817f\" CUSTOM_BUILD_PATH=/hymc_act/happy-huotui/ CUSTOM_ROUTE_PATH=/hymc_act/happy-huotui/ vite build --mode production"
- 检查构建产物:确认产物中只包含当前活动与必要的公共代码,并检查
index.html中的资源 URL 前缀是否正确。 - 部署并配置 Location:将产物发布到对应的活动目录,确认 NGINX 能够返回正确的静态文件和 SPA 入口。


通过路由 Bundle 和 Vite 配置,单个活动构建时不再携带其他活动的页面。如果确实存在“构建所有活动”的场景,仍可以保留 all.ts 作为显式入口,但它不应成为单活动日常发布的默认方式。

八、改造前后的效果对比与验证
改造是否有效,不能只看配置是否跑通,还需要从构建内容、产物路径、部署影响范围和回滚能力等方面进行验证。

构建体积和耗时的改善程度与活动数量、共享依赖和代码分割策略有关,应以实际构建数据为准,不宜在没有测量的情况下给出固定改善比例。
九、方案边界与不足
当前方案实现了活动级构建和静态资源隔离,但仍有一些可以继续优化的地方。
首先,各活动独立构建后,Vue、Pinia 等公共依赖可能重复出现在不同活动的产物中。这会占用更多存储空间,但在每个活动的产物都自包含且不可变的前提下,也能避免共享依赖升级直接影响历史活动。这是资源复用与发布稳定性之间的取舍。
其次,新增活动需要同时维护 Route Bundle、Vite 映射、构建命令、部署目录和 NGINX 规则。当活动数量增加后,多处配置容易出现命名不一致或遗漏。后续可以使用统一的活动 Manifest,集中描述活动名称、路由路径、资源前缀、输出目录和路由入口,再自动生成相关配置。
此外,前端配置只能解决构建内容和资源路径问题。真正的活动隔离还需要部署目录、NGINX Location 和发布流程共同配合。只有路由、构建和部署使用一致的活动标识与路径规则,才能形成完整的隔离链路。
十、总结与后续演进
这次改造要解决的核心问题,是多个活动共享构建依赖、构建产物和静态资源目录所带来的发布耦合。
在路由层,通过 Route Bundle 和 Vite Alias,在构建时选择本次需要构建的活动,避免无关活动进入模块依赖图。在构建层,通过 base、assetsDir 和 outDir 为不同活动生成清晰的资源地址与独立产物目录。在部署层,通过 NGINX Location 将活动路径映射到各自的静态资源目录。
完整链路可以概括为:
Route Bundle 控制构建内容 → Vite 生成独立资源与产物 → NGINX 映射独立目录 → 活动实现独立发布和回滚。
改造后的主要收益不仅是减少单次构建的无关代码,更重要的是缩小了活动上线的影响范围。不同活动可以继续复用公共组件和工具,同时拥有相对独立的构建、资源和发布生命周期。
后续可以通过统一活动配置、自动生成构建命令和部署规则,并接入 CI/CD,进一步减少人工配置,让活动构建与发布流程更加标准化。
