从节点到查询:Redux、LMDB 与 GraphQL Schema 构建
前置知识
- ›本系列第 1–3 篇文章
- ›Redux 基础知识(store、actions、reducers)
- ›GraphQL schema 基本概念(类型、resolver、directive)
- ›对内存映射数据库有基本了解
从节点到查询:Redux、LMDB 与 GraphQL Schema 构建
流经 Gatsby 站点的每一份内容——每个 Markdown 文件、每条 CMS 记录、每张图片——都要经过数据层的处理,才能从原始数据变成带类型的、可查询的 GraphQL schema。这个数据层是 Gatsby 架构的核心,由三根支柱支撑:用于全局状态管理的 Redux、用于持久化节点存储的 LMDB,以及用于 schema 构建的 graphql-compose。
在第 2、3 篇中我们看到,构建管道和开发状态机调用的是同一批服务函数——sourceNodes、buildSchema、createPages。本文将深入这些服务内部,解释数据如何进入系统、如何存储、schema 如何从数据中生成,以及查询如何被提取并执行。
Redux:整个系统的中枢神经
packages/gatsby/src/redux/index.ts 中的 Redux store 是整个构建过程的唯一数据来源,负责追踪一切:页面、节点、组件、查询、webpack 编译哈希、HTML 文件状态等。
IGatsbyState 的数据结构
IGatsbyState 接口定义了完整的状态结构,以下是其核心字段概览:
graph TD
subgraph "IGatsbyState"
nodes["nodes: Map<string, IGatsbyNode>"]
pages["pages: Map<string, IGatsbyPage>"]
components["components: Map<string, IGatsbyPageComponent>"]
schema["schema: GraphQLSchema"]
queries["queries: { trackedQueries, trackedComponents, ... }"]
html["html: { trackedHtmlFiles, compilationHashes, ... }"]
flattenedPlugins["flattenedPlugins: Array<FlattenedPlugin>"]
config["config: IGatsbyConfig"]
status["status: { PLUGINS_HASH, LAST_NODE_COUNTER }"]
jobsV2["jobsV2: { incomplete, complete, jobsByRequest }"]
end
| 状态切片 | 类型 | 用途 |
|---|---|---|
nodes |
Map<string, IGatsbyNode> |
系统中的所有内容节点 |
pages |
Map<string, IGatsbyPage> |
已注册的页面,包含路径和组件信息 |
components |
Map<string, IGatsbyPageComponent> |
页面模板,附带查询与渲染元数据 |
schema |
GraphQLSchema |
编译完成的 GraphQL schema |
queries |
复合对象 | 查询追踪:脏标记、依赖图 |
html |
复合对象 | HTML 文件状态、编译哈希 |
flattenedPlugins |
Array | 插件注册表的权威来源 |
三级 Action 权限体系
Gatsby 的 Redux action 在 packages/gatsby/src/redux/actions/public.js 中按访问权限分为三级:
- 公开 action(
actions/public.js):所有插件均可调用——createNode、createPage、createRedirect、deleteNode - 受限 action(
actions/restricted.ts):仅特定 API 可用——createTypes、addThirdPartySchema、setWebpackConfig - 内部 action(
actions/internal.ts):框架专用——SET_PROGRAM、SET_SITE_CONFIG、SET_SCHEMA
这一权限机制由 API runner 强制执行——它只会为当前 API hook 绑定对应的 action creator。一个实现了 sourceNodes 的插件可以拿到 createNode,但无法访问 setWebpackConfig。
Store 配置与持久化
Store 配置了两层中间件(第 101–115 行):处理异步 action 的 redux-thunk,以及一个自定义的 multi 中间件,用于将 action 数组拆分后逐个 dispatch。
在第 117–119 行,初始状态按条件加载:
export const store: GatsbyReduxStore = configureStore(
process.env.GATSBY_WORKER_POOL_WORKER ? ({} as IGatsbyState) : readState()
)
Worker 进程使用空 state(从主进程接收部分状态),主进程则从 LMDB 缓存中读取状态,从而支持增量构建。
mett:连接 Redux 与插件系统的事件桥梁
有一个不起眼但至关重要的模块将 Redux 与插件系统连接在一起。mett 是一个轻量级事件发射器(灵感来自 mitt),它用 Map<string, Set<Handler>> 替代了普通对象和数组。
在 redux/index.ts 第 172–175 行,每个 Redux action 都会通过 mett 广播出去:
store.subscribe(() => {
const lastAction = store.getState().lastAction
emitter.emit(lastAction.type, lastAction)
})
这构成了一个发布/订阅桥梁:系统的任何部分都可以监听特定的 Redux action。插件 runner(packages/gatsby/src/redux/plugin-runner.ts)正是借助这一机制自动触发插件钩子:
sequenceDiagram
participant Plugin as Source Plugin
participant Redux as Redux Store
participant Mett as mett emitter
participant Runner as plugin-runner.ts
participant OnCreate as onCreateNode plugins
Plugin->>Redux: createNode(fileNode)
Redux->>Redux: Reduce CREATE_NODE
Redux->>Mett: emit("CREATE_NODE", action)
Mett->>Runner: CREATE_NODE handler
Runner->>Runner: Check: is node.internal.type === "SitePage"?
Runner->>OnCreate: apiRunnerNode("onCreateNode", { node })
第 44–77 行的 startPluginRunner 函数会在启动时预先过滤插件——只有当至少一个插件实现了 onCreatePage 或 onCreateNode 时,才会注册对应的事件监听器,避免了无人监听时徒劳触发事件的开销。
提示: mett 还支持通过
*事件名注册通配符监听器。开发状态机的 mutation 监听器正是通过这种方式,捕获所有类型的节点变更事件,无论具体是哪种 action。
节点存储:从 Redux 到 LMDB
Gatsby 最初将所有节点存储在 Redux 的内存状态中。对于大型站点(节点数超过 10 万),这会消耗数 GB 的内存。解决方案是 LMDB——一种基于内存映射的 B 树数据库,提供接近内存读取速度的性能,同时具备磁盘持久化能力。
入口是 packages/gatsby/src/datastore/datastore.ts 中的懒加载模式:
let dataStore: IDataStore
export function getDataStore(): IDataStore {
if (!dataStore) {
const { setupLmdbStore } = require(`./lmdb/lmdb-datastore`)
dataStore = setupLmdbStore()
}
return dataStore
}
packages/gatsby/src/datastore/lmdb/lmdb-datastore.ts 中的 LMDB 实现使用 globalThis.__GATSBY_OPEN_ROOT_LMDBS 在不同的 require 上下文之间共享数据库句柄:
function getRootDb(): RootDatabase {
if (!rootDb) {
if (!globalThis.__GATSBY_OPEN_ROOT_LMDBS) {
globalThis.__GATSBY_OPEN_ROOT_LMDBS = new Map()
}
rootDb = globalThis.__GATSBY_OPEN_ROOT_LMDBS.get(fullDbPath)
if (rootDb) return rootDb
rootDb = open({
name: `root`,
path: fullDbPath,
compression: true,
})
globalThis.__GATSBY_OPEN_ROOT_LMDBS.set(fullDbPath, rootDb)
}
return rootDb
}
这种 globalThis 缓存机制避免了"多个 LMDB 实例"问题——当同一数据库在同一进程中被打开两次时(例如在 gatsby serve 中,引擎和 trailing-slash 中间件都需要访问节点),就会触发随机错误。
flowchart TD
A["createNode() action"] --> B["Redux Reducer"]
B --> C["LMDB updateNodes"]
C --> D[".cache/data/datastore"]
E["getNode(id)"] --> F["LMDB getNode"]
F --> D
G["getNodesByType(type)"] --> H["LMDB iterateNodesByType"]
H --> D
style D fill:#fff3e0
数据库路径在生产环境默认为 .cache/data/datastore,在测试环境则为 .cache/data/test-datastore-{workerId},确保 Jest worker 之间的测试隔离(第 32–44 行)。
GraphQL Schema:显式定义与自动推断的融合
Gatsby 的 GraphQL schema 分两个阶段构建:自定义阶段(插件提供的显式类型定义)和推断阶段(从节点数据自动生成类型)。编排逻辑位于 packages/gatsby/src/schema/index.js。
第一阶段:自定义
在 customizeSchema 服务执行期间,插件通过调用 createTypes() 定义显式 GraphQL 类型。这些类型定义存储在 store.getState().schemaCustomization.types 中。框架内置类型最先添加,其次是插件类型,最后是用户类型——确保用户定义具有最高优先级(schema/index.js 第 26–34 行):
return [
...builtInTypes,
...types.filter(type => type.plugin && type.plugin.name !== `default-site-plugin`),
...types.filter(type => !type.plugin || type.plugin.name === `default-site-plugin`),
]
第二阶段:推断
显式类型注册完成后,buildInferenceMetadata(第 53–80 行)会遍历所有节点类型,分析其数据,并 dispatch BUILD_TYPE_METADATA action 以驱动推断引擎。packages/gatsby/src/schema/schema.js 中的 schema 构建器随后使用 graphql-compose 将显式定义与推断类型合并。
Schema 扩展
packages/gatsby/src/schema/extensions/index.js 中的扩展系统提供了类型级和字段级的 directive:
| 扩展 | 作用层级 | 用途 |
|---|---|---|
@infer |
类型 | 启用自动字段推断(默认行为) |
@dontInfer |
类型 | 禁用推断,仅保留显式字段 |
@link |
字段 | 建立与其他节点的外键关联 |
@dateformat |
字段 | 为日期字段添加格式化参数 |
@fileByRelativePath |
字段 | 将相对文件路径解析为 File 节点 |
@mimeTypes |
类型 | 声明该类型处理的 MIME 类型 |
@childOf |
类型 | 声明父子节点关系 |
flowchart TD
A["Plugins call createTypes()"] --> B["Explicit type definitions"]
C["Node data in LMDB"] --> D["Inference engine"]
B --> E["graphql-compose SchemaComposer"]
D --> E
F["Schema extensions<br/>@infer @link @dateformat"] --> E
E --> G["Final GraphQLSchema"]
G --> H["Store: SET_SCHEMA"]
提示: 如果你在开发 source plugin 并希望完全掌控类型的 schema 结构,建议在类型定义上使用
@dontInfer。这样可以阻止 Gatsby 分析节点数据,避免自动添加你不需要的字段——一旦数据结构发生变化,这些多余字段可能会引发问题。
查询管道
schema 构建完成后,还需要从组件文件中提取查询,再经过编译、校验,最终执行。这条管道涉及 query/ 目录下的多个文件。
提取
packages/gatsby/src/query/query-compiler.js 中的查询编译器使用基于 Babel 的 FileParser 来查找组件文件中的 GraphQL 标签模板字面量。它会扫描项目目录和主题目录下的所有文件:
const parsedQueries = await parseQueries({
base: program.directory,
additional: resolveThemes(
flattenedPlugins.map(plugin => ({
themeDir: plugin.pluginFilepath,
}))
),
addError,
parentSpan: activity.span,
})
编译器使用标准的 GraphQL 校验规则(从 graphql 包导入,见第 14–29 行)对提取到的查询进行校验,然后将 fragment 合并到对应查询中——在 Gatsby 中,fragment 具有全局作用域,一个文件中定义的 fragment 可以在任意查询中使用。
执行
GraphQLRunner 类对标准 graphql 的 execute 函数进行了封装,添加了缓存和追踪能力。在实例化时,它会创建一个 LocalNodeModel——所有 Gatsby 字段 resolver 都会接收这个 resolver context:
this.nodeModel = new LocalNodeModel({
schema,
schemaComposer: schemaCustomization.composer,
createPageDependency,
_rootNodeMap,
_trackedRootNodes,
})
NodeModel 正是让 Gatsby resolver 变得"智能"的关键——它通过 createPageDependency 追踪每个查询依赖的节点,从而在源数据变更时自动使相关查询失效。
三种查询类型
Gatsby 处理三种不同类型的查询:
- Page query:定义在页面组件中,可接收
pageContext变量,每个页面执行一次。 - Static query(
useStaticQuery):可定义在任意位置,不接受变量,结果会内嵌到 JS bundle 中。 - Slice query:定义在 slice 组件中(Gatsby 5 新特性),每个 slice 执行一次,结果在页面间共享。
flowchart LR
A["Component files"] -->|"Babel parse"| B["FileParser"]
B --> C["Raw queries + fragments"]
C -->|"Fragment collocation"| D["Complete queries"]
D -->|"Validate against schema"| E["Valid queries"]
E -->|"calculateDirtyQueries"| F["Dirty query IDs"]
F --> G["GraphQLRunner.execute()"]
G -->|"Page queries"| H["page-data JSON files"]
G -->|"Static queries"| I["static-query JSON files"]
G -->|"Slice queries"| J["slice-data JSON files"]
calculateDirtyQueries 步骤是实现增量构建的关键——它将查询哈希和节点依赖与上次构建结果进行比对,判断哪些查询确实需要重新执行。对于一个拥有 10,000 个页面、但只有 3 个节点发生变化的站点,这一机制可以将查询执行时间从数分钟压缩到数秒。
下一步
至此,我们已经追踪了数据从原始内容到节点创建、LMDB 存储、schema 推断、再到查询执行的完整旅程。在最后一篇文章中,我们将深入 Gatsby 的可扩展性机制——将一切串联起来的插件系统、支持组件覆盖的主题系统、SSG/DSG/SSR 页面模式体系,以及部署适配器抽象层。