- 前端
- 静态站点
- Web框架
【免费下载链接】gatsby
React-based framework with performance, scalability, and security built in.
本文以仓库内 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 developAdmin 是 Recipes 的主要 GUI 载体(详见下文 Roadmap),也是探索 Recipes 读写 API 的入口。
调试开发环境
一个常见坑:之前开发时启动的 GraphQL 服务器可能处于挂死的「僵尸(zombie)」状态。如果发现 API 的改动没有生效,用ps aux检查是否有残留进程在挂起,把它清理掉再重启。
架构总览
Recipes 中存在一对核心概念:client(客户端)与backend(后端)。后端负责运行一个 Recipe(无论是 plan 还是 apply 模式),两者通过 GraphQL API 通信。整个数据流大致是:
- 客户端把 Recipe 的源码通过 GraphQL API 发送给服务端;
- 服务端运行后返回一份plan(计划);
- 客户端拿到 plan,渲染出摘要;
- 用户选择 "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)与SubscriptionServer(subscriptions-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_ADDED被send转发给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,使用xstate的Machine构建,初始状态为resolvingRecipe。
States(状态)
| 状态 | 对应章节 | 说明 |
|---|---|---|
resolveRecipe | Recipe resolution | 解析 Recipe 来源(文件/官方/URL) |
parseRecipe | Parser | 用 MDX 解析器解析源码 |
validateSteps | — | 校验解析出的 Recipe 步骤是否有效 |
createPlan | Renderer | 渲染生成 plan |
presentPlan | — | 把 plan 呈现给用户 |
applyPlan | Render in apply mode | 应用 plan |
done | — | 结束 |
(源码中还包含doneError终态,以及validateSteps失败、creatingPlan收到INVALID_PROPS等错误路径。)
Events(事件)
CONTINUE:应用 planINPUT_ADDED:更新 context 中的输入,重新运行presentPlanTICK:跟踪长时间运行的 plan 并更新客户端RESET:重置TICK计数
源码细节:applyingPlan状态中,RESET先把elapsed归零,随后每 10 秒发送一次TICK(setInterval(..., 10000)),elapsed累加 10000,用于向客户端报告长任务的进度。
Actions(动作)
addResourcesToContext:当 plan 应用过程中发生更新时,更新这个全局对象并下发给客户端,让客户端可以同步更新。
源码中该动作会把事件数据里的资源按_uuid(或resourceDefinitions._key)与 plan 中已有资源匹配,把_message与isDone状态合并回 plan。
Recipe resolution(Recipe 解析)
Recipe 可以来自几个地方,状态机的第一步resolveRecipe负责处理这些来源。对应实现见 src/resolve-recipe.js:
- 文件系统:
gatsby recipes ./my-local-file.mdx - 官方 recipes:
gatsby recipes theme-ui - URL:
gatsby 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.mdx、theme-ui.mdx、jest.mdx、typescript.mdx、gatsby-image.mdx、styled-components.mdx、tailwindcss.mdx、cypress.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,排除RecipeIntroduction、RecipeStep两个内部组件)注入_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:用于逐步展示的 MDXstepsAsJS:转换后的 JS,目标是让客户端无需再跑 Babel(WIP)ast:未使用,可能可移除recipe:完整文档(由exportsAsMdx与stepsAsMdx拼接而成)
(源码实际还返回steps(节点数组)与exportsAsMdx,其中stepsAsJS通过 src/transform-recipe-mdx.js 转换。)
Renderer(渲染器)
当创建或应用 Recipe plan 时,会使用一个自定义 React reconciler进行渲染。这是一个完整的 React 运行时,渲染结果是一个 JavaScript 对象,再被转换为 plan 发回客户端。
为此,原始 Recipe MDX 源码要经过几步变换:
- 解析(parsed):见 Parser
- 用 Babel 变换(transformed):以便内联求值
- 用
new Function求值(evaluated):注入必要的 scope
求值部分见 src/renderer/index.js:scope 中注入React、RecipeStep、RecipeIntroduction、Input、useInput/useInputByKey、useResource、useProvider、全部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 },appendChildToContainer按key/_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/file的plan或create调用。
实现方式是:把从 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 外,资源还必须实现schema、validate、plan:
schema:一个 Joi 对象,规定资源属性的形状。validate:校验函数,在任何 CRUD 函数调用前接收潜在属性,确保其合法。plan:返回资源当前状态与期望状态之间的 diff。
每次资源调用都会把自己的 diff 加入 plan。plan 就是 Recipe 中所有资源的 diff 组合。资源还有一个definition,指传给资源的 props 或参数。
当 plan 被调用时,会创建或更新 plan 中指定的所有资源——无论是写文件、更新配置,还是云上供给。
Resource API 签名
原文档以文件资源为例(编辑后精简)展示每个方法的 API 签名。plan 有固定形状;CRUD 中的动作(create、update、destroy)在动作完成后返回资源的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,可以看到完整实现:create用mkdirp建目录,若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);GatsbyPlugin的name/options/key/isLocal分别对应插件名、写入gatsby-config.js的配置对象、多实例区分键与本地插件标记;NPMPackage的version默认latest、dependencyType默认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.
相关推荐
如何在ESP32-S3上部署esp32-ai:从下载到运行的完整指南
如何在ESP32 S3上部署esp32 ai:从下载到运行的完整指南 esp32 ai是一款专为ESP32 S3开发板设计的AI模型部署工具,能够帮助开发者快速
大模型人工智能嵌入式本地部署模型量化预训练在 Gatsby 中理解与实践基础设施即代码(Infrastructure as Code)
在 Gatsby 中理解与实践基础设施即代码(Infrastructure as Code) 导读 本文基于 基础设施即代码(Infrastructure as
前端静态站点Web框架Chalice与CDK基础设施即代码
Chalice与CDK基础设施即代码 本文深入探讨了Chalice框架与AWS CDK的集成架构设计,实现了基础设施即代码(IaC)与无服务器应用开发的完美结合
后端ServerlessCLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考