Gatsby Recipes 源码解析与开发指南:基于 React 与 MDX 的「基础设施即代码」系统
2026/9/19 17:30:04 网站建设 项目流程
  • 前端
  • 静态站点
  • Web框架

【免费下载链接】gatsby

React-based framework with performance, scalability, and security built in.

项目地址:https://gitcode.com/gh_mirrors/ga/gatsby
点击查看免费下载

本文以仓库内 deprecated-packages/gatsby-recipes/CONTRIBUTING.md 为骨架,结合gatsby-recipes包源码,系统讲解 Gatsby Recipes 的动机、架构、开发环境搭建、状态机、解析器、渲染器以及 Provider/Resource 扩展机制。读完本文,你将理解 Recipes 如何在 Gatsby 中实现「用代码声明式地编排与供给技术栈」,并掌握运行单元测试、调试 CLI、编写与扩展资源(Resource)的完整方法。

Gatsby Recipes 是一个「基础设施即代码」(infrastructure as code)系统,让用户通过代码而非手工流程自动管理和供给 Gatsby 站点的技术栈。它由 React 和 MDX 驱动,一个贴切的类比是"React Native for Infrastructure":声明式之外,还能通过 JSX 提供程序化的逃生舱与条件逻辑。Recipes 同时为 Desktop/Admin 提供一套读写 API,用于在 Gatsby 及其集成服务之上构建低代码工具。

需要先说明一个重要的现状前提:根据 deprecated-packages/gatsby-recipes/README.md,Gatsby Recipes 已随gatsbyv4.5.0 从主包中移除,本仓库中该目录被标记为 deprecated(废弃归档)。如需继续使用,可安装gatsby-cli@4.4.0。本文基于当前仓库中的源码与文档展开,所有命令与配置均以该归档版本为准。

Recipes 是什么

Recipes 的核心思想是:把「安装插件、写配置文件、创建文件、运行脚本」这类一次性、易出错的手工操作,沉淀为可复用的、可版本化的 MDX 文档。一个 Recipe 就是一段包含指令组件(如<File><NPMPackage><GatsbyPlugin>)的 MDX 文件,采用Literate Programming(文学化编程)模型——文字说明与可执行指令交织在同一文档中。

设计目标

原文档明确列出的目标包括:

  • 让使用 Gatsby 的最初 10 分钟体验「充满魔法」
  • 为 Admin 与 Desktop 提供读写 API 层
  • 简化 Gatsby 插件的安装与配置
  • 取代 starters
  • 解决密钥(secrets)管理问题
  • 大幅简化复杂 Gatsby「技术栈」的供给与演进

其中「取代 starters」与「解决密钥管理」是两个值得留意的野心:Recipes 希望把 starter 从「复制一份代码」升级为「按需声明式生成」,并通过输入机制(Inputs)在运行期注入密钥等敏感值。

一个示例 Recipe

原文档用「创建一个 hello world Markdown 文件」作为贯穿全文的示例:

# Create a file --- Creates a "hello, world" file <File name="hello.md" content="# Hello, world!" /> --- That's it!

这个例子会在后续章节反复出现:解析器如何把---分成步骤(step)、渲染器如何把<File>组件变成文件系统的读写操作、plan 模式如何返回差异而不落盘。

搭建开发环境

在开始修改 Recipes 源码之前,需要先搭好本地 Gatsby 开发环境。核心的日常开发命令集中在单测、providers 调试、gatsby-dev-cli与 Gatsby Admin 四条路径上。

运行单元测试

Recipes 的单元测试用 Jest 组织,直接对packages/gatsby-recipes运行:

yarn jest packages/gatsby-recipes

该命令会执行src/下所有*.test.js测试文件。仓库中的测试覆盖了从解析器(src/parser/index.test.js、src/parser/validate.test.js)、状态机(src/recipe-machine/index.test.js)、渲染器(src/renderer/render.test.js)到各 Provider 资源的完整链路。

测试与扩展 Providers/Resources

如果你要修复某个资源的 bug 或扩展它,通常是直接对着该资源的测试文件工作。例如针对GatsbyPlugin,可以启动一个针对providers的 Jest watch 进程:

yarn jest packages/gatsby-recipes --testPathPattern "providers" --watch

--testPathPattern "providers"会把范围收窄到 src/providers 目录下的测试(如 src/providers/fs/file.test.js、src/providers/gatsby/plugin.test.js、src/providers/npm/package.test.js),--watch则在文件变更时自动重跑。原文档特别提示:大部分后续开发预计会发生在 providers/resources 层,因为内部框架稳定后,Admin 乃至第三方供给服务的新需求都会以新增资源的形式落地。

使用 gatsby-dev-cli

你可以创建测试 Recipe 并在测试站点中运行。这需要借助gatsby-dev-cli将本地改动的包复制到测试站点。有一个关键注意点:由于你测试的是 Gatsby CLI 本身的改动,不要运行全局的gatsby命令,而要运行被gatsby-dev-cli复制过来的本地版本

./node_modules/.bin/gatsby

调试 CLI 时,常常会遇到没有堆栈的错误。可以借助 Node 调试器绕过:

DEBUG=true node --inspect-brk ./node_modules/.bin/gatsby recipes ./test.mdx

然后打开 Chrome DevTools,点击 Node 图标进入调试会话。DEBUG=true会开启debug模块的日志输出(Recipes 内部大量使用debugCtor命名空间,例如状态机中的recipes-machine)。

若要看 Recipes GraphQL 服务器的日志输出,需要把 API 与 CLI 分开跑:先在一个终端启动 Recipes API:

node node_modules/gatsby-recipes/dist/graphql-server/server.js

再在另一个终端设置环境变量RECIPES_DEV_MODE=true后运行你的 recipe。这也是理解「客户端/服务端分离」架构的最直观方式——CLI 与 Admin 本质上都是这套 GraphQL API 的客户端。

用 Gatsby Admin 运行

确保所有包已构建:

yarn bootstrap

然后直接启动 Gatsby Admin:

yarn workspace gatsby-admin run develop

Admin 是 Recipes 的主要 GUI 载体(详见下文 Roadmap),也是探索 Recipes 读写 API 的入口。

调试开发环境

一个常见坑:之前开发时启动的 GraphQL 服务器可能处于挂死的「僵尸(zombie)」状态。如果发现 API 的改动没有生效,用ps aux检查是否有残留进程在挂起,把它清理掉再重启。

架构总览

Recipes 中存在一对核心概念:client(客户端)backend(后端)。后端负责运行一个 Recipe(无论是 plan 还是 apply 模式),两者通过 GraphQL API 通信。整个数据流大致是:

  1. 客户端把 Recipe 的源码通过 GraphQL API 发送给服务端;
  2. 服务端运行后返回一份plan(计划)
  3. 客户端拿到 plan,渲染出摘要;
  4. 用户选择 "apply plan" 后,服务端在当前环境上真正执行安装。

所有客户端(CLI、GUI、Admin)都使用urql与 API 通信,用于请求数据、发送 mutation,或订阅运行过程中的更新。

这套 API 本身也可以脱离 Recipes 独立运行和使用——Gatsby Admin 就是这么做的。

GraphQL API

如果通过gatsby-dev-cli在项目中开发,可以直接连上 GraphiQL 探索 API 端点。API 默认运行在:

http://localhost:50400/graphql

除非服务器是被gatsby develop自动调起的(端口可能不同)。该端口定义可在 src/graphql-server/server.js 中找到:const PORT = process.argv[2] || 50400,即也可以通过命令行参数覆盖端口。GraphiQL 自带自描述文档,原文档不再赘述 schema 细节。

从源码看,服务端基于express+express-graphql构建,内部用PubSub(来自graphql-subscriptions)与SubscriptionServersubscriptions-transport-ws)支持 WebSocket 订阅,并用interpret(来自xstate)驱动 recipe-machine 状态机(src/graphql-server/server.js)。

Subscriptions

GraphQL API 暴露一个operation订阅。当用户选中一个 Recipe 时,会创建一个operation订阅并调用状态机。服务端在状态迁移时发送更新(部分状态被忽略),客户端基于当前状态渲染新的 UI。当用户决定 apply 时,发送一个事件;若用户添加了输入(Inputs),还会把INPUT_ADDED事件发回服务端。

收到输入后,服务端会重新运行 Recipe 渲染,并把更新后的 plan 发回客户端。因此 renderer 可以理解为客户端与服务端之间的「运行时循环」,事件在其中来回传递。

值得补充的细节来自状态机源码(src/recipe-machine/index.js):presentPlan状态内部通过onReceive监听外部事件,INPUT_ADDEDsend转发给presentingPlan这个 invoke 回调,回调把输入写入context.inputs后重新执行createPlan并发出onUpdatePlan更新 plan。

生成类型

每个 Resource 定义自己的 schema,Recipes 用它来为 API 生成 GraphQL 类型。这里用的是一个内部 fork 的joi-to-graphql。因为该库返回的结构与「code-first 的 GraphQL 定义」所需形状不同,需要再做一些整理。这部分代码位于 src/joi-to-graphql。

原文档在此坦诚记录了一处技术债:Recipes 运行在较老的Joi版本上,且无法直接升级,因为内部 fork 的joi-to-graphql大量使用 Joi 的内部 API。fork 本身是为了支持资源中需要的更多 Joi 类型,而该库已不再维护,未来可能值得重写。另外,每个资源的schema还会混入公共的resourceSchema(见 src/providers/resource-schema.js)。

状态机

Recipe 状态机是 Recipes 特有的,负责逻辑流控制。当 Recipe 被发送到后端时,GraphQL 服务器初始化一个状态机;状态变化时,事件随 GraphQL 订阅发回客户端。

状态机的完整实现位于 src/recipe-machine/index.js,使用xstateMachine构建,初始状态为resolvingRecipe

States(状态)

状态对应章节说明
resolveRecipeRecipe resolution解析 Recipe 来源(文件/官方/URL)
parseRecipeParser用 MDX 解析器解析源码
validateSteps校验解析出的 Recipe 步骤是否有效
createPlanRenderer渲染生成 plan
presentPlan把 plan 呈现给用户
applyPlanRender in apply mode应用 plan
done结束

(源码中还包含doneError终态,以及validateSteps失败、creatingPlan收到INVALID_PROPS等错误路径。)

Events(事件)

  • CONTINUE:应用 plan
  • INPUT_ADDED:更新 context 中的输入,重新运行presentPlan
  • TICK:跟踪长时间运行的 plan 并更新客户端
  • RESET:重置TICK计数

源码细节:applyingPlan状态中,RESET先把elapsed归零,随后每 10 秒发送一次TICKsetInterval(..., 10000)),elapsed累加 10000,用于向客户端报告长任务的进度。

Actions(动作)

  • addResourcesToContext:当 plan 应用过程中发生更新时,更新这个全局对象并下发给客户端,让客户端可以同步更新。

源码中该动作会把事件数据里的资源按_uuid(或resourceDefinitions._key)与 plan 中已有资源匹配,把_messageisDone状态合并回 plan。

Recipe resolution(Recipe 解析)

Recipe 可以来自几个地方,状态机的第一步resolveRecipe负责处理这些来源。对应实现见 src/resolve-recipe.js:

  • 文件系统gatsby recipes ./my-local-file.mdx
  • 官方 recipesgatsby recipes theme-ui
  • URLgatsby recipes https://gist.github.com/123abc

源码中的判定逻辑为:

  • .开头的路径被当作相对路径,path.join(projectRoot, pathOrUrl)拼到项目根目录;
  • http(s)开头的(isUrl判定)直接fetch其文本;
  • 其余名字按「官方 recipes」处理,从https://unpkg.com/gatsby-recipes/recipes/${name}.mdx拉取(若名字不以.mdx结尾会自动补上)。

官方 Recipe 的 MDX 源文件就在本仓库 deprecated-packages/gatsby-recipes/recipes 目录下,共 28 个,覆盖emotion.mdxtheme-ui.mdxjest.mdxtypescript.mdxgatsby-image.mdxstyled-components.mdxtailwindcss.mdxcypress.mdx等场景。

Parser(解析器)

解析器接收 Recipe 的 MDX 源码,用MDX v2 解析器进行解析,然后按所有thematicBreak节点(---)对 Recipe 分区。每个分区成为一个step(步骤),其中第一个 step 作为 Recipe 的引言(introduction)。

核心实现见 src/parser/index.js,基于unified管线(remark-parse+remark-stringify+remark-mdx+remark-mdxjs):

  • partitionSteps:遍历 AST 子节点,遇到thematicBreak就递增步骤序号,其余节点按序号归入对应步骤;
  • pluckExports:把所有export节点从正文中抽出(exports 对整个文档是全局的),并暴露给每个 step 渲染时使用;
  • applyUuid:为每个 MDX 块级元素(mdxBlockElement,排除RecipeIntroductionRecipeStep两个内部组件)注入_uuid_type属性,用于跟踪其状态。

每个组件被赋予uuid以便跟踪状态,每个 step 被包裹进Step组件以提供 step 上下文(组件在渲染过程中可以访问它)。exports 允许用户实例化变量、甚至条件组件。文档中的 exports 示例:

# Create a file --- Creates a "hello, world" file export const fileName = "hello.md" <File name={fileName} content="# Hello, world!" /> --- That's it!

输入 MDX

# Create a file --- Creates a "hello, world" file <File name="hello.md" content="# Hello, world!" /> --- That's it!

输出 MDX

解析后,Intro 被包成RecipeIntroduction,每个后续步骤被包成带step/totalSteps属性的RecipeStep,组件被注入_uuid

<RecipeIntroduction># Create a file</RecipeIntroduction> <RecipeStep step={1} total={2}> Creates a "hello, world" file <File name="hello.md" content="# Hello, world!" _uuid="123abc" /> </RecipeStep> <RecipeStep step={2} total={2}> That's it! </RecipeStep>

Parser 返回对象

解析器返回同一个 Recipe 源码的多种变体,以便客户端以不同方式展示(对照源码 src/parser/index.js):

  • exports:导出节点数组,渲染每个 step 时暴露给它们
  • stepsAsMdx:用于逐步展示的 MDX
  • stepsAsJS:转换后的 JS,目标是让客户端无需再跑 Babel(WIP)
  • ast:未使用,可能可移除
  • recipe:完整文档(由exportsAsMdxstepsAsMdx拼接而成)

(源码实际还返回steps(节点数组)与exportsAsMdx,其中stepsAsJS通过 src/transform-recipe-mdx.js 转换。)

Renderer(渲染器)

当创建或应用 Recipe plan 时,会使用一个自定义 React reconciler进行渲染。这是一个完整的 React 运行时,渲染结果是一个 JavaScript 对象,再被转换为 plan 发回客户端。

为此,原始 Recipe MDX 源码要经过几步变换:

  1. 解析(parsed):见 Parser
  2. 用 Babel 变换(transformed):以便内联求值
  3. new Function求值(evaluated):注入必要的 scope

求值部分见 src/renderer/index.js:scope 中注入ReactRecipeStepRecipeIntroductionInputuseInput/useInputByKeyuseResourceuseProvider、全部resourceComponents以及mdx/MDXContent;代码尾部补上return React.createElement(MDXContent)后通过new Function(...scopeKeys, code)构造组件函数。

Recipes 源码可以包含嵌套资源,且所有资源默认是异步的。为此所有资源都用React Suspense渲染,随后返回一个 emitter,在资源被 diff 或 apply 时发出事件。

Reconciler

自定义 reconciler 的实现见 src/renderer/reconciler.js,基于react-reconciler的自定义 host config(supportsMutation: true)。它不渲染 DOM,而是把 React 元素树翻译为纯 JSON 结构:createInstance产生{ type, props }createTextInstance产生{ text }appendChildToContainerkey/_uuid去重后挂到容器children

输入:

<File path="red.js" content="red!"> <File path="blue.js" content="blue!" /> </File>

输出:

const result = [ { resourceName: "File", resourceDefinitions: { content: "red!", path: "red.js", }, currentState: "", describe: "Write red.js", diff: "OMITTED", newState: "red!", resourceChildren: [ { resourceName: "File", resourceDefinitions: { content: "blue!", path: "blue.js", }, currentState: "", describe: "Write blue.js", diff: "OMITTED", newState: "blue!", }, ], }, ]

可以看到:嵌套资源会形成resourceChildren递归结构,每个节点都携带资源名、定义、当前状态、描述、差异与新状态。

Recipe components

资源与 Suspense 的配合是代码库中最「奇特」的部分。当渲染File这样的资源时,它会抛出一个自定义 promise——该 promise 包装资源,并把 props/context 直接转发给providers/fs/fileplancreate调用。

实现方式是:把从 providers 生成的每个资源都包进 Suspense。resourceComponents的生成代码见 src/renderer/resource-components.js:

const Resource = props => ( <Suspense fallback={<p>Reading File...</p>}> <ResourceComponent _resourceName="File" {...props} /> </Suspense> )

ResourceComponent(位于 src/renderer/render.js,原文档略去了细节)大致如下:

const ResourceComponent = ({ _resourceName: Resource, _uuid, _type, children, ...props }) => { /* ... */ const resourceData = handleResource( Resource, { ...parentResourceContext, root: process.cwd(), _uuid, mode, resultCache, inFlightCache, blockedResources, queue, }, props ) return ( <ParentResourceProvider data={ { /* ... */ } } > <Resource> {JSON.stringify({ ...resourceData, _props: props, _stepMetadata: step, _uuid, _type, })} {children} </Resource> </ParentResourceProvider> ) }

它被包裹在 context 中,向下传递父级 context 与可能影响默认 props 的 inputs。handleResource根据当前模式(plan 还是 apply)发起异步调用、处理缓存,最终返回一个包含资源调用结果的 JS 对象——这也是「context」进入资源的地方:渲染时先建立 context 再在 React 侧向下传递。目前 context 硬编码了项目根目录,并传入 step、父级资源信息等数据。

handleResource的 promise 解析后,结果被序列化为 JSON 字符串,由自定义 reconciler 作为文本注入到 "Recipes VDOM"(一个 JSON 对象)中。

Server renderer(服务端渲染器)

服务端渲染器与客户端渲染器不同:它使用自定义 reconciler,且在所有资源渲染完成后进入空闲状态,直到收到输入或模式切换。

Client renderer(客户端渲染器)

主要有两个客户端——CLI 与 GUI,它们与 GraphQL API 通信,API 返回当前 plan 与转换后的源码 MDX。它们直接渲染到 DOM,使输入(input)支持能实时呈现在页面上——这也是客户端必须把输入变更发回服务端的原因:输入会被加入服务端渲染器的 context,收到事件后一次性冲刷成一份 plan。它们依赖前述 GraphQL API,并按场景渲染 UI。

原文档坦承这部分的代码是「最凌乱、最难解开」的区域:多为叠加了几层的原型代码,且主要是需要拆解的 React;CLI 有时难以开发,因为错误会丢失。另外需要确保客户端不需要变换 JS——这应由后端/解析器处理,让 Babel 只存在于服务端。

CLI

CLI 使用ink把 React 代码渲染到终端。它和 Terraform 一样有 plan 与 apply 两种模式,通过--install切换。入口见 src/cli/index.js。

GUI

GUI 代码当时正在迁移进 Gatsby Admin,与 CLI 类似但运行在浏览器中,将支持完善的输入功能并可以使用gatsby-interface

Render in apply mode(apply 模式渲染)

后端渲染器默认处于plan 模式:返回当前状态与目标状态的 diff,而不是真正更新环境。

当渲染器被告知 apply 时,它对资源调用create而非update,真正更新环境并把状态发回客户端。

Providers 与 Resources

Providers 和 Resources 是 Recipes 的「面包与黄油」(bread and butter)。原文档预期:当 Recipes 内部框架稳定后,大部分开发都会发生在这里——无论是 Admin 这类消费项目需要的新功能,还是第三方供给服务,都会以新增资源的形式添加。

Providers 与 Resources 如何工作

Provider(提供者)是包含资源的服务,比如 Gatsby、Contentful、文件系统或 GitHub。Resource(资源)可以是本地文件、Gatsby 插件,乃至 CMS 上的内容模型。

资源导出一组方法供 Gatsby Recipes 内部使用,且必须实现 CRUD

  • create:接收 context 和参数,从零创建资源。成功创建后,用新id调用read返回。
  • read:接收 context 和唯一标识,获取资源。
  • update:接收 context、id与参数。更新资源,成功后用给定id调用read返回。
  • destroy:接收 context 与id。先对现有状态调用read,然后移除,最后返回之前读到的对象。
  • all:可选方法,返回全部资源的索引。

除 CRUD 外,资源还必须实现schemavalidateplan

  • schema:一个 Joi 对象,规定资源属性的形状。
  • validate:校验函数,在任何 CRUD 函数调用前接收潜在属性,确保其合法。
  • plan:返回资源当前状态与期望状态之间的 diff。

每次资源调用都会把自己的 diff 加入 plan。plan 就是 Recipe 中所有资源的 diff 组合。资源还有一个definition,指传给资源的 props 或参数。

当 plan 被调用时,会创建或更新 plan 中指定的所有资源——无论是写文件、更新配置,还是云上供给。

Resource API 签名

原文档以文件资源为例(编辑后精简)展示每个方法的 API 签名。plan 有固定形状;CRUD 中的动作(createupdatedestroy)在动作完成后返回资源的read结果。destroy特殊处理:返回的是销毁之前read值,以便需要时利用其先前状态。如前所述,资源可选实现all,它会在 GraphQL API 中产生一个allResourceName字段。

const create = async (context, { id, ...otherData }) => { /* ... */ return await read(context, otherData.Path) } const update = async (context, resource) => { /* ... */ return await read(context, resource.id) } const read = async (context, id) => { /* ... */ return resource } const destroy = async context => { /* ... */ return fileResourceBeforeDestroy } const all = () => { /* ... */ return allTheResources } const plan = async (context, resource) => { /* ... */ return { currentState, newState, describe, diff, } } const schema = { path: Joi.string(), content: Joi.string(), ...resourceSchema, } const validate = resource => { return Joi.validate(resource, schema, { abortEarly: false }) } module.exports.plan = plan module.exports.schema = schema module.exports.validate = validate module.exports.create = create module.exports.update = update module.exports.read = read module.exports.destroy = destroy module.exports.all = all

对照真实源码 src/providers/fs/file.js,可以看到完整实现:createmkdirp建目录,若content是 URL 则downloadFile抓取写入,否则fs.writeFile直接写入;plan对二进制路径(isBinaryPath)特殊处理为Binary file,仅当currentState !== newState时才计算 diff(src/providers/utils/get-diff.js);read在文件不存在时返回undefined。这种「plan 只报告变化」的语义正是 Recipes 非破坏性的保证。

其他资源的实现同样可以对照源码研读:Gatsby 侧包括 src/providers/gatsby/plugin.js(通过 Babel 插件改写gatsby-config.js中的plugins数组,支持从对象/条件表达式节点中提取插件名、options 与__key)、src/providers/gatsby/site-metadata.js、src/providers/gatsby/page.js、src/providers/gatsby/shadow-file.js;npm 侧有 src/providers/npm/package.js、src/providers/npm/package-json.js、src/providers/npm/script.js;另有 git(src/providers/git/ignore.js)与 Contentful(src/providers/contentful)等 provider。

Resource 列表

各资源对应的 Recipes 组件(<GatsbyPlugin><GatsbyShadowFile><NPMPackage><NPMPackageJson><NPMScript><File><Directory>等)的 props 说明,见 recipes README。以File为例:path是相对 Node.js 项目根(package.json所在处)的路径,content支持直接内容或 URL(链接 gist 时需点 "Raw" 拿原始 URL);GatsbyPluginname/options/key/isLocal分别对应插件名、写入gatsby-config.js的配置对象、多实例区分键与本地插件标记;NPMPackageversion默认latestdependencyType默认production

Roadmap(规划中的功能)

以下功能是原文档写作时计划的下一步实现,其中相当一部分与后续 Recipes 的演进路线直接相关。

Gatsby Admin GUI

Recipes 曾包含一个 WIP GUI,当时正计划移植到 Gatsby Admin 中常驻。这(可能)将是大多数人探索、阅读与运行 Recipes 的地方。当前仓库的 deprecated-packages/gatsby-admin 即该管理界面的归档位置。

优化客户端 bundle

Admin 内 Recipes GUI 的一部分工作涉及优化客户端 bundle:把服务端预编译好的 JS 一并下发,这样客户端就不需要再包含 MDX 与 Babel 变换——这与上文「Babel 只存在于服务端」的架构意图一致。

Inputs(输入)

输入能力当时已部分发布,但需要完成实现,尤其是useInputAPI:

  • useInput:更开发者友好的 API
  • CLI 支持:把输入作为 CLI 参数传入

从源码看,useInput/useInputByKey已存在于 src/renderer/input-provider.js,Input组件在 src/renderer/input.js,状态机也已支持INPUT_ADDED事件——但面向用户的完整输入流(尤其是 CLI 传参)当时尚未收尾。

Statefile(状态文件)

Statefile 将让 Recipes 知道哪些 Recipe 已运行过、某个资源是否需要更新或创建。当时的问题在于:由于无法确定性地判断资源是否由某个 Recipe 创建,Recipes总是对资源调用create。Statefile 需要记录 recipe 历史与 recipe id,以便跨运行跟踪,使「无变化的 Recipe」能以幂等方式运行——这与 Terraform 的 state 文件思路一脉相承。

相关设计

考虑的替代方案

  • 代码脚手架(Code scaffolding):考察过简单的代码生成器,但希望获得更强的能力,例如多步骤 Recipe、在客户端与浏览器中都能运行 Recipe。
  • Themes(主题):最初希望主题覆盖这些想法(一个主题可以指定站点某部分所需的一切,并可组合主题),但后来意识到:人们在创建更复杂主题时想做的大部分事情,都涉及大量复杂的、一次性的「setup」代码,而这些代码在标准 Gatsby 生命周期中没有自然的位置——需要某种游离于正常 Gatsby 生命周期之外的东西。

灵感来源

  • Terraform:plan/apply 双模式、声明式基础设施即代码直接借鉴了它。
  • AWS CDK:用通用编程语言表达基础设施的构造方式。

延伸资料

原文档汇总了多位作者关于 Recipes 的写作,包括公开 Recipes RFC、Gatsby Recipes README、providers 与 resources README、WIP 工程设计文档与 Recipes GitHub 项目。这些资料的正文内容大多已沉淀进本仓库的 deprecated-packages/gatsby-recipes/README.md 与本文。

术语表

Recipes 有一些专用术语,定义如下以尽量消除歧义。

Apply

一种从 Terraform 借鉴而来的模式:把 plan 应用到当前环境。由服务端在客户端告诉它 "apply" 时处理。

Plan

一组指令,包括资源名、资源定义以及它将对环境当前状态施加的 diff。例如,<NPMPackage name="gatsby" />会产生一个 plan,其中资源名为 "NPMPackage",资源定义为 "package name gatsby",diff 是一个 ANSI 编码的 git diff,显示版本变化(如果有的话)。

这是 apply 的非破坏性版本:比较当前状态与目标状态并报告 "diff"。这也是为什么 Recipes 的 plan 环节可以在不触碰文件系统的前提下先行预览全部变更。

Providers

Providers 指一个服务,无论它是开发环境本地的东西(如文件系统),还是第三方远程服务(如 GitHub)。Providers 包含一组 resources。

Recipe

一个 MDX 文件,通过组件以Literate Programming(文学化编程)模型包含指令。

Resources

Resources 属于某个 provider。文件系统资源是文件和目录;Gatsby 资源包括页面、插件甚至站点元数据。


小结:Gatsby Recipes 把「配置与供给技术栈」这件事抽象为三层——MDX 编写的 Recipe(声明层)、xstate 驱动的状态机与自定义 React reconciler(执行层)、Provider/Resource 的 CRUD 契约(扩展层)。对本仓库而言,虽然该包已随 v4.5.0 归档到deprecated-packages,但它所体现的 plan/apply 语义、Suspense 异步资源渲染、GraphQL 订阅式运行时循环等设计,仍是理解 Gatsby 生态演进与「React Native for Infrastructure」思路的重要参考。若想动手实践,可先跑yarn jest packages/gatsby-recipes,再按本文开发环境章节的指引编写自己的 Recipe 或资源。

  • 前端
  • 静态站点
  • Web框架

【免费下载链接】gatsby

React-based framework with performance, scalability, and security built in.

项目地址:https://gitcode.com/gh_mirrors/ga/gatsby
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询