RealWorld(Conduit)实现功能清单详解:从 JWT 认证到文章、评论、收藏与关注
本文以 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 章节(introduction 与 expectations 的同级文件),与后端端点规范 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": "..."}},必填字段email、password;POST /api/users(注册):请求体在 user 对象中额外携带username,必填字段为email、username、password。
两者均返回 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 对认证全链路做了验证:
- 注册成功后跳转回首页,导航栏出现用户头像链接,且可进入
/editor(auth.spec.ts); - 错误密码登录必须展示
.error-messages错误区且停留在/login(auth.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(需鉴权),接受的字段为email、username、password、image、bio(见 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 的边界语义固化成了可执行契约,例如:
bio更新为空字符串应被规范化为null(06-update-user-bio-to-empty-string-should-normalize-to-null.bru);- 对可空字段
bio/image显式传null则应接受(09-update-user-bio-to-null-should-accept-for-nullable-field.bru、16-set-image-then-update-to-null-should-accept-for-nullable-field.bru); username/email更新为空字符串或null一律拒绝(12、14-update-email-to-null-should-reject.bru);- 密码更新短于 8 字符拒绝,8 字符与 64 字符均接受(18-update-password-shorter-than-8-chars-should-reject.bru、19-update-password-to-8-char-simple-word-should-accept.bru、20-update-password-to-64-chars-should-accept.bru)。
3.3 前端侧验证(设置页)
specs/e2e/settings.spec.ts 从 UI 角度覆盖了设置页的更新能力:仅更新 bio(settings.spec.ts)、仅更新 image、同时更新两个字段、更新后导航到 /profile/:username 展示新值,以及更新后导航栏用户名不被破坏、可再次进入设置页继续编辑。该套件同时支持两种运行模式:纯 SPA(浏览器直连 API,断言 PUT /api/user 的响应体)与 fullstack(表单 POST 后断言渲染结果),由配置项 BROWSER_API 切换。
4. 文章 CRUD:功能清单中唯一"全量"的资源
4.1 四个端点与字段约束
| 操作 | 端点 | 鉴权 | 关键约束 |
|---|---|---|---|
| 创建 | POST /api/articles |
需要 | 必填 title、description、body;可选 tagList(字符串数组) |
| 读取 | GET /api/articles/:slug |
不需要 | 返回单篇文章 |
| 更新 | PUT /api/articles/:slug |
需要 | 可选 title、description、body |
| 删除 | 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"]
}
}
两个容易被实现者忽略的细节:
- slug 随标题联动:更新时若修改了
title,slug也要随之更新。规范同时强调 slug 只需是唯一字符串,重复标题必须产生不同的 slug(如 kebab-case 派生); - 权限边界:更新/删除仅作者可操作。
specs/api/bruno/errors-authorization/用两个用户验证了这一点——用户 B 删除/更新用户 A 的文章应得到 403,且文章必须仍然存在(04-user-b-tries-to-delete-403.bru、05-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 返回第二篇(04、05)。
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/articles 与 GET /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.bru、05-articles-filtered-by-favorited-username.bru、07-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对象(username、bio、image、following); - 关注:
POST /api/profiles/:username/follow,需鉴权,无额外参数; - 取消关注:
DELETE /api/profiles/:username/follow,需鉴权,无额外参数(endpoints.md)。
following 是一个相对布尔值:响应中该字段表示"当前请求者是否已关注该 profile 的用户"。Profile 结构示例见 api-response-format.md。
8.2 测试佐证
- API 层:specs/api/bruno/profiles/ 覆盖未鉴权/鉴权读取 profile、关注、取消关注及"取消关注已持久化"的验证(05-follow-profile.bru、07-verify-unfollow-persisted.bru);对不存在用户执行关注/取关应返回 404(specs/api/bruno/errors-profiles/05-follow-unknown-user-authed.bru);
- UI 层:social.spec.ts 断言关注按钮文案在 Follow/Unfollow 间切换;查看他人 profile 时应显示 Follow 按钮而不显示 "Edit Profile Settings",自己的 profile 则相反(social.spec.ts);
- 关注 → Feed 闭环:关注某用户后切换到首页 "Your Feed",该用户发布的文章应出现在信息流中(social.spec.ts)。这把第 6 节的 feed 端点与第 8 节的 follow 端点在行为上连成了完整闭环。
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.md、api-response-format.md、error-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/api 与 specs/e2e 两套测试,即可证明该清单被完整满足。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00