- 后端
- 前端
- Web框架
- 开发工具
【免费下载链接】redwood
RedwoodGraphQL
导读
Cells 是 Redwood 最具标志性的数据获取抽象:它用一套命名导出约定(QUERY、Loading、Empty、Failure、Success)把 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。 - 手动指定列表:对于单复数同形的不可数/不规则词(如
equipment、pokemon),可以显式传入--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 导出了五个核心常量:QUERY、Loading、Empty、Failure和Success。QUERY中的根查询默认与<name>同名,这样当你基于schema.prisma中的模型生成 Cell 时,可以立刻从数据库拿到数据。但多数情况下你不会这样直接使用生成结果,因此务必按需修改根查询。
完整用法:七种导出
一个 Cell 总共可以有七种导出,各司其职:
| 名称 | 类型 | 说明 |
|---|---|---|
QUERY | string, function | 要执行的查询 |
beforeQuery | function | 生命周期钩子;为查询准备 variables 与 options |
isEmpty | function | 生命周期钩子;决定 Cell 是否渲染Empty |
afterQuery | function | 生命周期钩子;净化查询返回的数据 |
Loading | component | 请求进行中时渲染的组件 |
Empty | component | 无数据(null或[])时渲染的组件 |
Failure | component | 出错时渲染的组件 |
Success | component | 数据加载完成后渲染的组件 |
只有QUERY和Success是必需的。若未导出Empty,空结果会直接交给Success;若未导出Failure,错误只会输出到控制台(源码中对应 createCell.tsx:有Failure则渲染,否则直接throw error交由上层错误边界处理)。
除"在正确时机渲染正确组件"外,Cells 还会把正确的 props 分发给正确的组件:
Loading、Empty、Failure、Success都能以常规 React 方式访问父级传入的 props,并能拿到useQuery返回值的大部分内容(作为一个名为queryResult的 prop)。Empty与Success额外获得查询返回的data,以及一个表示 Cell 当前是否正在拉取新数据的updating布尔值(源码中updating={loading},即把loading重命名后暴露给用户,便于渲染"后台刷新中"的指示器)。Failure也拥有updating,并且独占error与errorCode两个 props。
useQuery的确切返回值以 Apollo Client 的 API 文档为准;注意error与data在 Cell 中享受了特殊处理。
QUERY
QUERY可以是字符串或函数;如果是函数,必须返回一个合法的 GraphQL 文档。
一个 Cell 完全支持包含多个根查询。例如:
export const QUERY = gql`{ query { posts { id title } authors { id name } } }此时posts与authors都会传给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 HomePageexport 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:!data或Object.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能拿到error与errorCode。其中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)。
所以,如果查询posts和authors,无需这样写:
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,因为一个模块只能有一个默认导出)以及是否导出了QUERY或data,满足条件才会继续装配。
具体的构建期装配过程(同样在 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常量(beforeQuery、QUERY、data、isEmpty、afterQuery、Loading、Success、Failure、Empty)。
高级示例:自己动手实现一个 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
相关推荐
Redwood Cells 完全指南:用声明式模式接管 GraphQL 数据获取的生命周期
Redwood Cells 完全指南:用声明式模式接管 GraphQL 数据获取的生命周期 Cells 是 Redwood 框架最具辨识度的抽象之一:它以声明式
后端前端Web框架开发工具Redwood Cells 深度指南:声明式 GraphQL 数据获取的生命周期管理
Redwood Cells 深度指南:声明式 GraphQL 数据获取的生命周期管理 导读 Cells 是 Redwood 框架最具标志性的抽象之一:它以纯声明
后端前端Web框架开发工具Redwood Cells 完全指南:声明式数据获取与查询生命周期管理
Redwood Cells 完全指南:声明式数据获取与查询生命周期管理 Cells 是 Redwood 框架最具标志性的抽象之一,它以声明式方式封装 Graph
后端前端Web框架开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考