从 CLI 到 HTML:追踪 `gatsby build` 的构建流水线
前置知识
- ›第 1 篇:架构与 Monorepo 概览
- ›webpack 基础概念(loaders、plugins)
- ›对构建流水线有基本了解
从 CLI 到 HTML:追踪 gatsby build 的构建流水线
在上一篇中我们已经熟悉了 monorepo 的整体结构。这一篇,我们来完整追踪 gatsby build 的执行过程——从命令处理函数被触发,到最终 HTML 文件落地 public/ 目录。这条路径横跨数十个文件,但理解它至关重要,因为 Gatsby 的每一个功能,最终都以流水线中某个阶段的形式呈现出来。
构建命令流水线
packages/gatsby/src/commands/build.ts 中的构建命令处理函数导出为一个单一的异步函数,整体结构是线性的——每个阶段完成后才进入下一个。
flowchart TD
A["bootstrap()"] --> B["onPreBuild hook"]
B --> C["writeOutRequires()"]
C --> D["buildProductionBundle()"]
D --> E["buildRenderer()"]
E --> F["preparePageTemplateConfigs()"]
F --> G["Build Rendering Engines"]
G --> H["calculateDirtyQueries()"]
H --> I["Run Queries"]
I --> J["buildHTMLPagesAndDeleteStaleArtifacts()"]
J --> K["onPostBuild hook"]
K --> L["adapterManager.adapt()"]
整条流水线可以分为三个宏观阶段:
- Bootstrap(第 127–131 行):初始化插件、拉取数据、构建 schema、创建页面、提取查询
- Bundle(第 153–295 行):四次 webpack 编译,分别产出 JS bundle 和 HTML 渲染器
- Render(第 302–511 行):执行查询、生成 HTML、通过 adapter 完成部署
下面逐一深入每个阶段。
初始化服务:配置、主题与插件
第 127 行的 bootstrap() 调用会委托给第 1 篇中介绍的 bootstrap 编排器,后者再调用 initialize()——这是整个代码库中最复杂的函数。它按顺序完成以下工作:
sequenceDiagram
participant Build as build.ts
participant Bootstrap as bootstrap/index.ts
participant Init as services/initialize.ts
participant Parcel as compileGatsbyFiles
participant Config as load-config
participant Themes as load-themes
participant Plugins as load-plugins
Build->>Bootstrap: bootstrap({ program })
Bootstrap->>Init: initialize(context)
Init->>Parcel: compileGatsbyFiles(siteDirectory)
Note over Parcel: Compile gatsby-config.ts, gatsby-node.ts
Init->>Config: loadConfig({ siteDirectory })
Config->>Themes: loadThemes(config)
Note over Themes: Recursive theme resolution
Themes-->>Config: Merged config
Config-->>Init: Validated config
Init->>Plugins: loadPlugins(config, siteDirectory)
Note over Plugins: Normalize → Validate → Flatten
Init->>Init: startPluginRunner()
Init-->>Bootstrap: { store, workerPool }
Parcel 编译
在读取配置之前,Gatsby 需要先编译 TypeScript 和 ESM 文件。在 initialize.ts 的第 170–173 行,compileGatsbyFiles 使用 Parcel 将 gatsby-config.ts 和 gatsby-node.ts 转换为 Node 可以直接 require 的 CommonJS 格式。这也是你能用 TypeScript 编写 Gatsby 配置的原因——Parcel 会在其他任何步骤执行之前完成转译工作。
配置加载
配置加载器(packages/gatsby/src/bootstrap/load-config/index.ts)完成三件事:
- 读取编译后的
gatsby-config文件 - 通过
handleFlags处理 feature flags - 将配置传入
loadThemes进行主题解析
根配置不能是函数形式——只有主题配置才可以是函数(接收主题选项作为参数)。这一差异在第 28–36 行有明确的校验逻辑。
主题解析
packages/gatsby/src/bootstrap/load-themes/index.ts 中的主题解析是递归进行的。主题本质上就是拥有自己 gatsby-config 的 Gatsby 插件,主题之间可以互相依赖,每个主题的配置都会与父级合并。解析算法以深度优先的方式遍历依赖树,收集所有主题配置,再自底向上进行合并。
插件加载
插件加载器(packages/gatsby/src/bootstrap/load-plugins/index.ts)接收合并后的配置,依次完成:
- 规范化:将字符串形式的插件引用转换为
{ resolve, options }对象 - 校验:对照 schema 验证插件选项
- 加载内置插件(随 Gatsby 一同打包的插件)
- 扁平化:将插件树展平为单一数组
- 整理:记录每个插件实现了哪些 API
- 验证导出:与已知 API 列表(
api-node-docs、api-browser-docs、api-ssr-docs)进行比对
最终结果是一个扁平数组,以 SET_SITE_FLATTENED_PLUGINS 的形式分发到 Redux(第 79–82 行)。这个数组是所有插件及其实现 API 的权威注册表。
API Runner:连接核心与插件的桥梁
每当 Gatsby 需要调用插件钩子时——无论是 sourceNodes、createPages,还是 onCreateWebpackConfig——都会经过 packages/gatsby/src/utils/api-runner-node.js 中的 API runner。
API runner 是 Gatsby 插件架构的中枢神经。每次 API 调用时,它会:
- 从扁平插件列表中查找实现了目标 API 的插件
- 为每个插件构建一个包含以下内容的上下文对象:
- 绑定好的 action creators(
createNode、createPage、createRedirect等) - 数据访问函数(
getNode、getNodes、getNodesByType) - 工具函数(
createNodeId、createContentDigest) - schema 类型构建器(
buildObjectType、buildUnionType等) - 作用域限定于该插件的
cache实例 - 用于结构化日志的
reporter
- 绑定好的 action creators(
- 顺序(而非并行)调用每个插件的实现
sequenceDiagram
participant Core as Gatsby Core
participant Runner as api-runner-node
participant PluginA as gatsby-source-filesystem
participant PluginB as gatsby-transformer-remark
participant Redux as Redux Store
Core->>Runner: apiRunnerNode("sourceNodes", { parentSpan })
Runner->>Runner: Look up plugins implementing "sourceNodes"
Runner->>Runner: Construct context (actions, getNode, cache...)
Runner->>PluginA: sourceNodes(context)
PluginA->>Redux: createNode(fileNode)
PluginA-->>Runner: done
Runner->>PluginB: sourceNodes(context)
PluginB-->>Runner: done
Runner-->>Core: results
传递给插件的 action creators 经过了"双重绑定":首先通过 bindActionCreators 绑定到 Redux,然后再经过一个包装函数绑定到具体的插件和 API 调用,注入 traceId、plugin.name、deferNodeMutation 等元数据(第 87–100 行)。这确保了插件分发的每一个 action 都携带可追溯的来源信息。
提示: 插件顺序执行是有意为之的设计——它保证了行为的确定性。插件 A 的
sourceNodes总会在插件 B 之前运行,当插件之间存在节点依赖时,这一点尤为重要。
四个 Webpack 阶段
Gatsby 使用四套独立的 webpack 配置,各有其用途。packages/gatsby/src/utils/webpack.config.js 中的工厂函数接收 stage 参数,并据此生成对应的配置:
flowchart TD
subgraph "Development"
A["develop"] -->|"Hot reload, CSS injection"| A1["Browser bundle"]
B["develop-html"] -->|"No HMR, SSR target"| B1["HTML renderer"]
end
subgraph "Production"
C["build-javascript"] -->|"Minified, chunked"| C1["Browser JS/CSS"]
D["build-html"] -->|"Node target, SSR"| D1["HTML renderer"]
end
style A fill:#e8f5e9
style B fill:#e8f5e9
style C fill:#e1f5fe
style D fill:#e1f5fe
| 阶段 | 目标环境 | 用途 |
|---|---|---|
develop |
Browser | 开发服务器,支持 React Fast Refresh 和 CSS 热更新 |
develop-html |
Node | 开发模式下的 SSR 渲染器(不含 HMR 插件) |
build-javascript |
Browser | 生产环境 JS 和 CSS bundle,支持代码分割 |
build-html |
Node | 用于静态生成的生产环境 HTML 渲染器 |
第 40–44 行的注释写得相当清楚:
// Four stages or modes:
// 1) develop: for `gatsby develop` command, hot reload and CSS injection into page
// 2) develop-html: same as develop without react-hmre in the babel config for html renderer
// 3) build-javascript: Build JS and CSS chunks for production
// 4) build-html: build all HTML files
插件可以通过 onCreateWebpackConfig 钩子修改任意阶段的配置,该钩子会接收 stage 参数,因此插件可以针对不同阶段做差异化处理。
在构建命令中,只会用到生产环境的两个阶段。第 160 行的 buildProductionBundle 运行 build-javascript 阶段,第 184 行的 buildRenderer 运行 build-html 阶段。
查询执行与 HTML 生成
webpack bundle 就绪后,Gatsby 进入查询执行阶段。第 302 行的 calculateDirtyQueries 负责判断哪些查询需要重新执行(在增量构建中,只有发生变化的查询才会运行)。
第 306–308 行有一处关键优化:
queryIds.pageQueryIds = queryIds.pageQueryIds.filter(
query => getPageMode(query) === `SSG`
)
构建时只会执行 SSG 页面的查询。DSG 页面将查询延迟到首次请求时执行,SSR 页面则在每次请求时都执行查询。这正是渲染模式系统的核心机制——我们将在第 5 篇中详细介绍。
对于多核机器,查询会在 worker pool 中并行执行(第 330 行)。查询执行完毕后,第 507 行的 buildHTMLPagesAndDeleteStaleArtifacts 负责生成最终的 HTML 文件。
缓存管理与构建持久化
在整个构建过程中,状态会通过 LMDB 持久化到 .cache/ 目录。packages/gatsby/src/redux/index.ts 中的 Redux store 明确定义了哪些状态切片需要持久化:
const persistedReduxKeys = [
`nodes`, `typeOwners`, `statefulSourcePlugins`, `status`,
`components`, `jobsV2`, `staticQueryComponents`,
`webpackCompilationHash`, `pageDataStats`, `pages`,
`staticQueriesByTemplate`, `pendingPageDataWrites`,
`queries`, `html`, `slices`, `slicesByTemplate`,
]
下次构建时,这些切片会在 store 创建阶段通过 readState() 从 LMDB 中读回。Gatsby 正是通过对比当前状态与持久化状态,来判断哪些查询"需要重新执行"、哪些 HTML 文件"已过期"。
flowchart LR
subgraph ".cache/ Directory"
A["data/datastore (LMDB)"] -->|Nodes| B["Redux State"]
C["caches-lmdb/"] -->|Plugin caches| D["Per-plugin data"]
E["redux.state (deprecated)"] -.->|Legacy| B
end
subgraph "Build"
F["Previous Build State"] --> G["Calculate Dirty Queries"]
G --> H["Only rebuild changed pages"]
end
B --> F
第 133–146 行的 saveState() 函数使用 writeToCache 将上述 key 对应的状态写入缓存。值得注意的是其中有一个 GATSBY_DISABLE_CACHE_PERSISTENCE 开关——这是为了解决超大型站点在 Node.js v8.serialize 序列化时遇到的 buffer 大小限制问题而添加的。
Adapter 集成
构建流水线的最后一步是通过 adapter 完成部署。第 645–648 行:
if (adapterManager) {
await adapterManager.storeCache()
await adapterManager.adapt()
}
adapter manager 在初始化阶段创建(initialize.ts 第 189–192 行),它从构建产物中收集 RoutesManifest 和 FunctionsManifest,再传递给对应平台的 adapter,由后者将其转换为部署平台的原生格式。我们将在第 5 篇中深入介绍 adapter 机制。
提示: 如果构建在中途失败,
.cache/目录可能残留不完整的状态。运行gatsby clean可以将其彻底清除,强制进行完整重建。遇到难以排查的缓存相关问题时,这是最有效的"重置"手段。
下一篇
至此,我们已经梳理了构建流水线作为线性传送带的完整运转过程。但 gatsby develop 面临的是一个更具挑战性的问题:它需要在实时响应变更的同时保持状态的一致性。下一篇,我们将深入探讨协调开发服务器的 XState 状态机——这是整个代码库中架构上最具特色的部分。