从分散到统一:六个 Astro 站点合并为一个 Monorepo 的迁移实践

标签: AstroMonorepoCloudflare Pages
实践 2026 年 4 月 20 日 约 8 分钟
从分散到统一:六个 Astro 站点合并为一个 Monorepo 的迁移实践

背景

rikolab 最早由一个开源主页及若干独立的工具站点组成。起初每个站点采用独立 Git 仓库、独立依赖与独立部署流程。随着站点数量增加至 7 个,多仓库架构的维护瓶颈逐渐显现:每次修改共用的 SEO 规则、国际化字典或站点元数据,都需要在所有仓库中重复修改并逐个提交发布,重复配置与同步成本居高不下。

为了彻底解决这一问题,本次工程改造将分散的独立仓库整合为一个统一的 Monorepo(单体代码仓库):

  • 根目录统一管理代码,各子站点收纳至 apps/ 目录;
  • 提取公共逻辑与全局配置至 shared/ 目录;
  • 包管理统一收敛至 pnpm workspace;
  • 部署平台迁移至 Cloudflare Pages,配合 GitHub Actions 脚本根据每次提交的改动路径实现受影响站点的按需差异化构建。

本文将系统梳理本次架构迁移前后的对比、技术选型权衡、实施细节,以及迁移过程中遇到的典型构建问题与解决方案。

一、迁移前后的架构对比

1.1 仓库组织与依赖管理

维度迁移前 (Multi-repo)迁移后 (Monorepo)
仓库数量7 个独立的 Git 仓库1 个统一的 Monorepo
工作区结构顶层目录无版本控制,子目录各带 .git顶层根目录作为统一 Git 仓库
依赖管理每个站点各自安装,依赖版本分散容易漂移根目录 pnpm workspace 统一锁定与去重
公共配置跨站点复制粘贴代码统一抽取至 shared/ 集中维护

1.2 部署链路与发布体验

维度迁移前迁移后
部署托管多平台分散或手工脚本上传Cloudflare Pages 统一托管
触发机制本地手动构建或单独触发推送到 main 分支全自动触发
构建范围人工判断需要发布的站点CI 脚本根据改动路径自动计算受影响的 App
流水线结构各站点割裂的配置GitHub Actions 按差异动态生成 Job Matrix
域名解析各自离散配置统一从中央元数据派生子域名映射

1.3 整合后的目录结构

rikolab/
├── apps/
│   ├── main/            # 个人主页与产品聚合站
│   ├── lumino/          # 独立工具站 1
│   ├── wakeupscreen/    # 独立工具站 2
│   ├── macnewfile/      # 独立工具站 3
│   ├── vibewebhelper/   # 独立工具站 4
│   └── xpathtools/      # 独立工具站 5
├── shared/
│   ├── astro/           # 共享的 Astro 布局与组件
│   ├── i18n/            # 国际化语言检测与字典
│   ├── seo/             # 统一的 SEO Meta、Canonical 规则
│   └── site-metadata/   # 统一站点定义与域名映射元数据
├── scripts/
│   └── ci/              # 差异化构建与矩阵生成脚本
├── .github/
│   └── workflows/       # 统一部署流水线
├── pnpm-workspace.yaml
└── package.json

其中一个体量极小的独立法务页面直接并入主站的 public/ 静态目录,整体收敛为 1 个主站与 5 个独立工具站点,不再维护分散的项目孤岛。

二、技术选型与架构考量

2.1 为什么选择 Monorepo

将多个独立站点合并至单一仓库,核心目标是解决复用与原子化更新问题:

  1. 共享代码无需发包:原本跨站复用工具函数只有“复制代码”或“发布私有 npm 包”两种路径。在 Monorepo 下,shared/ 中的模块可通过相对路径或 workspace 别名直接引用,无版本发布周期与升级磨合成本。
  2. 原子化变更:当需要调整一个公共配置字段(例如全站更新统计 ID 或调整 canonical 链接格式)时,单个 Commit 即可覆盖所有受影响的站点,避免跨仓库修改时由于遗漏或时序不一致引发线上问题。
  3. 维护基线统一:Node 运行时、包管理器版本、构建流水线以及全局依赖(如 TypeScript、ESLint)均可在根目录统一锁定。

对于维护者集中且站点体量适中的个人与小型团队,Monorepo 带来的维护效率提升明显高于仓库体积增加带来的轻微开销。

2.2 Cloudflare Pages 的接入路径选择

Cloudflare Pages 对多站点的支持主要有两种实现路径:

  • 路径 A:基于 Pages 原生 Git 集成
    每个 Pages 项目直接绑定 GitHub 仓库,指定 Root Directory 对应各自的子目录。该方式虽然配置直观,但每个项目固定监听某一子目录,无法根据代码改动差异灵活组合构建,且受限于免费版单一账户下的 Git 绑定配额。
  • 路径 B:基于外部 CI 构建并由 Wrangler 推送产物(选用方案)
    将构建与分发逻辑完全移交至 GitHub Actions,编译完成后通过 Cloudflare 官方的 wrangler pages deploy CLI 直接将静态产物推送到对应的 Pages 项目。

选用路径 B 具备更高的工程灵活性:触发规则、路径差异分析以及并发构建矩阵完全由我们掌控,同时能够规避平台侧的集成约束。

三、实施核心:差异化构建与共享层设计

3.1 统一技术栈基线

层次选型作用与定位
运行时Node.js 24本地与 CI 统一的 JavaScript 运行时
包管理pnpm 10 (workspace)多项目依赖共享与精确版本锁定
站点框架Astro 5静态 HTML 内容生成与图片优化
自动化集成GitHub Actions路径检测、矩阵编译与发布触发
静态托管Cloudflare Pages + Wrangler边缘 CDN 分发与产物部署

3.2 基于改动路径的按需构建

在 Monorepo 中,如果每次提交代码都对所有子项目全量构建,CI 的运行时间将随着站点数量增加而线性膨胀。因此必须引入精细化的改动识别机制。

在流水线中通过自定义脚本 scripts/ci/changed-apps.mjs 读取本次 Commit 的文件差异列表(changed files),按如下规则判定构建范围:

改动文件路径构建行为
仅改动 apps/<name>/ 目录内部文件仅构建 该特定的子站点 <name>
改动 shared/、根目录 package.json 或 CI 流水线本身全量构建 所有 6 个子站点
仅改动 docs/、README.md 等非构建文件跳过构建,不触发任何部署任务

确定受影响的 App 列表后,通过 deploy-matrix.mjs 将其转换为 GitHub Actions 的动态 Job Matrix,各个站点的编译与部署任务即可实现并行执行。

3.3 共享配置层的抽取

将散落的配置提炼至 shared/ 目录,涵盖四个核心模块:

  • site-metadata:定义各站点的基础信息、独立域名与 CDN 缓存规则,作为各站构建与 CI 脚本的单一真实数据源(Single Source of Truth);
  • seo:统一 Open Graph、Twitter Card 及规范链接(Canonical URL)的元标签生成函数;
  • i18n:统一浏览器语言嗅探、本地存储与国际化字典加载逻辑;
  • astro:共享通用头部、尾部组件与公共 CSS 样式。

四、迁移过程中的典型问题与修复

在将 7 个历史工程合并入统一 Workspace 的过程中,遇到并解决了如下典型工程问题:

4.1 pnpm 版本声明冲突

  • 现象:GitHub Actions 报错 Multiple versions of pnpm specified。
  • 原因:根目录 package.json 中已通过 Corepack 声明了 packageManager: "pnpm@10.0.0",而在 GitHub Actions 的 pnpm/action-setup 步骤中又显式指定了 version: 10,导致版本解析冲突。
  • 修复:移除 Action 配置文件中的 version 参数,统一由 package.json 保持唯一的版本声明。

4.2 缺失 pnpm-lock.yaml 导致 CI 缓存中断

  • 现象:CI 提示 Dependencies lock file is not found 并中断执行。
  • 原因:Workflow 中配置了 cache: pnpm,但初次提交代码时根目录未同步提交由 Workspace 生成的 pnpm-lock.yaml。
  • 修复:在根目录下执行 pnpm install 生成统一的 lockfile 并提交至仓库。

4.3 @astrojs/sitemap 依赖版本不兼容

  • 现象:多个应用在执行 astro build 时崩溃,报错 Cannot read properties of undefined (reading 'reduce')。
  • 原因:@astrojs/sitemap@3.7.2 在当前配置组合下存在解析缺陷,所有启用 Sitemap 插件的站点均受到波及。
  • 修复:在各 App 中将 @astrojs/sitemap 版本锁定回稳定的 3.2.1。得益于 Monorepo 结构,一次性提交即可修复全站的构建异常。

4.4 pnpm 10 对原生扩展构建脚本的拦截

  • 现象:主站构建过程中,Astro 图片优化模块报错 Could not find Sharp。
  • 原因:pnpm 10 引入了更严格的安全策略,默认拦截非白名单依赖的原生构建脚本,导致 sharp 与 esbuild 的编译产物未正确链接。
  • 修复:在根目录 package.json 中配置 onlyBuiltDependencies 放行构建白名单:
    "pnpm": {
      "onlyBuiltDependencies": [
        "esbuild",
        "sharp"
      ]
    }

4.5 间接依赖解析不稳定

  • 现象:放行编译脚本后,主站依然无法稳定加载 Sharp 模块。
  • 原因:主站对 sharp 存在强依赖,但仅通过 Astro 间接引入。在 pnpm 的严格依赖隔离机制下,深层传递依赖的解析可能受环境影响。
  • 修复:在主站 apps/main/package.json 中显式声明 sharp 为直接依赖,消除解析不确定性。

4.6 共享函数更名导致的运行时残留

  • 现象:本地开发与构建无异常,但在浏览器打开页面时控制台报错 ReferenceError: normalizeBrowserLang is not defined。
  • 原因:重构国际化模块时,将底层方法收敛为 detectPreferredLang,但个别页面的客户端内嵌脚本中残留了对旧函数名的直接调用。
  • 修复:全局检索旧函数名,统一更新为新接口。在抽取共享模块时,应对代码库进行彻底的符号检索,规避此类未被构建期捕获的运行时隐患。

五、Cloudflare Pages 部署实践与配额规避

5.1 绕过项目绑定数量限制

Cloudflare Pages 在传统的 Git 自动集成模式下,免费账户对绑定的站点数量存在一定限制。针对 6 个独立站点的发布需求,我们采用了更优的 Token 部署方案:

  1. 在 Cloudflare 控制台分别为 6 个站点创建对应的 Pages 项目(如 rikolab-main、rikolab-lumino 等);
  2. 在 GitHub 仓库 Secrets 中注入 CLOUDFLARE_ACCOUNT_ID 与 CLOUDFLARE_API_TOKEN;
  3. CI 编译出产物后,通过 Wrangler 指令定向发布:
    npx wrangler pages deploy apps/<name>/dist --project-name rikolab-<name>

该方案将代码存储与部署托管彻底解耦,不仅解除了项目绑定的数量约束,也使得后续新增站点时只需简单声明元数据并由 CI 自动完成推送,无须侵入现有的部署流水线。

六、总结

将多个独立站点合并至单一 Monorepo 并统一接入 Cloudflare Pages,是对前端资产的一次系统性收敛。回顾本次迁移,其核心收益在于:

  • 维护成本显著降低:消除跨项目重复样板代码,共享逻辑一处修改、全局生效;
  • 发布链路全面可控:基于改动路径的按需构建机制,既保障了构建效率,又免除了人工介入判断发布范围的不确定性;
  • 基础设施高性价比:利用 Cloudflare Pages 与 GitHub Actions 的免费资源额度,搭建起高可用、全球加速且维护成本极低的现代化静态站矩阵。

对于维护若干独立工具站或内容产品的团队与个人而言,当项目数量达到一定规模时,及早将分散的孤岛仓库整合为结构清晰的 Monorepo,是一项值得投入且长期受益的基础工程改造。