ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Redwood Cells 完全指南:用声明式组件优雅地处理 GraphQL 数据获取

Redwood Cells 完全指南:用声明式组件优雅地处理 GraphQL 数据获取 Redwood Cells 完全指南用声明式组件优雅地处理 GraphQL 数据获取【免费下载链接】redwoodRedwoodGraphQL项目地址: https://gitcode.com/gh_mirrors/re/redwoodRedwood 的 Cells 是一种声明式数据获取模式你只需按约定导出QUERY、Loading、Empty、Failure、Success等具名常量框架就会在构建期用 Babel 插件自动把它们组装成完整组件替你管理 GraphQL 请求的生命周期。本文以官方教程中为博客首页创建 ArticlesCell的实战为主线结合仓库内 createCell.tsx 的实现完整讲解 Cell 的导出约定、生成器用法、状态切换规则、生命周期钩子与 TypeScript 支持读完即可在自己的 Redwood 应用中以最小样板代码实现带加载态、错误态与空数据的列表页。Cell 是什么把数据获取变成导出声明在大多数 Web 应用中数据加载中、请求出错、暂无数据这三种状态几乎是每个列表页、详情页的标配。如果每次都要手写useEffect loading 标志 错误对象 空数据判断代码很快就会被样板逻辑淹没。Redwood 的答案是Cells一种更简单、更声明式的数据获取方式。一个 Cell 是完全自包含的它不依赖父组件传数据而是自己发起 GraphQL 查询、自己根据查询状态渲染对应界面。官方文档将其定义为Redwood 标志性的抽象模式——通过围绕数据获取建立约定Redwood 可以在请求与响应之间介入做查询优化等事情而你无需改动业务代码。一个典型的 Cell 形如export const QUERY gql query FindPosts { 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 key{post.id} h2{post.title}/h2 div{post.body}/div /article )) }当 React 渲染这个组件时Redwood 会执行QUERY在收到响应前显示Loading响应返回后按下面的规则三选一出错 → 渲染Failure数据为空null或空数组[]→ 渲染Empty否则 → 渲染SuccessQUERY与Success是唯二必需的导出。没有导出Empty时空结果会直接交给Success没有导出Failure时错误会输出到控制台。除此之外还有beforeQuery在传给QUERY前加工 props与afterQuery在数据交给Success前加工 GraphQL 返回值两个生命周期钩子。关于 Cell 的完整参考可以阅读 docs/docs/cells.md教程版位于 docs/versioned_docs/version-1.x/tutorial/chapter2/cells.md。什么时候该用 Cell判断标准很简单只要组件需要从数据库或其他可能延迟响应的服务获取数据就适合用 Cell。让 Redwood 去操心什么时候该显示什么你只需要专注于数据到位后最终渲染成什么样这条快乐路径。教程中还强调了一个最佳实践一个 URL 对应一个 Page但数据获取与展示放在 Cell 里。也就是说HomePage 本身不查数据而是把 ArticlesCell 作为子组件渲染出来。这样 Page 保持纯净Cell 负责所有数据逻辑。实战生成你的第一个 CellArticlesCell教程里的博客应用已经通过 scaffold 建立了 Post 模型的 CRUD 界面见 getting-dynamic.md 的 Creating a Post Editor 一节。为了让首页展示文章列表我们生成一个ArticlesCell刻意与 scaffold 的 Posts 区分命名便于记忆yarn rw g cell Articles生成器创建了哪些文件命令会在web/src/components/ArticlesCell/下生成四个文件文件说明ArticlesCell.{js,tsx}Cell 本体ArticlesCell.test.{js,tsx}覆盖各状态的 Jest 测试ArticlesCell.stories.{js,tsx}覆盖各状态的 Storybook storiesArticlesCell.mock.{js,ts}供测试与 Storybook 共用的 mock 数据关于测试与 Storybook 的深入用法见教程 chapter5/storybook.md。生成器本身的实现位于 packages/cli/src/commands/generate/cell/cell.js它依据传入的 name 决定生成单条 Cell还是列表 Cell。生成的初始样板export const QUERY gql query ArticlesQuery { articles { id } } export const Loading () divLoading.../div export const Empty () divEmpty/div export const Failure ({ error }) ( div style{{ color: red }}Error: {error.message}/div ) export const Success ({ articles }) { return ( ul {articles.map((item) { return li key{item.id}{JSON.stringify(item)}/li })} /ul ) }TypeScript 版本额外引入了从types/graphql生成的ArticlesQuery类型以及redwoodjs/web的CellSuccessProps、CellFailureProps工具类型import type { ArticlesQuery } from types/graphql import type { CellSuccessProps, CellFailureProps } from redwoodjs/web export const QUERY gql query ArticlesQuery { articles { id } } export const Loading () divLoading.../div export const Empty () divEmpty/div export const Failure ({ error }: CellFailureProps) ( div style{{ color: red }}Error: {error.message}/div ) export const Success ({ articles }: CellSuccessPropsArticlesQuery) { return ( ul {articles.map((item) { return li key{item.id}{JSON.stringify(item)}/li })} /ul ) }生成器如何判断单条还是列表从 cell.js 的源码可以看到生成器会先判断 name 的单复数如果名字是复数则生成列表 CellcellList.tsx.template单数则生成按 id 查询的详情 Cellcell.tsx.template。详情 Cell 的模板还会自动查询 Prisma schema 以确定主键的名字与类型getIdName、getIdType并生成如query FindPostQuery($id: Int!)的按 id 查询export const QUERY: TypedDocumentNode... gql query ${operationName}($id: ${idType}!) { ${camelName}: ${camelName}(${idName}: $id) { ${idName} } } 对于equipmentpokemon这类单复数同形的单词可以通过--list标志显式指定生成列表 Cellyarn rw g cell equipment --list大小写与命名约定生成 Cell 时可以使用任意大小写风格Redwood 会自动规范化。以下命令都会生成同一个文件web/src/components/BlogArticlesCell/BlogArticlesCell.{js,tsx}yarn rw g cell blog_articles yarn rw g cell blog-articles yarn rw g cell blogArticles yarn rw g cell BlogArticles关键是名字中必须有多词的暗示snake_caseblog_articles、kebab-caseblog-articles、camelCaseblogArticles或 PascalCaseBlogArticles皆可。如果写成yarn rw g cell blogarticles没有多词分隔生成的将是web/src/components/BlogarticlesCell/BlogarticlesCell.{js,tsx}。让 Cell 匹配真实的 GraphQL 字段生成器为了让你尽快跑起来默认假设根查询名与 Cell 名一致。但本教程中 Cell 叫Articles而 SDLapi/src/graphql/posts.sdl.{js,ts}与 Serviceapi/src/services/posts/posts.{js,ts}暴露的根查询叫posts所以需要手动改两处查询名与Success中的 prop 名export const QUERY gql query ArticlesQuery { posts { id } } export const Success ({ posts }) { return ( ul {posts.map((item) { return li key{item.id}{JSON.stringify(item)}/li })} /ul ) }Success 的 prop 从哪里来在Success中posts这个 prop 名的来源就是QUERY里的根查询名——查询叫什么prop 就叫什么。你也可以用 GraphQL 别名alias改变 prop 名export const QUERY gql query ArticlesQuery { articles: posts { id } } 这样Success里可用的就是articles而不是postsexport const Success ({ articles }) { ... }教程最终采用别名方案让 Cell 名与遍历的数据名保持一致并补全title、body、createdAt字段export const QUERY gql query ArticlesQuery { articles: posts { id title body createdAt } } export const Success ({ articles }) { return ( {articles.map((article) ( article key{article.id} header h2{article.title}/h2 /header p{article.body}/p divPosted at: {article.createdAt}/div /article ))} / ) }把 Cell 接入页面Cell 就是一个普通 React 组件直接 import 并渲染即可。在HomePage中加入 ArticlesCellimport { MetaTags } from redwoodjs/web import ArticlesCell from src/components/ArticlesCell const HomePage () { return ( MetaTags titleHome descriptionHome page / ArticlesCell / / ) } export default HomePageTypeScript 版本在 import 类型与 props 注解上略有不同组件用法完全一致。刷新浏览器后首页会显示数据库中所有 post 的id与 GraphQL 特有的__typename字段如果显示 Empty说明数据库还没有数据回到上一节的 scaffold 界面yarn rw g scaffold post生成的 CRUD 页面添加几篇即可。提示如果在 VSCode 中看到Cannot query posts on type Query的报错可以运行yarn rw g types重新生成类型或在命令面板中执行 VSCode GraphQL: Manual Restart 重启 GraphQL 引擎详见 getting-dynamic.md。Cell 的运行机制从声明到组件的组装Cell 的魔法并不神秘Redwood 在构建期通过 Babel 插件识别导出并在运行时调用 createCell.tsx 中的createCell工厂函数把各个具名导出组装成一个真正的 React 组件。核心逻辑如下function createNonSuspendingCell({ QUERY, beforeQuery, afterQuery, isEmpty, Loading, Failure, Empty, Success, displayName }) { function NamedCell(props) { const { children: _, ...variables } props const options beforeQuery(variables) const query typeof QUERY function ? QUERY(options) : QUERY let { error, loading, data, ...queryResult } useQuery(query, options) if (error) { if (Failure) { return Failure error{error} errorCode{...} {...props} updating{loading} queryResult{queryResult} / } else { throw error } } else if (data) { const afterQueryData afterQuery(data) if (isEmpty(data, { isDataEmpty }) Empty) { return Empty {...props} {...afterQueryData} updating{loading} queryResult{queryResult} / } else { return Success {...props} {...afterQueryData} updating{loading} queryResult{queryResult} / } } else if (loading) { return Loading {...props} queryResult{queryResult} / } else { throw new Error(Cannot render Cell: ...query succeeded but data is null...) } } return (props) NamedCell {...props} / }从源码可以看到几个值得注意的实现细节默认beforeQuery把父组件传入的 props 全部当作 GraphQL 变量并设置fetchPolicy: cache-and-network、notifyOnNetworkStatusChange: true——即有缓存先渲染缓存同时后台刷新的 stale-while-revalidate 行为updating就是loading的重命名默认 fetch policy 下刷新数据时data仍在、loading为 true此时会渲染Success并带上updating标志方便你在列表顶部画一个小 spinner类型注释见 cellTypes.tsFailure未导出时错误会被重新抛出而不是静默吞掉渲染状态机是错误优先、数据次之、加载兜底只要拿到data即使loading为 true 也渲染Success。关于状态切换的边界行为createCell.test.tsx 中有大量测试佐证例如数据字段为null且存在Empty时渲染 Empty数据字段为空数组时渲染 Empty没有Empty时空数据也会交给SuccessCell 的 props 会作为变量传给查询没有Failure时错误被抛出可以自定义isEmpty也可以与默认实现组合。默认的isEmpty判断实现在 isCellEmpty.ts当data不存在或所有根字段都为null/ 空数组时认为数据为空。注意是所有字段——只要有一个根字段有值就不会渲染 Empty对应的多根查询测试见 createCell.test.tsx。另外Cell 并非绑定死 ApollouseQuery来自GraphQLHooksProvider见 GraphQLHooksProvider.tsx用户可以替换成自己的 GraphQL 客户端而保持 Cell 兼容。七个导出与生命周期钩子详解完整版 Cells 文档docs/docs/cells.md将 Cell 的导出扩展到了七个名称类型说明QUERYstring, function要执行的查询beforeQueryfunction生命周期钩子准备查询变量与选项isEmptyfunction生命周期钩子决定是否渲染 EmptyafterQueryfunction生命周期钩子清洗查询返回的数据Loadingcomponent请求进行中时渲染Emptycomponent没有数据null或[]时渲染Failurecomponent出错时渲染Successcomponent数据加载完成后渲染除显示正确组件外Cell 还会把正确的 props 分发给正确的组件四个状态组件都能拿到父组件传入的 props 与queryResultuseQuery返回值中除loading、error、data之外的部分Empty和Success额外获得查询数据与updatingFailure独享error与errorCode。QUERY字符串或函数QUERY可以是字符串也可以是返回合法 GraphQL 文档的函数由beforeQuery的返回值调用源码见 createCell.tsx。它还支持多个根查询export const QUERY gql query { posts { id title } authors { id name } } 此时posts和authors都会出现在Success中export const Success ({ posts, authors }) { ... }查询变量来自父组件传入的 props——beforeQuery默认就是把 props 当变量。例如父组件渲染BlogPostsCell numberToShow{3} /Cell 内即可使用$numberToShowexport const QUERY gql query ($numberToShow: Int!) { posts(numberToShow: $numberToShow) { id title } } 换句话说可以从 SDL 倒推 Cell 的 propsSDL 里定义了哪些变量Cell 的 props 就该是哪些。beforeQuery配置 useQuerybeforeQuery本质上是配置底层useQuery钩子Apollo 语境下即useQuery的 options的机会。默认实现等价于export const beforeQuery (props) { return { variables: props, fetchPolicy: cache-and-network, } }比如想开启 Apollo 的轮询并禁用缓存export const beforeQuery (props) { return { variables: props, fetchPolicy: no-cache, pollInterval: 2500 } }它还可以从非 props 来源Context、全局状态、useAuth()等补充变量。注意一旦你提供了beforeQueryCell 的 props 类型会自动与函数第一个参数对齐——函数无参数则 Cell 不接受 props// 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 wordabc export const beforeQuery ({ word }: { word: string }) { return { variables: { magicWord: word }, } }isEmpty自定义空的判定isEmpty接收两个参数data以及包含默认实现isDataEmpty的对象便于在默认逻辑上扩展export const isEmpty (data, { isDataEmpty }) { return isDataEmpty(data) || data?.blog?.status hidden }afterQuery数据到达 Success 前的最后一站afterQuery在数据交给Success前运行适合做数据清洗默认原样返回export const afterQuery (data) { return { ...data, posts: data.posts.map((post) ({ ...post, title: post.title.toUpperCase() })), } }Failure 与 errorCode想在错误组件里区分不同错误可以用errorCode它来自查询结果或 GraphQL 错误的extensions.code见 createCell.tsxexport const Failure ({ error, errorCode }: CellFailureProps) { return ( div style{{ color: red }} {errorCode NO_CONFIG ? h1NO_CONFIG/h1 : h1ERROR/h1} Error: {error.message} - Code: {errorCode} /div ) }Success 的数据展开Success并不直接拿到data属性——Redwood 会把data展开后注入Success让你直接解构查询结果// Redwood 允许直接这样写 export const Success ({ posts, authors }) { ... } // 而不是 export const Success ({ data }) { const { posts, authors } data ... }Success仍是普通 React 组件其它自定义 props 照常透传。Cell 中的 TypeScript工具类型生成器产出的 Cell 自带完整类型。核心工具类型定义在 packages/web/src/components/cell/cellTypes.ts更详细的说明见 docs/docs/typescript/utility-types.mdCellSuccessPropsTData, TVariables为Success的 props 提供类型。TData是查询返回的数据类型通常从types/graphql导入TVariables是查询变量类型。它不仅标注查询数据还标注 ApollouseQuery返回的变量与方法import type { FindBlogPostQuery, FindBlogPostQueryVariables } from types/graphql import type { CellSuccessProps } from redwoodjs/web type SuccessProps CellSuccessProps FindBlogPostQuery, FindBlogPostQueryVariables export const Success ({ blogPost, // 来自查询类型完备 queryResult, // 来自 Apollo同样有类型 }: SuccessProps) { ... }CellFailurePropsTVariables为Failure提供类型可选泛型TVariables可用于输出类似Couldnt load data for ${variables.searchTerm}的错误文案export const Failure ({ error, variables, }: CellFailurePropsFindBlogPostQueryVariables) ( divCouldnt load data for {variables.searchTerm}/div )CellLoadingPropsTVariables为Loading提供类型用法与CellFailureProps类似。一个仓库内的真实示例是测试项目的 PostsCell.tsx可以看到TypedDocumentNode、CellFailurePropsFindPosts、CellSuccessPropsFindPosts, FindPostsVariables的完整组合。生成器附属产物测试与 Storybook生成的.mock.js文件定义了standard()函数为测试与 Storybook 提供统一 mock 数据// Define your own mock data here: export const standard (/* vars, { ctx, req } */) ({ articles: [ { __typename: Article as const, id: 42 }, { __typename: Article as const, id: 43 }, { __typename: Article as const, id: 44 }, ] })生成的测试模板见 packages/cli/src/commands/generate/cell/templates/test.js.template分别渲染Loading、Empty、Failure注入new Error(Oh no)与Success注入standard()的 mock 数据并断言不抛错。Storybook 模板stories.tsx.template则为四种状态各生成一个 story。得益于 mock 数据你不需要故意断网或写坏查询就能在 Storybook 里单独打磨Loading与Failure的 UI。进阶视角如果自己实现一个 Cell最后用一个假如没有 Babel 插件Cell 会是什么样的例子来收尾——它恰好就是 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 }) { /* ... */ } 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 ) }这段命令式代码就是 Cell 抽象帮你省掉的样板。另外判断一个文件是不是 Cell 只靠文件名以 Cell 结尾即会被 Redwood 识别但如果文件没有导出名为QUERY的常量、且存在默认导出它会被跳过参见 docs/docs/cells.md 的 How Does Redwood Know a Cell is a Cell? 一节。小结回到教程从空项目到首页展示博客文章我们走过的路径是生成首页 → 生成布局 → 定义数据库 schema → 迁移建表 → scaffold 出 CRUD 界面 →创建 Cell 处理加载/空/失败/成功四态→ 把 Cell 挂到页面。这最后几步会成为你构建 Redwood 应用时的标准动作用yarn rw g cell生成骨架调整QUERY与Success匹配真实的 SDL 字段必要时用别名统一 prop 名再通过beforeQuery/afterQuery/isEmpty微调生命周期行为。整个过程中数据从数据库到页面的管道工程都由 Redwood 代劳了。【免费下载链接】redwoodRedwoodGraphQL项目地址: https://gitcode.com/gh_mirrors/re/redwood创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表