尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Keystone 6 代码复用实战:List、字段与 Hooks 的类型安全抽取模式

发布时间:2026/9/24 14:45:14

资讯中心
01
ARTICLE

Keystone 6 代码复用实战:List、字段与 Hooks 的类型安全抽取模式

Keystone 6 代码复用实战:List、字段与 Hooks 的类型安全抽取模式
后端【免费下载链接】keystoneThe superpowered headless CMS for Node.js — built with GraphQL and React项目地址https://gitcode.com/gh_mirrors/key/keystone点击查看免费下载导读在 Keystone 6 中随着项目增长多个列表List往往需要共享同一组系统字段——例如谁在何时创建/更新了这条记录。直接在每个列表里重复粘贴配置会导致代码膨胀且难以维护。本文以仓库中的 examples/reuse 示例为蓝本完整讲解如何把列表、字段、hooks 与权限配置抽象成可复用的 TypeScript 函数并借助BaseListTypeInfo泛型约束实现类型安全的复用——避免复用代码时常见类型退化成any的问题。读完本文你将掌握一套可立即落地的 Keystone 列表字段复用工程化方案。示例项目概览这个示例到底在做什么examples/reuse示例的目标非常明确演示如何复用 lists、fields、hooks 以及其他函数。示例作者在 README 中直言Navigating the types of these primitives can be difficult without experience, so hopefully this project helps you understand how you can apply this to your project.即这些原语primitives的类型体系并不直观该示例正是为那些希望在自己的项目中应用复用模式的开发者准备的。同时 README 也给出了一个重要提醒Reuse is not encouraged typically, but it can be helpful in projects that have grown beyond having everything inline.也就是说Keystone 官方并不鼓励为了复用而复用——当项目还很小时把所有配置内联inline写在schema.ts里反而更清晰只有当项目已经成长到全部内联变得难以维护时才值得引入本文介绍的抽取与复用模式。该示例在仓库中的完整文件结构如下examples/reuse文件作用schema.ts定义全部列表与可复用字段工厂函数文章核心keystone.tsKeystone 入口配置SQLite 数据库与 Prisma 适配器package.json示例的脚本与依赖dev/build/start/checkprisma.config.tsPrisma 独立配置schema 路径、migrations、datasource URLschema.prisma由 Keystone 自动生成的 Prisma 数据模型schema.graphql由 Keystone 自动生成的 GraphQL Schema快速开始克隆、安装与启动按照 README 的说明运行该示例需要先克隆 Keystone 仓库在仓库根目录执行pnpm install安装依赖然后进入示例目录启动开发服务# 在仓库根目录安装全部 workspace 依赖 pnpm install # 进入示例目录 cd examples/reuse # 启动 Keystone 开发服务器 pnpm devpnpm dev实际执行的命令是keystone dev见 examples/reuse/package.json。启动后Admin UI运行在 localhost:3000你可以直接在界面中向一个空数据库添加数据例如新建 Invoice、Order、User当NODE_ENV不等于production时默认还可以在 localhost:3000/api/graphql 使用 GraphQL Playground 交互式查询与变更数据数据库文件默认生成在file:./keystone-example.db可通过环境变量DATABASE_URL覆盖见下方配置小节。package.json中还提供了其他常用脚本{ scripts: { dev: keystone dev, start: keystone start, build: keystone build, check: keystone postinstall } }其中keystone postinstallpnpm check用于生成 Prisma Client 等产物keystone build会构建 Admin UI 静态资源。入口配置SQLite Prisma BetterSQLite3 适配器examples/reuse/keystone.ts 展示了现代 Keystone 6配 Prisma 独立配置的入口写法import { PrismaBetterSqlite3 } from prisma/adapter-better-sqlite3 import { config } from keystone-6/core import { lists } from ./schema export default config({ db: { provider: sqlite, prismaClientOptions: () ({ adapter: new PrismaBetterSqlite3({ url: process.env.DATABASE_URL || file:./keystone-example.db, }), }), // WARNING: this is only needed for our monorepo examples, dont do this }, lists, })要点说明provider: sqlite声明数据库方言示例目录没有独立的.env文件数据库 URL 通过process.env.DATABASE_URL注入缺省回退到file:./keystone-example.dbprismaClientOptions允许注入 Prisma 客户端适配器这里使用prisma/adapter-better-sqlite3提供驱动层实现代码中的WARNING注释明确提醒prismaClientOptions这种写法仅仅是为本仓库 monorepo 示例而存在在真实项目中请勿照搬应直接使用标准的 Prisma 驱动配置。与之配套的 examples/reuse/prisma.config.ts 是 Prisma 侧的独立配置文件import { defineConfig } from prisma/config export default defineConfig({ schema: schema.prisma, migrations: { path: migrations, }, datasource: { url: process.env.DATABASE_URL || file:./keystone-example.db, }, })它声明了 Prisma schema 文件位置、migrations 目录以及数据源 URL——注意 URL 回退逻辑与keystone.ts保持一致避免配置漂移。核心模式把系统字段抽取为工厂函数examples/reuse的灵魂在 examples/reuse/schema.ts。示例把一组审计/追踪字段createdBy、createdAt、updatedBy、updatedAt抽成了一个函数trackingFields让Invoice、Order两个列表通过展开运算符一次性获得这四个字段export const lists { Invoice: list({ access: allowAll, fields: { title: text(), completed: checkbox(), ...trackingFieldsLists.Invoice.TypeInfo(), }, }), Order: list({ access: allowAll, fields: { title: text(), completed: checkbox(), name: text(), ...trackingFieldsLists.Order.TypeInfo(), }, }), // User、Unused 略 } satisfies Lists从生成的 examples/reuse/schema.prisma 可以看到Invoice与Order两个 model 都平等地拥有了createdBy String、createdAt DateTime?、updatedBy String、updatedAt DateTime?四列——这证明字段工厂函数被正确地展开进了每一个列表model Invoice { id String id default(cuid()) title String default() completed Boolean default(false) createdBy String default() createdAt DateTime? updatedBy String default() updatedAt DateTime? } model Order { id String id default(cuid()) title String default() completed Boolean default(false) name String default() createdBy String default() createdAt DateTime? updatedBy String default() updatedAt DateTime? }同时在 examples/reuse/schema.graphql 中Invoice与Order的 GraphQL 输出类型也同步包含createdBy: String、createdAt: DateTime、updatedBy: String、updatedAt: DateTime字段且这些字段不会出现在 create/update 输入类型中因为系统字段通常不允许调用方直接写入见下文systemField的graphql.omit配置。类型安全的诀窍BaseListTypeInfo泛型约束复用代码最容易踩的坑是类型丢失一旦把字段配置抽成普通函数TypeScript 常常把类型推断成宽泛的BaseListTypeInfo导致resolvedData、item里的字段全部变成不可知的类型写 hooks 时毫无提示、容易写错。examples/reuse给出了优雅的解法先定义CompatibleLists接口把复用代码真正依赖的字段显式声明出来type CompatibleLists BaseListTypeInfo { item: { completed: boolean } }然后让工厂函数以该约束为下限的泛型参数定义function trackingFieldsListTypeInfo extends CompatibleLists() { ... }这里的关键是extends CompatibleLists它要求调用方传入的ListTypeInfo至少具备item.completed: boolean同时保持自身的全部真实类型信息。BaseListTypeInfo的定义位于 packages/core/src/types/type-info.ts其item字段对应列表项的基本类型。示例中有一段特意用来演示类型精确性的辅助函数// we use this function to show that completed is a boolean type // which would be missing if the types were unrefined // a common problem when re-using code function isTrue(b: boolean) { return b true }它被用在updatedBy字段的resolveInputhook 中updatedBy: textListTypeInfo({ ...systemField, hooks: { async resolveInput({ context, operation, resolvedData, item, fieldKey }) { // show we have refined types for compatible item.* fields if (isTrue(item?.completed ?? false) resolvedData.completed ! false) return undefined ... }, }, }),注意item?.completed和resolvedData.completed之所以能直接传给isTrue(b: boolean)正是因为CompatibleLists约束声明了item.completed: boolean如果没有这层类型精化item.completed会是unknown或宽泛类型isTrue调用处就会报类型错误——这正是复用代码时常见问题的活教材。而User、Unused这类列表没有completed字段因而不能调用trackingFields类型上不允许从类型层面杜绝了把不兼容的字段工厂误用到不匹配的列表上。从源码注释还可以看到作者标注的 TODOFIXME: CommonFieldConfig need not always be generalised提示字段工厂返回类型的泛化程度仍可进一步优化——这属于示例中的开放边界读者在实际项目中可按需自行收敛。systemField字段级访问控制与 UI 行为的统一抽取除了 hooks示例还把字段级的access访问控制与UI 表现抽成了公共对象systemField并通过...systemField展开进每个系统字段const systemField { access: { read: { item: allowAll, filter: denyAll, order: denyAll }, create: denyAll, update: denyAll, }, graphql: { omit: { create: true, update: true, }, }, ui: { createView: { fieldMode: hidden as const }, itemView: { fieldMode: read as const, fieldPosition: sidebar as const, }, listView: { fieldMode: read as const }, }, }逐项解读其语义accessread拆成三项——item读具体项放行allowAll、filter按字段过滤与order按字段排序拒绝denyAllcreate、update全部拒绝denyAll。也就是说调用方无法主动创建或修改这些系统字段只能通过 hooks 写入graphql.omit在 create/update 的 GraphQL 输入类型中隐藏这些字段对应 examples/reuse/schema.graphql 中InvoiceCreateInput只有title、completed的现象ui创建视图中隐藏、详情视图中只读且置于侧栏、列表视图中只读从而避免在 Admin UI 里误编辑系统字段。allowAll/denyAll的底层实现非常直接见 packages/core/src/access.tsexport function allowAll() { return true } export function denyAll() { return false }它们是返回布尔值的函数配合 Keystone 的访问控制引擎使用——在 packages/core/src/lib/core/access-control.ts 中各操作的默认值即为allowAll如query: allowAll、create: allowAll而字段断言逻辑会检查field.access.read.item ! allowAll等见 packages/core/src/lib/core/field-assertions.ts以保证自定义 access 不会与框架假设冲突。用 hooks 实现自动审计createdBy / createdAt / updatedBy / updatedAttrackingFields中四个字段的 hooks 组合起来就构成了一套完整的谁在何时改了什么的自动审计逻辑createdBy: textListTypeInfo({ ...systemField, hooks: { resolveInput: { async create({ context }) { return ${context.req?.socket.remoteAddress} (${context.req?.headers[user-agent]}) }, async update() { return undefined }, }, }, }),createdBy仅在create时写入客户端 IP User-Agent字符串从context.req读取update时不改变createdAt仅在create时写入new Date()update时返回undefined保持不变updatedBy在resolveInput完整回调中做条件判断——若item.completed为 true 且本次更新没有把completed改为 false则不更新返回undefined否则记录 IP User-AgentupdatedAt任何写入操作create/update都刷新为new Date()。updatedBy: textListTypeInfo({ ...systemField, hooks: { async resolveInput({ context, operation, resolvedData, item, fieldKey }) { if (isTrue(item?.completed ?? false) resolvedData.completed ! false) return undefined return ${context.req?.socket.remoteAddress} (${context.req?.headers[user-agent]}) }, }, }),这段updatedBy逻辑同时演示了resolveInput完整形态的四个关键参数参数含义contextKeystone 请求上下文可访问req、数据库等operation当前操作类型create / updateresolvedData本次写入的已解析数据含本次提交的字段值item已存在的数据库记录update 时有值create 时通常为空fieldKey当前字段的 key注意context.req可能为 undefined例如脚本或测试环境示例中统一用可选链?.安全访问。验证复用成果看生成的 Schemaexamples/reuse目录下的schema.prisma与schema.graphql都是由 Keystone 自动生成的产物可用于验证复用配置是否正确落地从 examples/reuse/schema.prisma 看Invoice、Order都含四个追踪列而User只有name与Unused只有completed则没有——说明字段工厂只影响被调用的列表从 examples/reuse/schema.graphql 看InvoiceCreateInput/InvoiceUpdateInput只暴露title、completed追踪字段不出现在输入类型中graphql.omit生效但在Invoice输出类型中完整可见Mutation类型中每个列表都自动获得了create/update/delete及其复数批量形式如createInvoices、updateInvoices、deleteInvoicesUnused列表在 GraphQL 中同样拥有完整的 CRUDcreateUnused、updateUnused、deleteUnused演示了一个仅做演示用途的列表也可以被正常生成。复用边界什么时候该用什么时候不该用结合 README 的告诫与示例代码可以总结出几条实践准则先内联后抽取项目初期把所有字段配置写在列表内等出现两处以上完全一致的字段 hooks权限且修改成本变高时再考虑抽取用泛型约束守住类型边界工厂函数必须像trackingFieldsListTypeInfo extends CompatibleLists这样声明它依赖的字段类型避免退化为any或宽泛类型让 hooks 中的item、resolvedData保持可提示、可校验把行为和表现一起抽示例把 access、graphql.omit、ui 表现统一放进systemField并随字段一起展开确保复用的不仅是字段定义还有完整的权限与 UI 语义善用生成产物验证每次调整复用代码后重新运行keystone build或pnpm check并检查schema.prisma/schema.graphql确认字段、权限、GraphQL 输入输出符合预期。小结examples/reuse演示的复用模式本质上是把 Keystone 的字段配置对象当作一等公民进行组合用展开运算符做字段合并、用泛型参数做类型精化、用systemField做行为封装、用resolveInputhooks 做自动审计。它没有引入任何黑魔法全部建立在 Keystone 6 公开的类型体系BaseListTypeInfo、字段工厂的泛型参数之上因此可以直接迁移到任何基于keystone-6/core的项目中。对于已经长大、需要系统化组织列表配置的 Keystone 项目这套模式是一个高性价比的工程化参考。赞分享后端【免费下载链接】keystoneThe superpowered headless CMS for Node.js — built with GraphQL and React项目地址https://gitcode.com/gh_mirrors/key/keystone点击查看免费下载相关推荐Keystone 6 Multiselect 字段完整指南类型、选项与 GraphQL 集成实战Keystone 6 Multiselect 字段完整指南类型、选项与 GraphQL 集成实战 导读 multiselect 是 Keystone 内置的“后端langchain/cloudflare 版本演进详解从原生 streamEvents 转换到 AbortSignal 处理与兼容性修复langchain/cloudflare 版本演进详解从原生 streamEvents 转换到 AbortSignal 处理与兼容性修复 langchai后端终极指南如何用Gopeed全平台高速下载器告别下载烦恼终极指南如何用Gopeed全平台高速下载器告别下载烦恼 还在为下载大文件时速度缓慢而烦恼吗是否经常遇到网络不稳定导致下载中断不得不重新开始的困扰Gope网络CLI后端上一篇如何在昇腾处理器上部署LiteLlama-460M-1T5分钟快速入门教程下一篇Area51协议文档版本控制变更跟踪与历史创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。