news 2026/9/24 15:35:44

Redwood Cells 完全指南:用声明式约定接管 GraphQL 数据获取与生命周期

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redwood Cells 完全指南:用声明式约定接管 GraphQL 数据获取与生命周期
  • 后端
  • 前端
  • Web框架
  • 开发工具

【免费下载链接】redwood

RedwoodGraphQL

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

导读

Cells 是 Redwood 最具标志性的数据获取抽象:它用一套命名导出约定(QUERYLoadingEmptyFailureSuccess)把 GraphQL 查询的完整生命周期(加载中、空数据、出错、成功)拆解为纯粹的 UI 组件,由框架在构建期通过 Babel 插件自动装配,让你无需编写任何命令式代码。读完本文,你将掌握如何生成 Cell、如何利用beforeQuery/isEmpty/afterQuery三个生命周期钩子精确控制查询行为,并能理解 Redwood 在源码层面(createCell.tsx)是如何把七种导出组合成真实组件的。

Cells 是什么:声明式数据获取的核心抽象

Cells 是一套对数据获取的声明式约定。它的核心思路是:由你导出若干命名常量,声明你在查询生命周期的每个阶段希望 UI 长成什么样子;Redwood 在构建期借助 Babel 插件,把这些导出装配成一个组件模板,最终替你执行 GraphQL 查询并管理其生命周期。

正因为 Redwood 介入了"请求与响应之间"的过程,它才有机会在不改变你任何业务代码的前提下,做查询优化等额外工作。从底层看,一个 Cell 的本质就是"执行一次 GraphQL 查询 + 管理其生命周期"——具体实现在 createCell.tsx 中,createCell接收 Cell 的各种导出,返回一个标准的 React 组件,内部使用useQuery(通过GraphQLHooksProvider解耦,便于替换 GraphQL 客户端)驱动整个流程。

生成一个 Cell

使用 Redwood 的 Cell 生成器:

yarn rw generate cell <name>

该命令会在web/src/components下创建一个名为<name>Cell的目录,包含四个文件:

文件说明
<name>Cell.js真正的 Cell
<name>Cell.test.js覆盖 Cell 各状态的 Jest 测试
<name>Cell.stories.js覆盖 Cell 各状态的 Storybook stories
<name>Cell.mock.js供 Jest 测试与 Storybook stories 共用的 Mock 数据

生成器的实际逻辑位于 cell.js,它会依次生成 Cell 组件文件(基于 cell.tsx.template 或cellList.tsx.template)、测试文件(test.js.template)、stories 文件(stories.tsx.template)与 mock 文件。

单条数据 Cell 与列表 Cell

Redwood 的 Cell 生成器同时支持"渲染单个条目"与"渲染列表"两种形态:

  • 自动判断单复数:生成器先检测<name>是单数还是复数。例如要生成渲染用户列表的 Cell,直接运行yarn rw generate cell users
  • 手动指定列表:对于单复数同形的不可数/不规则词(如equipmentpokemon),可以显式传入--list告诉 Redwood 生成列表 Cell:
yarn rw generate cell equipment --list

在源码层面,cell.js 通过isWordPluralizable(cellName) ? isPlural(cellName) : options.list决定shouldGenerateList,进而选择cellList模板并对单词强制复数化。同时,生成器会为列表与单条查询分别生成唯一且可预期的 operation name(列表形如UsersQuery、单条形如FindUserQuery),冲突时自动追加数字下标,见 utils.js。

提示:单条 Cell 的QUERY默认基于schema.prisma中的模型生成(如按id查询),若你的查询字段与模型不同,记得修改根查询。教程中有一个很好的实例:Tutorial - Cells。

深入了解 Cells

Cells 导出了五个核心常量:QUERYLoadingEmptyFailureSuccessQUERY中的根查询默认与<name>同名,这样当你基于schema.prisma中的模型生成 Cell 时,可以立刻从数据库拿到数据。但多数情况下你不会这样直接使用生成结果,因此务必按需修改根查询。

完整用法:七种导出

一个 Cell 总共可以有七种导出,各司其职:

名称类型说明
QUERYstring, function要执行的查询
beforeQueryfunction生命周期钩子;为查询准备 variables 与 options
isEmptyfunction生命周期钩子;决定 Cell 是否渲染Empty
afterQueryfunction生命周期钩子;净化查询返回的数据
Loadingcomponent请求进行中时渲染的组件
Emptycomponent无数据(null[])时渲染的组件
Failurecomponent出错时渲染的组件
Successcomponent数据加载完成后渲染的组件

只有QUERYSuccess是必需的。若未导出Empty,空结果会直接交给Success;若未导出Failure,错误只会输出到控制台(源码中对应 createCell.tsx:有Failure则渲染,否则直接throw error交由上层错误边界处理)。

除"在正确时机渲染正确组件"外,Cells 还会把正确的 props 分发给正确的组件:

  • LoadingEmptyFailureSuccess都能以常规 React 方式访问父级传入的 props,并能拿到useQuery返回值的大部分内容(作为一个名为queryResult的 prop)。
  • EmptySuccess额外获得查询返回的data,以及一个表示 Cell 当前是否正在拉取新数据的updating布尔值(源码中updating={loading},即把loading重命名后暴露给用户,便于渲染"后台刷新中"的指示器)。
  • Failure也拥有updating,并且独占errorerrorCode两个 props。

useQuery的确切返回值以 Apollo Client 的 API 文档为准;注意errordata在 Cell 中享受了特殊处理。

QUERY

QUERY可以是字符串或函数;如果是函数,必须返回一个合法的 GraphQL 文档。

一个 Cell 完全支持包含多个根查询。例如:

export const QUERY = gql`{ query { posts { id title } authors { id name } } }

此时postsauthors都会传给Success

export const Success = ({ posts, authors }) => { // ... }

查询通常带变量。Cells 默认会把父组件传入的任何 props 当作查询变量(这一步在beforeQuery中完成)。例如下面的BlogPostsCell接收一个numberToShowprop,它可以直接在QUERY中使用:

import BlogPostsCell from 'src/components/BlogPostsCell' const HomePage = () => { return ( <div> <h1>Home</h1> <BlogPostsCell numberToShow={3} /> </div> ) } export default HomePage
export const QUERY = gql` query ($numberToShow: Int!) { posts(numberToShow: $numberToShow) { id title } } `

因此你可以从 SDL 反向推导 Cell 的 props:SDL 里有什么变量,Cell 的 props 就应该是什么。

beforeQuery

beforeQuery是一个生命周期钩子,最恰当的理解是:它是配置 Apollo ClientuseQuery选项的机会(对应 CreateCellProps.beforeQuery 的类型定义)。

默认行为是:把父组件传入的所有 props 作为查询变量,并设置fetchPolicy'cache-and-network'(团队认为这最符合大多数用户期望的"先用缓存立即渲染、同时后台刷新"行为),同时打开notifyOnNetworkStatusChange,对应 createCell.tsx 的默认实现:

export const beforeQuery = (props) => { return { variables: props, fetchPolicy: 'cache-and-network', } }

例如,想开启 Apollo 的轮询并禁用缓存,可以这样导出(polling 与 fetchPolicy 详见 Apollo 文档):

export const beforeQuery = (props) => { return { variables: props, fetchPolicy: 'no-cache', pollInterval: 2500 } }

beforeQuery还可以用来填充 props 之外的数据,例如从 React Context 或全局状态库取值。一旦你提供了beforeQuery函数,Cell 的 props 类型会自动变为该函数第一个参数的类型(这正是 cellTypes.ts 中CellPropsVariables的推导逻辑):

// The Cell will take no props: <Cell /> export const beforeQuery = () => { const { currentUser } = useAuth() return { variables: { userId: currentUser.id }, } }
// The cell will take 1 prop named "word" that is a string: <Cell word="abc"> export const beforeQuery = ({ word }: { word: string }) => { return { variables: { magicWord: word } } }

注意QUERY若是函数,它会在beforeQuery之后被调用,并接收beforeQuery的返回结果作为参数(见 createCell.tsx:const query = typeof QUERY === 'function' ? QUERY(options) : QUERY)。

isEmpty

isEmpty是可选的生命周期钩子,返回布尔值,指示 Cell 是否应渲染Empty,用于覆盖默认的空数据判断逻辑。

默认判断是:检查 Cell 的根字段是否为null或空数组。其实现位于 isCellEmpty.ts:!dataObject.values(data).every(field => field === null || 空数组),即所有根字段都为空才算空。例如{ post: null }{ posts: [] }都视为空;单个根字段为null时(如posts: [Post!]的可空场景)同样成立。

它接收两个参数:1)data;2)一个包含默认isEmpty函数的对象(名为isDataEmpty),以便你在其基础上扩展:

export const isEmpty = (data, { isDataEmpty }) => { return isDataEmpty(data) || data?.blog?.status === 'hidden' }

afterQuery

afterQuery是生命周期钩子,在数据到达Success之前运行,用于净化QUERY返回的数据。默认实现是原样返回数据(非 Suspense 版为(data) => data,Suspense 版为(data) => ({ ...data }),见 createCell.tsx 与 createSuspendingCell.tsx)。

Loading

如果没有缓存数据且请求仍在进行,Cell 渲染Loading

本地开发时,可以在浏览器开发者工具 Network 面板把网速调成 "Slow 3G",观察 Cell 短暂停留在加载态。但更推荐的做法是使用 Storybook:生成的*.stories.js覆盖了 Cell 的各个状态,无需依赖 Slow 3G 或故意弄坏应用,就能轻松开发Loading(和Failure)组件。

Empty

Cell 在"没有数据"时渲染Empty。所谓没有数据指的是响应为:1)null;2)空数组[]。若未导出Empty,空结果会直接进入Success

Failure

Cell 在出错时渲染Failure。想快速触发错误,可以给QUERY加一个不存在的字段:

const QUERY = gql` query { posts { id title unTypedField } } `

Loading一样,用 Storybook 开发Failure是更好的选择。

Failure能拿到errorerrorCode。其中errorCode由 Redwood 在运行时计算:优先取useQuery结果中的errorCode,否则从error.graphQLErrors?.[0]?.extensions?.['code']提取(见 createCell.tsx),对应类型定义 CellFailureProps。下面的例子用errorCode条件渲染错误标题,并把它作为翻译字符串的 key:

export const Failure = ({ error, errorCode }: CellFailureProps) => { const { t } = useTranslation() return ( <div style={{ color: 'red' }}> {errorCode === 'NO_CONFIG' ? <h1>NO_CONFIG</h1> : <h1>ERROR</h1>} Error: {error.message} - Code: {errorCode} - {t(`error.${errorCode}`)} </div> ) }

Success

一切正常时,Cell 渲染Success

如前所述,Success能拿到data,但如果你试图从 props 里解构data,会发现它并不存在——这是 Redwood 的一层便利:Redwood 会把data展开(spread)进Success,让你直接从QUERY期望的数据解构。源码中的对应逻辑是return <Success {...props} {...afterQueryData} updating={loading} queryResult={queryResult} />(createCell.tsx)。

所以,如果查询postsauthors,无需这样写:

export const Success = ({ data }) => { const { posts, authors } = data // ... }

Redwood 允许你直接写:

export const Success = ({ posts, authors }) => { // ... }

当然,你仍然可以向Success传入任意其他 props——毕竟它只是一个普通的 React 组件。TypeScript 下的类型提示可参考 CellSuccessProps / CellSuccessData:当查询只有一个根字段时,Redwood 能保证该字段非空(Guaranteed<T>);多根字段时则无法保证每个属性都有数据,这一点与默认isEmpty只检查"存在部分数据"的行为一致。

:::tip 想了解 Cells 与 TypeScript 的配合,请参阅 Utility Types 文档。 :::

何时应该使用 Cell?

任何时候你想获取数据,都可以使用 Cell。让 Redwood 去处理什么时机显示什么,你只需专注这些状态各自长什么样。

不过要强调:你并不必须使用 Cell。想做任何自定义都是允许的。例如,对于一次性的查询,始终可以用useApolloClient拿到客户端并直接执行查询:

// In a react component... client = useApolloClient() client.query({ query: gql` ... `, })

可以在 Cell 里执行 Mutation 吗?

完全可以。Redwood 官方在示例 todo 应用(example-todo-mainfixture 中的TodoListCell)里就演示了在 Cell 内调用 mutation 的写法。Redwood 也不认为这是反模式——恰恰相反,你的 Cell 可能会承载大量逻辑,在很多时候成为应用的"枢纽"。

此外请记住:除了"导出某些特定名字的常量"这一条规则外,Cells 几乎没有其他限制——常规组件里能做的一切,在 Cell 里依然可以做(例如在Success中调用useMutation、使用其他 hooks)。

Redwood 如何识别一个文件是 Cell?

基本规则是:文件名以 "Cell" 结尾。但还有一条补充规则。

Redwood 会扫描所有以 "Cell" 结尾的文件(所以想让组件成为 Cell,文件名确实必须以 "Cell" 结尾),但如果该文件1)没有导出名为QUERY的常量,且 2)存在默认导出,那么它会被跳过,不会被当作 Cell 处理。

什么时候会需要这种跳过?比如你只是出于某种原因想让某个文件以 "Cell" 结尾。除此之外不必担心。

该逻辑的源码实现在 Babel 插件 babel-plugin-redwood-cell.ts:插件在Program.exit时检查是否已有默认导出(若已有默认导出则说明不是 Cell,或已经是包装好的 Cell,因为一个模块只能有一个默认导出)以及是否导出了QUERYdata,满足条件才会继续装配。

具体的构建期装配过程(同样在 babel-plugin-redwood-cell.ts)包括:

  • 在文件顶部自动插入import { createCell } from '@redwoodjs/web'(若导出的是data而非QUERY,则导入createServerCell,走服务端 Cell 路径);
  • 在文件底部自动追加export default createCell({ QUERY, Loading, Success, Failure, Empty, beforeQuery, isEmpty, afterQuery, displayName })
  • 自动根据文件名设置displayName,便于在 React DevTools 中识别;
  • 合法的导出名清单见插件中的EXPECTED_EXPORTS_FROM_CELL常量(beforeQueryQUERYdataisEmptyafterQueryLoadingSuccessFailureEmpty)。

高级示例:自己动手实现一个 Cell

如果没有这些构建期魔法,你该如何自己实现一个 Cell?

以教程中获取 posts 的示例为例:

export const QUERY = gql` query { posts { id title body createdAt } } ` export const Loading = () => <div>Loading...</div> export const Empty = () => <div>No posts yet!</div> export const Failure = ({ error }) => ( <div>Error loading posts: {error.message}</div> ) export const Success = ({ posts }) => { return posts.map((post) => ( <article> <h2>{post.title}</h2> <div>{post.body}</div> </article> )) }

假设 Babel 不会来帮你装配这些导出,你大概率会写出这样的东西:

const QUERY = gql` query { posts { id title body createdAt } } ` const Loading = () => <div>Loading...</div> const Empty = () => <div>No posts yet!</div> const Failure = ({ error }) => ( <div>Error 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> ) }

这是一大段命令式代码——它实际上就是把 createCell.tsx 的内容"倒进"了你的文件里。可以想象,如果每次想获取可能延迟响应的数据都要写一遍,那将是多么痛苦。这正是 Cells 存在的意义。

值得补充的是,仓库中的 createCell.test.tsx 覆盖了上述各分支(加载、成功、空、失败、errorCode计算、props 分发等),而 createSuspendingCell.tsx 则展示了开启流式 SSR(RWJS_ENV.RWJS_EXP_STREAMING_SSR)时的另一套实现:改用useBackgroundQuery+useReadQuery配合<Suspense>CellErrorBoundary完成同样的生命周期分发(createCell.tsx 中根据环境变量在两种工厂间切换)。如果你好奇 Cells 的边界行为与异常路径,直接阅读这两份源码和测试是最佳途径。

  • 后端
  • 前端
  • Web框架
  • 开发工具

【免费下载链接】redwood

RedwoodGraphQL

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

相关推荐

上一篇:Kimi-K3-mlx-mxfp4-6bit-aimer91模型性能评测:6bit量化与传统模型的终极对比
下一篇:PlayCanvas npm 包的 ESM 非打包模块树与 sideEffects 配置对 tree-shaking 有何影响?

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 15:34:54

实时仿真机SimuDev

1&#xff09;产品简介SimuDev实时仿真机产品系列&#xff0c;适用于微秒级步长仿真及测试需求的应用场合。SimuDev是基于多核CPUFPGA架构的高性能实时仿真平台&#xff0c;方便与实际设备连接进行快速原型验证和硬件在环测试。2&#xff09;技术特点提供RS232、RS422、RS485各…

作者头像 李华
网站建设 2026/9/24 15:34:29

工业振动传感器选型12个生死问题:温度、冲击、EMI全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 15:34:14

手撸 SpringBoot 脚手架:用 FreeMarker 模板引擎构建企业级工程框架

文档教程后端 【免费下载链接】CodeGuide :books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总&#xff0c;旨在为大家提供一个清晰详细的学习教程&#xff0c;侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助&#xff0c;请给予支持(关注、…

作者头像 李华