首页
/ RealWorld(Conduit)实现功能清单详解:从 JWT 认证到文章、评论、收藏与关注

RealWorld(Conduit)实现功能清单详解:从 JWT 认证到文章、评论、收藏与关注

2026-09-04 14:44:27作者:余洋婵Anita

本文以 RealWorld 仓库中面向"实现者"的功能清单 features.md 为核心,逐条拆解 Conduit(一个 Medium.com 风格的博客应用)实现必须满足的七项通用功能:JWT 用户认证、用户 CRU、文章 CRUD、评论 CR-D、分页文章列表、收藏与关注。读完本文,你将掌握每项功能对应的 API 端点、请求/响应契约、错误码约定,以及仓库中 Hurl/Bruno API 测试与 Playwright e2e 测试对每项功能的验证方式。

1. 功能总览:Conduit 实现的"最小功能集"

features.md 给出的完整功能清单如下(CRUD 缩写中缺省的字母表示"不需要实现"):

功能 覆盖操作 说明
Authenticate users via JWT 认证 登录/注册页面 + 设置页上的登出按钮
CRU users 创建/读取/更新 通过注册页与设置页操作,无需删除用户
CRUD Articles 全量 文章的增删改查
CR-D Comments 创建/读取/删除 文章下评论,无需更新评论
GET paginated lists 读取 获取并展示带分页的文章列表
Favorite articles 收藏 收藏/取消收藏文章
Follow other users 关注 关注/取消关注其他用户

这份清单是"实现 Conduit"这一命题的验收标准:它位于文档站 implementation-creation 章节(introductionexpectations 的同级文件),与后端端点规范 endpoints.md、响应格式规范 api-response-format.md 共同构成完整契约。下文逐条展开。

2. JWT 用户认证:登录、注册与登出

2.1 两个认证入口端点

对应功能项 "Authenticate users via JWT (login/signup pages + logout button on settings page)",认证由两个免鉴权的 POST 端点完成:

  • POST /api/users/login(登录):请求体 {"user": {"email": "...", "password": "..."}},必填字段 emailpassword
  • POST /api/users(注册):请求体在 user 对象中额外携带 username,必填字段为 emailusernamepassword

两者均返回 user 对象,其中包含 JWT token。返回结构见 api-response-format.md

{
  "user": {
    "email": "jake@jake.jake",
    "token": "jwt.token.here",
    "username": "jake",
    "bio": null,
    "image": null
  }
}

2.2 JWT 的传输与存储约定

  • 请求头格式:后续所有需要鉴权的请求,token 通过请求头传递,格式为 Authorization: Token jwt.token.here(见 endpoints.md)。
  • 前端存储:前端路由规范 routing.md 要求登录/注册页(/login/register)"使用 JWT(token 存入 localStorage)",并明确该认证方式可以方便地切换为 session/cookie 机制。

2.3 登出与会话持久性(e2e 测试佐证)

功能清单把"设置页上的登出按钮"列为认证功能的组成部分。仓库的 Playwright 套件 specs/e2e/auth.spec.ts 对认证全链路做了验证:

  • 注册成功后跳转回首页,导航栏出现用户头像链接,且可进入 /editorauth.spec.ts);
  • 错误密码登录必须展示 .error-messages 错误区且停留在 /loginauth.spec.ts);
  • 页面刷新后会话必须保持(localStorage 中的 token 仍有效)(auth.spec.ts);
  • 一个值得注意的健壮性要求:当 localStorage 中残留一个无效 token 时,刷新页面不得白屏,应用应回落到未认证 UI,且该无效 token 应被清除(auth.spec.ts)。

登出动作本身由测试辅助模块 specs/e2e/helpers/auth.ts 提供的 logout 方法执行,其效果验证为:登出后 /login 链接重新可见、profile 链接消失(auth.spec.ts)。

3. 用户 CRU:注册 + 设置页(无删除)

功能清单明确用户操作只有 C、R、U 三项:"sign up & settings page - no deleting required"。

3.1 涉及的端点

  • 创建:POST /api/users(即 2.1 节的注册端点);
  • 读取当前用户:GET /api/user(需鉴权,返回当前用户的 user 对象);
  • 更新:PUT /api/user(需鉴权),接受的字段为 emailusernamepasswordimagebio(见 endpoints.md)。

更新请求示例:

{
  "user": {
    "email": "jake@jake.jake",
    "bio": "I like to skateboard",
    "image": "https://i.stack.imgur.com/xHWG8.jpg"
  }
}

3.2 API 测试套件验证的更新语义

specs/api/bruno/errors-auth/ 目录下的用例把 PUT /api/user 的边界语义固化成了可执行契约,例如:

3.3 前端侧验证(设置页)

specs/e2e/settings.spec.ts 从 UI 角度覆盖了设置页的更新能力:仅更新 biosettings.spec.ts)、仅更新 image、同时更新两个字段、更新后导航到 /profile/:username 展示新值,以及更新后导航栏用户名不被破坏、可再次进入设置页继续编辑。该套件同时支持两种运行模式:纯 SPA(浏览器直连 API,断言 PUT /api/user 的响应体)与 fullstack(表单 POST 后断言渲染结果),由配置项 BROWSER_API 切换。

4. 文章 CRUD:功能清单中唯一"全量"的资源

4.1 四个端点与字段约束

操作 端点 鉴权 关键约束
创建 POST /api/articles 需要 必填 titledescriptionbody;可选 tagList(字符串数组)
读取 GET /api/articles/:slug 不需要 返回单篇文章
更新 PUT /api/articles/:slug 需要 可选 titledescriptionbody
删除 DELETE /api/articles/:slug 需要

端点细节见 endpoints.md。创建请求示例:

{
  "article": {
    "title": "How to train your dragon",
    "description": "Ever wonder how?",
    "body": "You have to believe",
    "tagList": ["reactjs", "angularjs", "dragons"]
  }
}

两个容易被实现者忽略的细节:

  1. slug 随标题联动:更新时若修改了 titleslug 也要随之更新。规范同时强调 slug 只需是唯一字符串,重复标题必须产生不同的 slug(如 kebab-case 派生);
  2. 权限边界:更新/删除仅作者可操作。specs/api/bruno/errors-authorization/ 用两个用户验证了这一点——用户 B 删除/更新用户 A 的文章应得到 403,且文章必须仍然存在(04-user-b-tries-to-delete-403.bru05-user-b-tries-to-update-403.bru)。

4.2 前端要求与测试佐证

前端路由规范要求:文章页 URL 为 /article/:slug删除文章按钮仅对作者显示;编辑器页 URL 为 /editor(新建)与 /editor/:slug(编辑);文章正文 Markdown 由服务端下发、客户端渲染(routing.md)。e2e 套件中,文章创建是评论、社交、导航等用例的前置动作(如 social.spec.ts 创建两篇文章后在 profile 页断言其可见),未登录用户直接访问 /editor 会被重定向离开(auth.spec.ts)。

5. 评论 CR-D:可增、可查、可删,不可改

5.1 端点定义

  • 添加:POST /api/articles/:slug/comments,需鉴权,请求体 {"comment": {"body": "..."}},必填字段 body
  • 列表:GET /api/articles/:slug/comments,鉴权可选,返回 comments 数组(注意该响应不携带计数,结构见 api-response-format.md);
  • 删除:DELETE /api/articles/:slug/comments/:id,需鉴权。

5.2 前端细节(e2e 佐证)

specs/e2e/comments.spec.ts 将"CR-D"中的"D"权限与前端行为固化下来:

  • 只有评论作者能看到删除按钮:对他人(如演示用户 johndoe)的评论,删除图标 span.mod-options i.ion-trash-a 不可见;对自己的评论则可见(comments.spec.ts);
  • 未登录用户看不到评论表单,只看到登录/注册链接(comments.spec.ts);
  • HTTP 状态码宽容性:规范中 DELETE 使用 204 No Content,但测试故意把 DELETE 响应改写为 200,验证前端应按"2XX 均为成功"处理而不是死盯 204(comments.spec.ts)。

API 侧的错误场景(空 body、针对不存在文章的评论操作等)由 specs/api/bruno/errors-comments/ 目录覆盖,例如空 body 创建评论应返回 422 并携带 errors.body 错误数组。

6. 分页文章列表:GET + limit/offset

6.1 查询参数

功能项 "GET and display paginated lists of articles" 对应全局文章列表端点 GET /api/articles,鉴权可选,按最新优先排序,支持如下查询参数(见 endpoints.md):

参数 作用 示例
tag 按标签过滤 ?tag=AngularJS
author 按作者过滤 ?author=jake
favorited 按某用户已收藏过滤 ?favorited=jake
limit 每页数量,默认 20 ?limit=20
offset 跳过条数,默认 0 ?offset=0

响应结构为 articles 数组 + articlesCount 总计数(api-response-format.md),前端据此渲染分页控件。API 层的 limit/offset 行为由 specs/api/bruno/pagination/ 验证:创建两篇文章后,?limit=1 返回最新一篇,?limit=1&offset=1 返回第二篇(0405)。

6.2 关注后的信息流端点

GET /api/articles/feed 与列表端点共享 limit/offset 语义,但必须鉴权,且只返回"已关注用户"发布、按最新优先排列的文章(endpoints.md)。新用户(未关注任何人)的 feed 应为空,这一点被 specs/api/bruno/feed/ 用例显式验证(03-feed-for-new-user-returns-empty.bru)。

6.3 一个重要的响应结构变更

规范在 api-response-format.md 中以醒目提示标明:自 2024/08/16 起,GET /api/articlesGET /api/articles/feed 出于性能考虑不再返回文章 body(列表项只含 slug、title、description、tagList、时间戳、收藏状态与作者信息)。实现列表页时不要依赖列表响应里的正文,正文只能从 GET /api/articles/:slug 获取。

6.4 UI 侧分页断言

specs/e2e/navigation.spec.ts 创建了 12 篇共享唯一 tag 的文章,访问 /tag/:tag 后断言第一页的文章预览数 > 0<= 10——即 UI 层单页展示上限为 10 条。注意这与 API 默认 limit=20 并不矛盾:前者是前端每次请求取回并渲染的数量约束,后者是端点的默认参数。

7. 收藏文章

7.1 端点

  • 收藏:POST /api/articles/:slug/favorite,需鉴权,无需任何参数,返回更新后的文章对象;
  • 取消收藏:DELETE /api/articles/:slug/favorite,同样无参数、返回文章对象(endpoints.md)。

收藏状态直接体现在文章响应里:单篇文章含 favorited(对当前请求者)与 favoritesCount(总数)字段(api-response-format.md)。

7.2 与列表过滤的联动

GET /api/articles?favorited=:username 用于拉取某用户收藏的文章集合,这正是 profile 页 "Favorited" 标签页的数据来源。API 侧由 specs/api/bruno/favorites/ 全链路验证:收藏 → 再查持久化 → 按 favorited 参数过滤(含鉴权/未鉴权两种调用)→ 取消收藏 → 再验证(03-favorite-article.bru05-articles-filtered-by-favorited-username.bru07-unfavorite-article.bru)。

UI 侧,social.spec.ts 在文章页点击 Favorite 按钮后,进入 profile 的 Favorited 标签页(URL 变为 /profile/:username/favorites,与 routing.md 一致)断言收藏的文章出现;navigation.spec.ts 还验证了 profile 两个标签(My Articles / Favorited)各自的文章计数展示。

8. 关注用户

8.1 端点与 following 字段

  • 读取资料:GET /api/profiles/:username,鉴权可选,返回 profile 对象(usernamebioimagefollowing);
  • 关注:POST /api/profiles/:username/follow,需鉴权,无额外参数;
  • 取消关注:DELETE /api/profiles/:username/follow,需鉴权,无额外参数(endpoints.md)。

following 是一个相对布尔值:响应中该字段表示"当前请求者是否已关注该 profile 的用户"。Profile 结构示例见 api-response-format.md

8.2 测试佐证

9. 如何验证一份实现满足全部功能:两套测试套件

功能清单不是口头约定,仓库提供了两套可执行验证:

API 测试(Hurl / Bruno):针对任意后端实现,specs/api/ 目录按功能域组织——auth、articles、comments、profiles、favorites、feed、pagination、tags,外加 errors-* 错误场景集。运行方式(见 specs/api/README.md):

# Hurl(源文件 specs/api/hurl/*.hurl 为 source of truth)
HOST=http://localhost:3000/api ./run-api-tests-hurl.sh

# Bruno(由 Hurl 生成,make bruno-generate 生成,CI 中 make bruno-check 校验同步)
HOST=http://localhost:3000/api ./run-api-tests-bruno.sh

Bruno 集合可直接用 Bruno 客户端打开 specs/api/bruno/ 目录交互式执行。

前端 e2e 测试(Playwright)specs/e2e/ 下的 spec 文件与功能清单一一对应——

features.md 功能项 e2e 验证文件
JWT 认证(登录/注册/登出) specs/e2e/auth.spec.ts
用户 CRU(设置页) specs/e2e/settings.spec.ts
文章 CRUD specs/e2e/articles.spec.ts
评论 CR-D specs/e2e/comments.spec.ts
分页列表与过滤 specs/e2e/navigation.spec.ts
收藏 + 关注 specs/e2e/social.spec.ts

页面元素定位约定见 specs/e2e/SELECTORS.md,API 调用辅助函数见 specs/e2e/helpers/

10. 小结:把功能清单读成验收表

features.md 虽然只有一页纸,但它与 endpoints.mdapi-response-format.mderror-handling.md(422 错误体、401/403/404 状态码约定)一起,构成了 RealWorld 实现的完整验收表:JWT 认证(Token 请求头 + localStorage)、用户 CRU(PUT /api/user 的字段规范化语义)、文章 CRUD(slug 联动与 403 权限边界)、评论 CR-D(仅作者可删、2XX 宽容处理)、分页列表(limit 默认 20、列表不含 body)、收藏(无参 POST/DELETE + ?favorited= 过滤)、关注(following 相对语义 + feed 闭环)。对任何一份前端或后端实现,只要逐条跑通 specs/apispecs/e2e 两套测试,即可证明该清单被完整满足。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384