Gatsby 的架构:探索一个拥有 105 个包的 Monorepo
前置知识
- ›React 基础知识
- ›熟悉 npm/yarn 包管理
- ›了解 monorepo 的基本概念
Gatsby 的架构:探索一个拥有 105 个包的 Monorepo
Gatsby 是迄今为止最具雄心的开源 JavaScript 项目之一。在我们熟悉的 gatsby build 和 gatsby develop 命令背后,是一个包含约 105 个包的 monorepo,涵盖 source 插件、transformer 插件、GraphQL 数据层、基于 XState 的开发服务器,以及完整的部署适配器抽象。理解这些模块如何协同运作,是深度参与贡献或充分利用这个框架的关键所在。
本文是五篇系列文章的第一篇。我们将从俯瞰整个仓库结构开始,追踪一条 CLI 命令从全局安装到项目本地执行的完整路径,并介绍构建流水线与开发服务器之间的核心架构差异——这一差异贯穿后续所有内容。
Monorepo 结构:Lerna + Yarn Workspaces
Gatsby 使用 Lerna 配合 Yarn Workspaces 来管理各个包。根目录的配置十分简洁,但已足以说明整体结构:
{
"packages": ["packages/*"],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "independent"
}
其中 "version": "independent" 这一行至关重要——每个包独立进行版本管理和发布。这样一来,核心框架的变更就不会强制触发所有生态插件的版本升级。根目录 package.json 中配置的 Yarn Workspaces 则负责处理符号链接,让各包之间可以直接相互 require,无需先发布到 npm。
graph TD
subgraph "Core"
gatsby["gatsby"]
cli["gatsby-cli"]
coreutils["gatsby-core-utils"]
pluginutils["gatsby-plugin-utils"]
end
subgraph "Source Plugins"
fs["gatsby-source-filesystem"]
contentful["gatsby-source-contentful"]
drupal["gatsby-source-drupal"]
wordpress["gatsby-source-wordpress"]
end
subgraph "Transformer Plugins"
remark["gatsby-transformer-remark"]
sharp["gatsby-transformer-sharp"]
yaml["gatsby-transformer-yaml"]
end
subgraph "Feature Plugins"
image["gatsby-plugin-image"]
mdx["gatsby-plugin-mdx"]
feed["gatsby-plugin-feed"]
offline["gatsby-plugin-offline"]
end
subgraph "Adapters"
netlify["gatsby-adapter-netlify"]
end
cli --> gatsby
gatsby --> coreutils
gatsby --> pluginutils
fs --> coreutils
remark --> fs
这些包自然地划分为三个层级:
| 层级 | 示例 | 职责 |
|---|---|---|
| 核心层 | gatsby、gatsby-cli、gatsby-core-utils、gatsby-plugin-utils、gatsby-page-utils |
框架运行时、CLI、共享工具 |
| 生态插件层 | gatsby-source-filesystem、gatsby-transformer-remark、gatsby-plugin-image |
数据采集、转换与功能扩展 |
| 适配器与工具层 | gatsby-adapter-netlify、gatsby-graphiql-explorer、create-gatsby |
部署适配器、工具链、脚手架 |
提示: 想知道某个功能在哪个包里?命名规范就是你的向导。
gatsby-source-*负责数据采集,gatsby-transformer-*负责节点转换,gatsby-plugin-*负责添加构建或运行时功能,gatsby-adapter-*则处理各部署平台的特定逻辑。
CLI 委托模式
Gatsby 架构中最精妙的设计之一,是它的两阶段 CLI 解析机制。实际上存在两个 CLI:一个全局安装的 gatsby-cli 包,以及一个项目本地的 gatsby 包。当你执行 gatsby build 时,全局 CLI 并不会自己执行构建逻辑——它会将任务委托给项目本地安装的版本。
第一阶段:全局入口
全局入口位于 packages/gatsby-cli/src/index.ts,职责虽然简单,却至关重要:
- Node.js 版本校验(第 22–38 行):确保运行时满足最低版本要求
- 全局错误处理:注册
unhandledRejection和uncaughtException处理器 - CLI 初始化:在第 76 行调用
createCli(process.argv)
sequenceDiagram
participant User
participant GlobalCLI as gatsby-cli (global)
participant Yargs
participant LocalGatsby as gatsby (local)
User->>GlobalCLI: gatsby build
GlobalCLI->>GlobalCLI: Check Node.js version
GlobalCLI->>GlobalCLI: Set up error handlers
GlobalCLI->>Yargs: createCli(process.argv)
Yargs->>GlobalCLI: resolveLocalCommand("build")
GlobalCLI->>LocalGatsby: resolveCwd.silent("gatsby/dist/commands/build")
LocalGatsby-->>GlobalCLI: build command handler
GlobalCLI->>LocalGatsby: Execute build(args)
第二阶段:本地命令解析
核心逻辑在 packages/gatsby-cli/src/create-cli.ts 中实现。resolveLocalCommand 函数通过 resolveCwd.silent() 找到项目本地安装的 gatsby:
const cmdPath =
resolveCwd.silent(`gatsby/dist/commands/${command}`) ||
// Old location of commands
resolveCwd.silent(`gatsby/dist/utils/${command}`)
这一模式确保了全局安装的 gatsby-cli@5.x 能够与项目中 node_modules 里的 gatsby@4.x 正常协作。全局 CLI 只是一个轻量的路由器,实际的命令逻辑来自项目所依赖的 gatsby 版本。
项目本地的 gatsby 包也有自己的 bin 入口(packages/gatsby/cli.js)——仅三行代码,直接 require ./dist/bin/gatsby.js。这是通过 npx gatsby build 直接运行时的备用路径。
提示: 排查 CLI 问题时,务必确认
resolveLocalCommand实际解析到的是哪个gatsby版本。全局 CLI 的期望与本地包导出之间的版本不匹配,是许多令人困惑的报错的常见根源。
构建 vs 开发:两种架构
Gatsby 真正有趣的设计在这里展开。gatsby build 和 gatsby develop 不仅输出结果不同——它们采用的是截然不同的架构模式。
gatsby build:顺序执行的命令式流水线
packages/gatsby/src/commands/build.ts 中的构建命令是一个直观的 async 函数,按顺序依次执行各个步骤:
flowchart LR
A[bootstrap] --> B[writeOutRequires]
B --> C[Build JS Bundle]
C --> D[Build HTML Renderer]
D --> E[Run Queries]
E --> F[Generate HTML]
F --> G[onPostBuild]
G --> H[Adapter Deploy]
每个步骤完成后才开始下一步,没有并发,没有事件处理,也没有状态机。这是一条经典的构建流水线:加载配置 → 采集数据 → 构建 schema → 创建页面 → 打包 JS → 渲染 HTML → 部署。
gatsby develop:基于 XState 的响应式状态机
开发命令的架构则截然不同。它需要处理一个本质上具有响应式特征的问题:文件会变更、webhook 会触发、GraphQL mutation 会执行,开发服务器必须响应所有这些事件——有时是同时响应,有时是在另一次重新构建仍在进行时响应。
packages/gatsby/src/commands/develop.ts 会启动一个 ControllableScript 子进程,在该子进程(packages/gatsby/src/commands/develop-process.ts)内部,一个 XState 状态机被创建并开始运行:
const machine = developMachine.withContext({
program,
parentSpan,
app,
reporter,
pendingQueryRuns: new Set([`/`]),
shouldRunInitialTypegen: true,
})
const service = interpret(machine)
service.start()
这并非一个表面上的设计选择——而是架构层面的必然。我们将在第三篇文章中深入探讨这个状态机。
flowchart TD
subgraph "Parent Process (develop.ts)"
A[ControllableScript] -->|IPC| B[Child Process]
A -->|heartbeat| C[Crash Detection]
end
subgraph "Child Process (develop-process.ts)"
B --> D[XState developMachine]
D --> E[initializing]
E --> F[initializingData]
F --> G[runningQueries]
G --> H[startingDevServers]
H --> I[waiting]
I -->|file change| J[recompiling]
I -->|node mutation| K[recreatingPages]
J --> I
K --> G
end
Gatsby 的核心:packages/gatsby/src/ 目录结构
packages/gatsby/src/ 目录是框架核心逻辑的所在地。目录体量庞大,但内部结构遵循清晰的规律。
| 目录 | 职责 |
|---|---|
bootstrap/ |
初始化流水线:配置加载、插件加载、主题解析 |
commands/ |
CLI 命令处理器:build.ts、develop.ts、serve.ts、clean.ts |
services/ |
作为独立函数的各构建阶段:initialize、sourceNodes、buildSchema 等 |
state-machines/ |
用于 develop 的 XState 状态机:develop/、data-layer/、query-running/、waiting/ |
redux/ |
Redux store、reducer、action creator、类型定义与持久化 |
schema/ |
GraphQL schema 的构建、推断、扩展与 resolver |
query/ |
Query 的提取、编译、验证与执行 |
datastore/ |
基于 LMDB 的持久化节点存储 |
internal-plugins/ |
随 Gatsby 一同打包的内置插件,使用其自身的插件 API |
utils/ |
大量工具函数:webpack 配置、API runner、页面数据、适配器等 |
Bootstrap 编排器
Bootstrap 序列——构建与开发共用的初始化流水线——定义在 packages/gatsby/src/bootstrap/index.ts 中:
const context = {
...bootstrapContext,
...(await initialize(bootstrapContext)),
}
await customizeSchema(context)
await sourceNodes(context)
await buildSchema(context)
// ... createPages, extractQueries, etc.
从 ../services 导入的每个函数代表一个独立的构建阶段。这种服务函数模式使各阶段具备可复用性——同一个 sourceNodes 服务既用于构建流水线,也用于开发状态机。
flowchart TD
A[initialize] --> B[customizeSchema]
B --> C[sourceNodes]
C --> D[buildSchema]
D --> E[createPages]
E --> F[extractQueries]
F --> G[writeOutRedirects]
G --> H[postBootstrap]
style A fill:#e1f5fe
style C fill:#e8f5e9
style D fill:#e8f5e9
style E fill:#fff3e0
style F fill:#fff3e0
下一篇
在下一篇文章中,我们将从头到尾追踪一条 gatsby build 命令的完整执行路径——跟随数据流经 initialize 服务(基于 Parcel 的编译、配置加载、主题解析),穿过连接核心与插件的 API runner 桥接层,历经四个不同的 webpack 阶段,最终生成 HTML 文件。我们还会深入探讨 .cache/ 目录是如何支撑增量构建的。