后端前端Web框架开发工具【免费下载链接】redwoodRedwoodGraphQL项目地址https://gitcode.com/gh_mirrors/re/redwood点击查看免费下载本指南基于 RedwoodJS 官方教程第六章 Multiple Comments见 multiple-comments.md完整演示如何为博客文章构建评论列表功能从yarn rw g cell Comments生成 Cell到用 Storybook 的standardmock 独立开发 UI再到挂载进文章详情页并补齐 Jest 组件测试。读完你将掌握 RedwoodJS Cell 模式的完整工作流数据获取与展示自治的设计动机、Cell 四种渲染状态的测试方法以及waitFor处理异步渲染的实战细节——全程不依赖任何后端接口仅凭 mock 数据即可完成整个前端功能。为什么评论列表要用一个 Cell在教程当前阶段博客首页只展示每篇文章的摘要summary用户需要进入文章详情页才能看到完整正文。因此评论列表显然应该出现在文章详情页上。但详情页目前的 GraphQL 查询只取了单个Post的数据并没有评论。既然我们既要获取评论数据、又要展示评论这正是 RedwoodJSCell的典型应用场景。也许你会问能不能让文章页的查询把评论一起查出来当然可以但 Cell 的设计哲学在于让组件更加可组合每个 Cell 自己负责数据获取与展示。如果让Article页面去获取评论那么新的Comments组件就必须依赖别人把评论数据传进来一旦这个组件在别处被复用就不得不在两个地方重复获取评论。把数据获取内聚到组件自身组件才能被随意搬移、复用而不破坏数据流。还有两个常见追问值得一并说清上一节做好的Comment单条组件为什么不让它自己取数据因为几乎没有单独展示一条评论的场景——评论总是以某篇文章下的全部评论形式出现。如果你确实需要单条展示完全可以把它改造成CommentCell让它自取数据。但要权衡假设一篇文章有 50 条评论每个CommentCell发一次 GraphQL 请求页面就要发出 50 个请求。任何架构选择都有代价。既然最终都要在CommentsCell里展示为什么还要单独做一个Comment组件一方面教程希望循序渐进另一方面把 UI 拆成更小、更易推理的块本来就更利于维护和团队协作。生成 CommentsCellyarn rw g cell Comments在项目根目录执行yarn rw g cell CommentsStorybook 中立即多出一个Cells/CommentsCell而且它真的显示了内容。这些内容从哪来的来自生成的CommentsCell.mock.{js,ts}文件。此时项目里还没有Comment的 Prisma 模型Redwood 的 cell 生成器便猜测你的模型至少会有一个id字段并用它生成了 mock 数据。从生成器源码可以印证这个猜测行为。查看 packages/cli/src/commands/generate/cell/cell.js生成器会尝试通过getSchema()读取 Prisma 模型来确定idName/idType读取失败例如模型尚未创建时会吞掉错误并把idType默认设为Int同时使用内置的mockIdValues [42, 43, 44]生成三条假数据。这也解释了为什么 mock 模板 mockList.ts.template 里只有id字段。另外注意生成器的几个细节同一文件 cell.jsComments本身是复数生成器会判定为列表型 CellshouldGenerateList强制复数化并套用cellList模板从而生成查询comments而非单条comment操作名operation name会调用uniqueOperationName保证全局唯一你也可以通过--query参数强制指定必须是唯一的当项目已存在对应 SDL 时生成器会顺带执行类型生成generateTypes()否则会提示先运行yarn rw g sdl。查看列表型 Cell 的模板 cellList.tsx.template默认生成的QUERY只查询id一个字段Success也只是把每条数据JSON.stringify打印出来——这正是教程接下来要改造的起点。让 CommentsCell 渲染 Comment 组件并补全查询字段打开生成的CommentsCell导入上一节创建的Comment组件并把Comment渲染所需的name、body、createdAt字段全部补进QUERYJavaScript 版本web/src/components/CommentsCell/CommentsCell.jsximport Comment from src/components/Comment export const QUERY gql query CommentsQuery { comments { id name body createdAt } } 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 ({ comments }) { return ( {comments.map((comment) ( Comment key{comment.id} comment{comment} / ))} / ) }TypeScript 版本web/src/components/CommentsCell/CommentsCell.tsximport Comment from src/components/Comment import type { CommentsQuery } from types/graphql import type { CellSuccessProps, CellFailureProps } from redwoodjs/web export const QUERY gql query CommentsQuery { comments { id name body createdAt } } 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 ({ comments }: CellSuccessPropsCommentsQuery) { return ( {comments.map((comment) ( Comment key{comment.id} comment{comment} / ))} / ) }注意两点在map遍历数组时为每个Comment传了key{comment.id}这是 React 渲染列表的要求TypeScript 版本使用redwoodjs/web导出的CellSuccessProps、CellFailureProps泛型以及types/graphql中由 SDL 生成的CommentsQuery类型——目前后端 SDL 还不存在类型生成会延后到后端就绪时完成。回到 Storybook你会看到Comment组件确实被渲染了 3 次但因为 mock 数据里只有id页面是空的。接下来补上真实的 mock 数据。用 standard mock 填充演示数据更新CommentsCell.mock.{js,ts}为comments提供两条完整数据JavaScript 版本web/src/components/CommentsCell/CommentsCell.mock.jsexport const standard () ({ comments: [ { id: 1, name: Rob Cameron, body: First comment, createdAt: 2020-01-02T12:34:56Z, }, { id: 2, name: David Price, body: Second comment, createdAt: 2020-02-03T23:00:00Z, }, ], })TypeScript 版本web/src/components/CommentsCell/CommentsCell.mock.tsexport const standard () ({ comments: [ { id: 1, name: Rob Cameron, body: First comment, createdAt: 2020-01-02T12:34:56Z, }, { id: 2, name: David Price, body: Second comment, createdAt: 2020-02-03T23:00:00Z, }, ], })standard是什么它是这个 Cell 的标准默认 mock。如果不在测试/Storybook 中做额外设置就使用这份数据。取名standard而非default是因为default在 JavaScript 中是保留字。刷新 Storybook两条评论就显示出来了。不过两条评论紧挨着、难以区分——既然CommentsCell负责绘制多条评论那么评论之间的间距也应由它统一管理。用 space-y-8 统一评论列表的间距给Success的外层容器加上 Tailwind 的space-y-8export const Success ({ comments }) { return ( div classNamespace-y-8 {comments.map((comment) ( Comment comment{comment} key{comment.id} / ))} /div ) }为什么用space-y-8而不是给每条Comment单独加上下 marginspace-y-8只在元素之间插入间距不会在整组元素的最上方和最下方多出空隙而给每个Comment加mt-8/mb-8会在列表两端产生多余的空白。这是 Tailwind 布局的经典技巧。把 CommentsCell 挂载到文章详情页现在把CommentsCell放进实际的博客文章展示组件ArticleJavaScript 版本web/src/components/Article/Article.jsximport { Link, routes } from redwoodjs/router import CommentsCell from src/components/CommentsCell const truncate (text, length) { return text.substring(0, length) ... } const Article ({ article, summary false }) { return ( article header h2 classNametext-xl text-blue-700 font-semibold Link to{routes.article({ id: article.id })}{article.title}/Link /h2 /header div classNamemt-2 text-gray-900 font-light {summary ? truncate(article.body, 100) : article.body} /div {!summary CommentsCell /} /article ) } export default ArticleTypeScript 版本web/src/components/Article/Article.tsximport { Link, routes } from redwoodjs/router import CommentsCell from src/components/CommentsCell import type { Post } from types/graphql const truncate (text: string, length: number) { return text.substring(0, length) ... } interface Props { article: OmitPost, createdAt summary?: boolean } const Article ({ article, summary false }: Props) { return ( article header h2 classNametext-xl text-blue-700 font-semibold Link to{routes.article({ id: article.id })}{article.title}/Link /h2 /header div classNamemt-2 text-gray-900 font-light {summary ? truncate(article.body, 100) : article.body} /div {!summary CommentsCell /} /article ) } export default Article核心逻辑一行如果不是摘要模式summary为 false就渲染CommentsCell。到 Storybook 里查看Article的Full和Summary两个 story可以看到前者有评论、后者没有。等等Article本身不是 Cell它内部渲染的CommentsCell真的会发出 GraphQL 请求吗为什么在 Storybook 里能正常显示这得益于 RedwoodJS 为 Storybook 注入的能力当被测试/展示的组件本身不是 Cell却渲染了某个 Cell时Storybook 会自动拦截 GraphQL 请求并使用该 Cell 对应的standardmock。底层实现见 packages/storybook/src/mocks/StorybookProvider.tsxMockingLoader通过import.meta.glob预加载所有*.mock.{js,ts}文件随后启动 MSWstartMSW(browsers)并注册请求处理器setupRequestHandlers()使每个 Cell 的standardmock 在渲染时即被命中。这也是教程强调前端 UI 完全可以与后端并行开发的技术基础。评论挂进文章后又暴露出一个视觉问题评论紧贴在文章正文下方。再包一层mt-12拉开间距{!summary ( div classNamemt-12 CommentsCell / /div )}TypeScript 版本改动完全相同。为什么真实页面会报错——后端尚未就绪如果你此时访问真实站点评论位置会报错。原因正如本节标题所说我们从一开始就只做了CommentsCell从未在schema.prisma中创建 Comment 模型也没有创建 SDL 和 service。后端就绪后添加模型、yarn rw prisma migrate dev、生成 SDL/service这个错误会自然消失——相关操作正是教程下一节 comments-schema.md 的内容。这个先报错的过程恰恰体现了 Storybook 的另一大收益UI 开发可以与 api 侧完全隔离。web 团队可以在后端尚未动工时就把界面做完、测完api 团队同时开发后端互不阻塞。测试CommentsCell 与 Article我们新增了CommentsCell、修改了Article该测什么、在哪里测测试 CommentsCellComment组件本身承担了大部分渲染工作它的功能已在Comment自己的测试里覆盖无需在CommentsCell中重复。CommentsCell独有的职责是有加载中Loading提示有空数据Empty提示有失败Failure提示渲染成功时输出与QUERY返回数量一致的评论具体渲染成什么样交给Comment的测试。生成器默认生成的测试 test.js.template 已经覆盖了全部四种状态——虽然只是最基础的不抛错断言import { render } from redwoodjs/testing/web import { Loading, Empty, Failure, Success } from ./CommentsCell import { standard } from ./CommentsCell.mock describe(CommentsCell, () { it(renders Loading successfully, () { expect(() { render(Loading /) }).not.toThrow() }) it(renders Empty successfully, async () { expect(() { render(Empty /) }).not.toThrow() }) it(renders Failure successfully, async () { expect(() { render(Failure error{new Error(Oh no)} /) }).not.toThrow() }) it(renders Success successfully, async () { expect(() { render(Success comments{standard().comments} /) }).not.toThrow() }) })TypeScript 版本除导入类型外完全相同。千万别小看这套冒烟测试React 组件要么 100% 正常工作、要么直接炸掉这种测试能保证能渲染不抛错失败时能立刻抓住问题。在此基础上我们还可以更进一步更新Success的测试断言传入多少条评论就渲染多少条。怎么判断一条评论被渲染了检查每条评论最重要的部分——comment.body——是否出现在屏幕上import { render, screen } from redwoodjs/testing/web import { Loading, Empty, Failure, Success } from ./CommentsCell import { standard } from ./CommentsCell.mock describe(CommentsCell, () { it(renders Loading successfully, () { expect(() { render(Loading /) }).not.toThrow() }) it(renders Empty successfully, async () { expect(() { render(Empty /) }).not.toThrow() }) it(renders Failure successfully, async () { expect(() { render(Failure error{new Error(Oh no)} /) }).not.toThrow() }) it(renders Success successfully, async () { const comments standard().comments render(Success comments{comments} /) comments.forEach((comment) { expect(screen.getByText(comment.body)).toBeInTheDocument() }) }) })这里循环遍历 mock 中的每条评论做断言——mock 数据来自与 Storybook 同一份standard将来往 mock 里加数据时测试自动覆盖。千万不要写死正好有两条评论之类的断言当时能过可一旦为了在 Storybook 里尝试不同形态而往 mock 里加数据测试就挂了。避免在测试里硬编码数据尤其是魔法数字magic number尽量从 mock 数据推导。测试 ArticleArticle新增的行为是非摘要模式下显示评论。关于全篇和摘要两种渲染我们已经各有一个测试了。好的测试习惯是一个测试只验证一件事——如果测试描述里出现and比如renders a blog post and its comments多半该拆成两个测试。为新增功能补两个测试import { render, screen, waitFor } from redwoodjs/testing import { standard } from src/components/CommentsCell/CommentsCell.mock import Article from ./Article const ARTICLE { id: 1, title: First post, body: Neutra tacos hot chicken prism raw denim, put a bird on it enamel pin post-ironic vape cred DIY. Street art next level umami squid. Hammock hexagon glossier 8-bit banjo. Neutra la croix mixtape echo park four loko semiotics kitsch forage chambray. Semiotics salvia selfies jianbing hella shaman. Letterpress helvetica vaporware cronut, shaman butcher YOLO poke fixie hoodie gentrify woke heirloom., createdAt: new Date().toISOString(), } describe(Article, () { it(renders a blog post, () { render(Article article{ARTICLE} /) expect(screen.getByText(ARTICLE.title)).toBeInTheDocument() expect(screen.getByText(ARTICLE.body)).toBeInTheDocument() }) it(renders comments when displaying a full blog post, async () { const comment standard().comments[0] render(Article article{ARTICLE} /) await waitFor(() expect(screen.getByText(comment.body)).toBeInTheDocument() ) }) it(renders a summary of a blog post, () { render(Article article{ARTICLE} summary{true} /) expect(screen.getByText(ARTICLE.title)).toBeInTheDocument() expect( screen.getByText( Neutra tacos hot chicken prism raw denim, put a bird on it enamel pin post-ironic vape cred DIY. Str... ) ).toBeInTheDocument() }) it(does not render comments when displaying a summary, async () { const comment standard().comments[0] render(Article article{ARTICLE} summary{true} /) await waitFor(() expect(screen.queryByText(comment.body)).not.toBeInTheDocument() ) }) })TypeScript 版本相同仅涉及类型标注差异。这里有三个值得注意的实战要点跨组件导入 mock 完全合法这里从src/components/CommentsCell/CommentsCell.mock导入standard而不是从Article自己的 mock 导入——mock 只是普通模块随处可复用。waitFor的用途Article渲染了CommentsCell后者内部要等 GraphQL被 mock 拦截返回后才会渲染Success。waitFor会等待这类异步操作完成后再执行断言。这正是redwoodjs/testing提供waitFor的原因。摘要模式的测试也要waitFor摘要版Article本就不渲染CommentsCell为什么还要等设想一下如果将来有人误把CommentsCell加进摘要版而测试没有等待就会出现假阳性——断言时评论还没渲染出来还停留在Loading于是文本不在页面上恰好通过。等待之后评论 body 真的渲染出来测试才会正确地失败。深入理解Cell 运行时的四种状态机教程中的Loading/Empty/Failure/Success四个导出并不是魔法而是由 Cell 运行时统一驱动的。查看 packages/web/src/components/cell/createCell.tsx 可以看清这套状态机查询返回error且有Failure时渲染Failure并额外传入errorCode来自 GraphQL 扩展字段extensions.code若未定义Failure则直接抛出错误返回data且经isEmpty判定为空、同时定义了Empty时渲染Empty返回data且非空时渲染Success并把afterQuery(data)的结果与props、updating、queryResult一并传入仍在加载时渲染Loading若出现无 error、无 data、不在 loading的异常状态通常是缓存问题则抛出明确错误并提示给查询的所有字段加上id可能解决该问题。beforeQuery的默认实现还会设置fetchPolicy: cache-and-network与notifyOnNetworkStatusChange: true见 createCell.tsx这也是 Cell 能在数据更新时自动刷新的底层原因。理解了这套状态机你就能放心地在beforeQuery/afterQuery/isEmpty上做定制。总结与下一步至此多评论列表的前端已完成用yarn rw g cell Comments生成 Cell理解生成器对复数/单数、mock 字段的推断逻辑让CommentsCell复用Comment组件并补全查询字段用standardmock 在 Storybook 中独立演示用space-y-8、mt-12打磨布局并挂载进Article为CommentsCell与Article补齐状态渲染与异步渲染测试。接下来的自然步骤是补齐后端在 schema.prisma 中添加Comment模型并建立与Post的关联生成 SDL 与 service让真实页面上的评论列表真正跑起来随后再实现评论的创建表单见 comment-form.md。想继续深挖 Cell 机制可以直接阅读 createCell.tsx、Storybook mock 预加载 与 MSW 请求拦截 的源码实现。赞分享后端前端Web框架开发工具【免费下载链接】redwoodRedwoodGraphQL项目地址https://gitcode.com/gh_mirrors/re/redwood点击查看免费下载相关推荐Redwood 教程实战用 Cell 构建博客评论列表CommentsCell 的创建、Mock 与测试Redwood 教程实战用 Cell 构建博客评论列表CommentsCell 的创建、Mock 与测试 本篇技术指南以 Redwood 官方教程「Mul后端前端Web框架开发工具Redwood 教程用 Cell 构建评论列表CommentsCell——从 Storybook 开发到组件测试全流程Redwood 教程用 Cell 构建评论列表CommentsCell——从 Storybook 开发到组件测试全流程 本篇教程是 Redwood 官方教后端前端Web框架开发工具Redwood 实战用 Cell 为博客文章渲染多条评论CommentsCell 完整实践Redwood 实战用 Cell 为博客文章渲染多条评论CommentsCell 完整实践 本篇教程对应 Redwood 官方教程第 6 章「Multip后端前端Web框架开发工具上一篇3秒解析500页PDFollama-python智能文档助手从0到1构建指南下一篇SQLFluff终极指南如何轻松维护700 dbt模型的最佳实践创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考