尧图精选

RedwoodJS Cells 深度指南:用声明式约定驾驭 GraphQL 数据获取生命周期

🕒 发布时间:2026/9/21 2:58:53 📁 来源:尧图网络
RedwoodJS Cells 深度指南用声明式约定驾驭 GraphQL 数据获取生命周期【免费下载链接】redwoodRedwoodGraphQL项目地址: https://gitcode.com/gh_mirrors/re/redwoodCells 是 RedwoodJS 最具标志性的抽象模式之一你只需导出QUERY、Loading、Empty、Failure、Success等命名常量声明查询生命周期各个阶段长什么样框架便会借助 Babel 插件在构建期把这些常量自动装配成一个完整的 React 组件。本文以官方文档 docs/docs/cells.md 为主线结合仓库中 createCell.tsx、babel-plugin-redwood-cell.ts 等源码实现带你从会用深入到懂原理掌握 Cell 的全部七个导出、生成器用法、生命周期钩子以及框架在构建期究竟替你做了什么。Cells 是什么一次查询五种状态零命令式代码Cells 是一套声明式的数据获取方案是 RedwoodJS 最具代表性的抽象模式之一。它通过约定convention接管请求 → 响应的整个过程让框架得以在请求与响应之间插入查询优化等能力而你的应用代码完全无需改动。表面上看起来充满魔法但一个 Cell 的本质非常简单它只是执行一次 GraphQL 查询并管理其生命周期。核心思想是你导出若干个命名常量声明查询生命周期每个阶段 UI 应该长什么样Redwood 在构建期通过 Babel 插件把这些常量装配成一个组件模板——全程不需要你写一行命令式代码。例如一个最基本的 Cell 文件看起来是这样的export const QUERY gql query { posts { id title body createdAt } } export const Loading () divLoading.../div export const Empty () divNo posts yet!/div export const Failure ({ error }) divError loading posts: {error.message}/div export const Success ({ posts }) { return posts.map((post) ( article h2{post.title}/h2 div{post.body}/div /article )) }这段代码没有useQuery、没有loading/error/data的判断逻辑——框架会处理好一切。生成一个 Cell一个命令产出四个文件使用 Redwood 的 Cell 生成器即可创建 Cellyarn rw generate cell name该命令会在web/src/components下创建一个名为nameCell的目录包含四个文件文件说明nameCell.js真正的 Cell 本体nameCell.test.js覆盖 Cell 各状态的 Jest 测试nameCell.stories.js覆盖 Cell 各状态的 Storybook storiesnameCell.mock.js供 Jest 测试与 Storybook stories 共用的 Mock 数据从生成器源码 cell.js 可以看到生成器不仅产出这四个文件还会做大量智能推断解析 Prisma 模型通过getSchema(pascalcase(singularize(cellName)))从schema.prisma中读取对应模型进而推断主键字段名getIdName与主键类型getIdType并据此生成按 id 查询的默认QUERY如post(id: $id)以及 mock 值默认[42, 43, 44]String 类型会自动加引号生成唯一操作名通过uniqueOperationName确保生成的query FindXxxQuery操作名在整个项目中唯一若你手动用--query指定操作名还会调用operationNameIsUnique校验其不与其他已存在的操作名冲突选择模板根据单条还是列表选择cell.tsx.template或cellList.tsx.template并分别产出对应的测试、story 与 mock 文件。单条 Cell vs 列表 Cell智能推断与 --list 兜底有时你需要渲染单条数据有时需要渲染列表Redwood 的 Cell 生成器两者皆可。首先生成器会检测name是单数还是复数。例如要生成一个渲染用户列表的 Cell运行yarn rw generate cell users即可。其次对于单复数同形的不规则单词如 equipment、pokemon需要显式传入--list告知 Redwood 生成列表型 Cellyarn rw generate cell equipment --list在源码层面这个判断逻辑对应 cell.js 中的shouldGenerateListconst shouldGenerateList (isWordPluralizable(cellName) ? isPlural(cellName) : options.list) || options.list即单词可复数化时按单词本身的单复数判断不可复数化单复同形时完全依赖--list参数。一旦判定为列表cellName会被强制复数化forcePluralizeWord模板切换到cellList.tsx.template操作名也变为FindPlurals形式。Cells 的七个导出一张表讲清全部契约使用 Cells 时你一共有七个导出可用名称类型说明QUERYstring, function要执行的查询beforeQueryfunction生命周期钩子为查询准备 variables 和 optionsisEmptyfunction生命周期钩子决定 Cell 是否应渲染EmptyafterQueryfunction生命周期钩子对查询返回的数据做清洗Loadingcomponent请求进行中时渲染的组件Emptycomponent无数据null或[]时渲染的组件Failurecomponent出错时渲染的组件Successcomponent数据加载成功后渲染的组件其中只有QUERY和Success是必需的。如果你不导出Empty空结果会直接进入Success如果不导出Failure错误会输出到控制台。这些契约在类型层面被严格定义在 cellTypes.ts 的CreateCellProps接口中源码注释明确了每个字段的语义QUERY是 The GraphQL syntax tree to execute or function to call that returns itbeforeQuery是 Parsepropsinto query variablesafterQuery是 Sanitize the data returned from the query。而在 Babel 插件的 EXPECTED_EXPORTS_FROM_CELL 列表中允许从 Cell 文件导出的正是这些名称beforeQuery、QUERY、data、isEmpty、afterQuery、Loading、Success、Failure、Emptydata是 RSC 服务端 Cell 场景的扩展。组件与 Props 的精确分流除了在正确的时机渲染正确的组件Cells 还会把正确的 props 精确地传给正确的组件。Loading、Empty、Failure、Success都能以标准的 React 方式访问父组件传下来的 props并且都能拿到useQuery返回结果的大部分内容作为名为queryResult的 prop。在此基础上Empty和Success额外获得查询返回的data以及一个updating布尔值表示 Cell 是否正在拉取新数据Failure额外获得updating并且独占error和errorCode两个 props。useQuery返回结果的大部分内容这一说法的精确定义可以在 createCell.tsx 的源码中看到框架从useQuery的返回值中解构出error、loading、data三个字段做生命周期判断其余全部原样收进queryResult透传给各个状态组件。QUERY字符串或函数多个根查询也 OKQUERY可以是字符串也可以是函数函数必须返回一个合法的 GraphQL document。在源码 createCell.tsx 中可以看到如果QUERY是函数它会被以beforeQuery的返回结果作为参数调用一次再执行。一个 Cell 里含有多个根查询完全没问题export const QUERY gql{ query { posts { id title } authors { id name } } } 这种情况下posts和authors都会出现在Success的 props 里export const Success ({ posts, authors }) { // ... }props 即 variables从 SDL 反推 Cell 的 props通常查询都带有变量。Cells 默认会把它们从父组件接收到的任何 props 当作查询变量这个行为正是通过默认的beforeQuery实现的。例如BlogPostCell接收一个numberToShowprop那么numberToShow在QUERY中直接可用import BlogPostsCell from src/components/BlogPostsCell const HomePage () { return ( div h1Home/h1 BlogPostsCell numberToShow{3} / /div ) } export default HomePageexport const QUERY gql query ($numberToShow: Int!) { posts(numberToShow: $numberToShow) { id title } } 这意味着你可以从 SDL 反向思考 Cell 的 propsSDL 里定义了哪些查询变量Cell 的 props 就应该是什么。类型层面cellTypes.ts 中的CellPropsVariables精确刻画了这一行为如果 Cell 定义了beforeQueryprops 类型取自beforeQuery的第一个参数否则直接取GQLVariables。beforeQuery配置 useQuery 的唯一入口beforeQuery是一个生命周期钩子最好的理解方式是把它看作配置 Apollo ClientuseQueryhook 的机会。默认情况下beforeQuery会把父组件传入的任何 props 交给QUERY作为 variables同时把 fetch policy 设置为cache-and-network框架认为这最符合多数用户期望的行为export const beforeQuery (props) { return { variables: props, fetchPolicy: cache-and-network, } }这一默认实现与 createCell.tsx 中createNonSuspendingCell的默认参数完全一致。值得注意的是源码注释还揭示了一个细节默认的fetchPolicy: cache-and-network和notifyOnNetworkStatusChange: true之所以在beforeQuery里重复设置是因为 Apollo Client v3.5.4 的一个疑似 bug——它没有尊重RedwoodApolloProvider中配置的defaultOptions。例如若想开启 Apollo 的轮询并禁用缓存可以这样写export const beforeQuery (props) { return { variables: props, fetchPolicy: no-cache, pollInterval: 2500 } }你还可以用beforeQuery从 Cell 的 props 之外获取数据来填充变量比如 React Context API 或全局状态管理库。一旦你提供了beforeQuery函数Cell 会自动把它的 props 类型改为该函数第一个参数的类型// 该 Cell 不接受任何 propsCell / export const beforeQuery () { const { currentUser } useAuth() return { variables: { userId: currentUser.id }, } }// 该 Cell 接收 1 个名为 word 的 string 类型 propCell wordabc export const beforeQuery ({ word }: { word: string }) { return { variables: { magicWord: word } } }这正是 cellTypes.ts 中CellPropsVariables的条件类型逻辑有beforeQuery时以ParametersCell[beforeQuery][0]为准没有时以GQLVariables为准。isEmpty覆盖什么算空的默认判断isEmpty是可选的生命周期钩子返回一个布尔值指示 Cell 是否应渲染Empty组件。用它来覆盖默认判断——默认判断检查 Cell 的根字段是否为null或空数组。它接收两个参数1)data2) 一个包含默认isEmpty函数名为isDataEmpty的对象方便你在默认行为之上做扩展export const isEmpty (data, { isDataEmpty }) { return isDataEmpty(data) || data?.blog?.status hidden }默认实现位于 isCellEmpty.ts其逻辑为data不存在或data的所有字段值均为null或空数组时视为空export function isDataEmpty(data?: DataObject) { return ( !data || Object.values(data).every((fieldValue) { return fieldValue null || isFieldEmptyArray(fieldValue) }) ) }注意两个细节源码注释与 cellTypes.ts 均有说明列表字段的 SDL 声明不同如posts: [Post!]vsposts: [Post]空数据可能返回[]也可能返回null默认实现两种情况都覆盖对于多根查询默认isEmpty只保证至少有一处数据并不保证所有字段都有数据——因此 cellTypes.ts 中的ConditionallyGuaranteed类型只在单根查询时把数据标记为必然非空。afterQuery数据抵达 Success 前的最后一道清洗afterQuery是一个生命周期钩子在数据到达Success之前执行用来对QUERY返回的数据做清洗sanitize。默认情况下afterQuery原样返回数据。在 createCell.tsx 中可以看到它的调用位置与调用方式const afterQueryData afterQuery(data)随后这份处理后的数据会被展开spread进Empty或Success的 props。四个状态组件Loading / Empty / Failure / SuccessLoading请求进行中如果没有任何缓存数据且请求仍在进行中Cell 渲染Loading组件。本地开发时如果想捕捉 Cell 等待响应的瞬间可以把浏览器 Inspector 的Network面板里的网络速度调成类似 Slow 3G。不过Redwood 自带的 Storybook 是更好的选择——用它开发Loading和Failure组件完全不必忍受 Slow 3G 这种临时方案也不需要故意把应用弄坏。Empty无数据时的兜底如果没有数据Cell 渲染Empty组件。所谓没有数据指响应为 1)null或 2) 空数组[]。Failure出错时的兜底如果出了任何差错Cell 渲染Failure组件。想快速看到效果可以在QUERY里加一个 SDL 中没有定义的字段const QUERY gql query { posts { id title unTypedField } } 但和Loading一样Storybook 通常是开发这个组件的更佳场所。Failure组件独有的errorCodeprop 非常实用可以据此做条件渲染甚至作为国际化i18n的翻译键export const Failure ({ error, errorCode }: CellFailureProps) { const { t } useTranslation() return ( div style{{ color: red }} {errorCode NO_CONFIG ? h1NO_CONFIG/h1 : h1ERROR/h1} Error: {error.message} - Code: {errorCode} - {t(error.${errorCode})} /div ) }errorCode的来源可以在 createCell.tsx 中找到它优先取queryResult.errorCode否则从error.graphQLErrors[0].extensions[code]中提取。类型定义见 cellTypes.ts 的CellFailurePropserror甚至允许直接传入用于测试和 Storybook。Success数据就绪时的渲染如果一切顺利Cell 渲染Success组件。如前所述Success独享data。但如果你试图从props中解构它会发现它并不存在——这是 Redwood 特意加的一层便利Redwood 会把data展开spread进Success让你可以直接解构QUERY中预期的字段。所以如果你查询的是posts和authors无需这样写export const Success ({ data }) { const { posts, authors } data // ... }Redwood 允许你直接这样写export const Success ({ posts, authors }) { // ... }注意其他 props 仍然可以照常传给Success——说到底它只是一个普通的 React 组件。这一展开行为对应 createCell.tsx 中Success {...props} {...afterQueryData} updating{loading} queryResult{queryResult} /的渲染方式。关于 TypeScript 与 Cells 的配合可进一步阅读 Utility Types 文档其中定义了CellSuccessData、CellSuccessProps、CellFailureProps等工具类型的用法。何时该用 Cell何时不必只要你想获取数据就可以用 Cell。让 Redwood 去处理何时显示什么你只需专注于这些状态应该长什么样。但用 Cell 并非强制。比如一次性查询随时可以使用useApolloClient拿到客户端直接执行查询// 在 React 组件中... client useApolloClient() client.query({ query: gql ... , })可以在 Cell 里执行 Mutation 吗完全可以。仓库中的示例项目example-todo-main里TodoListCell 就是一个在 Cell 中同时进行查询与变更加待办事项的典型实现。Redwood 官方并不认为在 Cell 中做 mutation 是反模式恰恰相反——你的 Cell 可能会承载大量逻辑在很多方面真正成为应用的枢纽。同时请记住除了导出特定名称的特定常量之外Cells 几乎没有其他规则——普通组件里能做的一切在 Cell 里照样能做。框架如何识别一个文件是 CellBabel 插件剖析你只需要让文件名以 Cell 结尾对吗基本正确但还有一件事需要知道。Redwood 会查找所有以 Cell 结尾的文件所以如果你想让它成为 Cell文件名确实必须以 Cell 结尾但满足以下两个条件的文件会被跳过没有导出名为QUERY的常量有默认导出default export。什么时候会用到这个规则比如你只是想有个文件碰巧以 Cell 结尾。除此之外完全不用在意它。这一逻辑在 babel-plugin-redwood-cell.ts 中有完整实现插件的工作流程如下收集导出遍历文件中的ExportNamedDeclaration把名称命中的EXPECTED_EXPORTS_FROM_CELL列表beforeQuery、QUERY、data、isEmpty、afterQuery、Loading、Success、Failure、Empty的导出名收集到exportNames数组判定在Program.exit阶段检查——如果文件已有默认导出说明它可能已是被包装过的 Cell或根本不是 Cell或没有QUERY/data导出就直接返回不做处理对应源码 babel-plugin-redwood-cell.ts#L75-L87注入代码在文件顶部自动插入import { createCell } from redwoodjs/web如果导出的是data而非QUERY则改为从redwoodjs/web/dist/components/cell/createServerCell导入createServerCell这是 RSC 服务端 Cell 的场景在文件底部自动追加export default createCell({ ...exportNames, displayName })其中displayName取自文件名。也就是说构建期 Babel 插件 运行时createCell高阶组件共同完成了把命名导出装配成完整 Cell这件魔法。装配后的渲染决策树就是 createCell.tsx 中的那段逻辑有error且有Failure→ 渲染Failure没有Failure→ 直接抛出错误有data→ 先过afterQuery再用isEmpty判断为空且有Empty就渲染Empty否则渲染Success既无data也无error且loading→ 渲染Loading到达一个查询成功但data为null的意外状态 → 抛出带排查建议的错误提示检查 Storybook 中查询是否缺少字段、是否为 GraphQL 缓存 bug、是否在查询字段上补充id。值得一提的是createCell.test.tsx 中有 20 余个createCell测试用例覆盖了各状态渲染、props 透传、错误处理等行为是理解这套决策树行为细节的最佳参考。进阶示例如果不用 Babel手写一个 Cell 是什么样假设 Babel 不会来帮你装配导出你该怎么自己实现一个 Cell以上文获取 posts的示例为例你很可能需要写出类似下面的代码——这段代码本质上就是把 createCell.tsx 的内容抄进你自己的文件const QUERY gql query { posts { id title body createdAt } } const Loading () divLoading.../div const Empty () divNo posts yet!/div const Failure ({ error }) ( divError loading posts: {error.message}/div ) const Success ({ posts }) { return posts.map((post) ( article h2{post.title}/h2 div{post.body}/div /article )) } const isEmpty (data) { return isDataNull(data) || isDataEmptyArray(data) } export const Cell () { return ( Query query{QUERY} {({ error, loading, data }) { if (error) { if (Failure) { return Failure error{error} / } else { console.error(error) } } else if (loading) { return Loading / } else if (data) { if (typeof Empty ! undefined isEmpty(data)) { return Empty / } else { return Success {...data} / } } else { throw Cannot render Cell: graphQL success but data is null } }} /Query ) }这是相当可观的一段代码——而且全是命令式代码。试想一下每次要获取一份可能延迟响应的数据都要把这段逻辑重写一遍想想就头大。这正是 Cells 的核心价值所在Redwood 把这段样板逻辑固化在框架里当前仓库的运行时实现是 createCell.tsx并且会依据RWJS_ENV.RWJS_EXP_STREAMING_SSR环境开关在普通实现与 Suspense 版实现 createSuspendingCell.tsx 之间自动切换再加上构建期的 Babel 自动装配让你只需要用七个命名导出描述 UI剩下的一切交给框架。【免费下载链接】redwoodRedwoodGraphQL项目地址: https://gitcode.com/gh_mirrors/re/redwood创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →