使用 @tinacms/graphql 将文件与文件夹构建为可查询的 GraphQL 内容数据库
2026/9/15 1:20:04 网站建设 项目流程

使用 @tinacms/graphql 将文件与文件夹构建为可查询的 GraphQL 内容数据库

【免费下载链接】tinacmsTinaCMS is the leading open-source headless CMS that supports Markdown and Visual Editing. Your content is stored in your own GitHub repo 🦙 ❤️项目地址: https://gitcode.com/GitHub_Trending/ti/tinacms

TinaCMS 是面向 Markdown、MDX、JSON、YAML 等文本格式的开源 headless CMS,而本篇文章聚焦的@tinacms/graphql正是其核心引擎:它把仓库里的一堆文件夹和文件"编译"成一个可以用 GraphQL 查询的数据库,并提供文档间引用、schema 预生成与索引加速等能力。读完本文,你将掌握该包的完整使用流程——从安装、定义 schema、建立数据库索引到执行 GraphQL 查询,并深入理解其 Bridge、Level 存储、索引与解析器的源码实现。

包定位:文件系统到 GraphQL 数据库的桥梁

@tinacms/graphql位于 packages/@tinacms/graphql,它解决的问题非常明确:让静态内容(Markdown、MDX、JSON、YAML 等)具备数据库般可查询的能力。其核心思路是两阶段流水线:

  1. 建库阶段(index):把磁盘上的内容文件读取、解析,连同生成的 GraphQL Schema 一起写入一个 LevelDB 兼容的索引存储中;
  2. 查询阶段(resolve):接收 GraphQL 查询字符串,借助预生成的 schema 与索引数据完成字段解析、引用关联与列表过滤。

包入口 src/index.ts 导出了这套流水线的全部关键 API:DatabasecreateDatabasecreateLocalDatabaseFilesystemBridgebuildSchemaresolve,以及getChangedFilesgetShashaExists等 Git 辅助函数。

三大核心特性

  • 用 GraphQL 查询多种内容格式:Markdown、MDX、JSON、YAML 等文件都被统一建模为"文档"(Document),并以相同的方式暴露给查询层;
  • 文档间引用(references):一个文档可以引用另一个文档,查询时能按需展开被引用文档的字段;
  • 预生成 schema 与查询数据:在构建期生成_schema.json_graphql.json_lookup.json等产物,加速网站编译期查询,避免运行时再全量解析。

安装方式

最简单的接入方式是通过官方脚手架创建完整的 TinaCMS 站点,在项目根目录执行:

npx create-tina-app@latest

如果只想在现有项目中单独使用本包,则按如下步骤安装:

pnpm install pnpm add @tinacms/graphql

从仓库内的 package.json 可以看到,包当前版本为2.4.11,它依赖graphql@15.8.0作为查询执行引擎,并通过@tinacms/schema-tools(同仓库工作区包)提供 schema 类型定义与校验能力。

构建你的第一个查询程序

README 提供了一个完整的端到端示例,下面逐段展开并补充说明。

1. 引入依赖

import { MemoryLevel } from 'memory-level'; import { Database, FilesystemBridge, buildSchema, resolve } from '@tinacms/graphql'; import { Schema } from '@tinacms/schema-tools';
  • FilesystemBridge:负责从文件系统读写内容的桥接器(对应源码 src/database/bridge/filesystem.ts);
  • MemoryLevel:来自memory-level的内存版 LevelDB 实现,作为索引数据的存储层;
  • buildSchema:根据 Tina 配置生成 schema 产物(_schema.json_graphql.json_lookup.json);
  • resolve:执行 GraphQL 查询并返回结果(对应源码 src/resolve.ts)。

2. 准备数据源与索引存储

const dir = 'content'; // Where to source content from const bridge = new FilesystemBridge(dir); // Where to store the index data const indexStorage = new MemoryLevel<string, Record<string, string>>();

这里bridge告诉数据库"内容从哪来",indexStorage告诉数据库"索引数据放哪"。生产环境中索引存储通常换成持久化的 LevelDB(如classic-level)或由 Tina 提供的远程数据层。

3. 定义 schema 结构

// Create the schema/structure of the database const rawSchema: Schema = { collections: [ { name: 'post', path: '', // Don't require content to be placed within a subdirectory fields: [ { type: 'string', name: 'title', isTitle: true, required: true } ] } ] }; const schema = await buildSchema({ schema: rawSchema, build: { publicFolder: '', outputFolder: '' } });
  • collections是 Tina schema 的核心:每个 collection 定义一类内容文件(如post);
  • path指定内容存放的子目录,设为空字符串表示允许内容直接放在content根目录下,不需要额外子目录;
  • isTitle: true标记该字段作为文档标题,用于_sys.title等系统字段;
  • buildSchema内部会调用createSchema(源码 src/schema/createSchema.ts)对配置做validateSchema校验,并注入包版本号(major/minor/patch)到TinaSchema中。

4. 创建数据库对象

// Create the object for editing and querying your repository const database = new Database({ bridge, level: indexStorage, tinaDirectory: 'tina' });

Database构造参数(DatabaseArgs,见 src/database/index.ts)包括:

参数类型说明
bridgeBridge内容 I/O 桥接器,可从文件系统、GitHub 等数据源读写
levelLevel索引存储,需实现 abstract-level 的 Level 接口
tinaDirectorystring生成产物目录名,默认'tina'(兼容旧版.tina
onPut/onDelete回调内容写入/删除时的钩子,常用于同步 Git
indexStatusCallback回调索引进行中/完成/失败的状态通知
versionboolean是否启用多版本索引空间
namespacestring命名空间,多租户隔离用
levelBatchSizenumber批量写入的批次大小,默认 25

5. 生成索引数据

// Generate the index data required to support querying await database.indexContent(schema)

这一步是整条流水线的核心。indexContent(src/database/index.ts)会:

  1. 从桥接器读取预生成的_lookup.json
  2. _graphql.json_schema.json_lookup.json写入索引存储;
  3. 调用_indexAllContent扫描全部内容文件,为每个文档生成默认排序键__filepath__、字段索引与引用索引;
  4. 若启用了version,还会把新版本写入_metadata子库,实现无锁的版本切换(旧版本在回调中清理)。

索引的底层写入逻辑在 src/database/datalayer.ts:文档会为每个可索引字段生成形如字段值\u001D文件路径的排序键,数字字段还会做零填充(默认 4 位整数 + 3 位小数),datetime 字段统一转为 UTC ISO 字符串,从而保证 LevelDB 的字典序即字段的语义序,查询时可直接用范围扫描完成过滤与排序。

6. 执行 GraphQL 查询

// Query the database and output the result // In this case, it will retrieve the title of the post 'in.md' const graphQLQuery = ` query { document(collection: "post", relativePath: "in.md") { ...on Document { _values, _sys { title } } } } ` const result = await resolve({ database, query: graphQLQuery, variables: {} }); // Output the result console.log(JSON.stringify(result))

resolve的执行路径(src/resolve.ts)清晰可循:

  1. 从数据库读取预生成的 GraphQL AST,用buildASTSchema构建 schema;
  2. 读取 Tina schema 并创建 resolver;
  3. 调用graphql()执行查询,其中fieldResolver依据_lookup.json中的resolveType分发到不同的解析策略,例如document字段走multiCollectionDocument分支,把relativePath交给resolver.getDocument完成取数;
  4. 收集所有字段解析 Promise 并Promise.allSettled,防止解析器"失控"提前返回。

7. 准备内容文件并验证输出

为了程序能跑通,还需要:

  1. 安装依赖包:@tinacms/schema-toolsmemory-level
  2. content目录下添加文件content/in.md
--- title: Hello ---

预期的输出结果为:

{"data":{"document":{"_values":{"_collection":"post","_template":"post","title":"Hello"},"_sys":{"title":"Hello"}}}}

可以看到_values返回了文档的原始字段值(附带_collection_template系统字段),而_sys.titleisTitle: true的字段派生而来。

深入:Bridge 与 Level 两层抽象

@tinacms/graphql之所以能同时支持本地开发与云端部署,关键在于其双抽象设计。

Bridge(内容访问层)定义于 src/database/bridge/index.ts,FilesystemBridge是其文件系统实现:get/put/delete/glob分别对应读、写、删与通配扫描。值得一提的细节是它对路径安全做了纵深防御(filesystem.ts):所有公开方法都会通过assertWithinBase校验路径是否逃逸出基准目录(CWE-22 路径穿越防护),同时解析符号链接(CWE-59),并对tina/__generated__/生成目录做二次约束。仓库中还提供了AuditFileSystemBridge,它只允许写入_lookup.json_schema.json_graphql.json三个生成产物,其余内容一律丢弃,适合审计/预检场景。从源码注释可以推断,GitHub 等远程桥接器也遵循同一Bridge接口。

Level(索引存储层)定义于 src/database/level.ts,它要求传入实现 abstract-level 接口的对象,MemoryLevel只是其中一个实现。索引数据按子库(sublevel)组织:~前缀存放内容本体,collection 名 + 排序键存放各类索引。LevelProxyget做了容错——当键不存在抛出LEVEL_NOT_FOUND时返回undefined而非抛错,这让"查不到"成为正常流程而非异常。

从最小示例走向生产:createDatabase 与真实查询形态

最小示例用new Database(...)直连,而生产代码通常使用createDatabase(src/database/index.ts),它要求同时提供databaseAdapter(索引存储)与gitProvider(写入回调),并接受namespace实现多租户数据隔离;若仍传入旧的level/onPut/onDelete,会打印弃用警告并走兼容分支。此外createLocalDatabase则封装了TinaLevelClient+FilesystemBridge的组合,便于本地开发。

仓库src/spec/movies目录下的 GraphQL 查询示例可以帮你理解真实查询形态。例如 getMovieDocument/_query.movies.gql 演示了:

  • 通过movie(relativePath: "star-wars.mdx")按相对路径取单个文档;
  • _sys系统字段携带filenamebasenamebreadcrumbspathrelativePathextensiontemplatecollection等元信息;
  • director引用字段通过... on Director { name }内联片段展开被引用文档的字段,这正是"文档间引用"特性的查询侧体现(引用关系在索引期由makeRefOpsForDocument__refs__伪索引写入存储)。

连接(Connection)形态的查询遵循 Relay 风格:edges { node { ... } }分页结构配合first/last/after/before游标参数,底层由Database.query(src/database/index.ts)基于索引键范围扫描实现,游标即索引键的 base64 编码。过滤条件(eqgtgteltltestartsWithin)会先被转换为索引扫描的上下界(makeFilterSuffixes),无法用索引表达的条件再由makeFilter基于jsonpath-plus逐条二次过滤,兼顾性能与灵活性。

源码级验证:测试与规格文件

仓库用一套"请求-响应"规格文件来验证查询行为,位于 src/spec,例如:

  • movies/requests/getMovieDocument/:单个文档查询(含引用展开);
  • movies/requests/getMovieList/:连接查询与错误响应,其_response.json展示了字段拼写错误时返回的标准 GraphQL 错误结构("Cannot query field ... Did you mean ...");
  • movies-with-datalayer/:数据层(datalayer)模式下文档的增改与列表查询;
  • forestry-sample/:面向 Forestry 迁移场景的请求/变更规格。

对应测试入口 requests.test.ts 把这些.gql查询与期望响应逐一比对,是理解本包行为最直接的"可执行文档"。包内单元测试还包括database.test.tsdatalayer.test.tsalias-utils.test.tsfilter-utils.test.ts等(src/database 与 src/resolver),覆盖索引、别名、过滤、媒体富文本等细节。

小结

@tinacms/graphql把"一堆 Markdown/MDX/JSON/YAML 文件"变成"可查询的 GraphQL 数据库",其价值链条清晰:FilesystemBridge负责内容 I/O,buildSchema/createSchema负责把 Tina 配置编译成 GraphQL schema 与查询查找表,indexContent建立索引,resolve在预生成 schema 之上执行查询并解析引用。无论是通过create-tina-app起步,还是将本包嵌入自研内容管线,理解 Bridge/Level 双抽象与"索引期构建、查询期扫描"的设计,都能帮助你在 TinaCMS 之上构建出高性能、可扩展的内容查询层。

【免费下载链接】tinacmsTinaCMS is the leading open-source headless CMS that supports Markdown and Visual Editing. Your content is stored in your own GitHub repo 🦙 ❤️项目地址: https://gitcode.com/GitHub_Trending/ti/tinacms

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

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

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

立即咨询