RealWorld 前端路由规范解析:Conduit 应用的 URL 设计、Feed 切换、分页与认证守卫
【免费下载链接】realworld"The mother of all demo apps" — Exemplary fullstack Medium.com clone powered by React, Angular, Node, Django, and many more项目地址: https://gitcode.com/GitHub_Trending/re/realworld
RealWorld("The mother of all demo apps")官方文档中的前端路由规范(routing.md)是 Conduit 类应用前端实现的"骨架文件":它规定了全部页面 URL、认证方式、以及每个页面上的关键交互要求。本文以该规范为主体,逐页拆解每一条路由的设计意图,并借助仓库中的共享 Playwright E2E 测试(specs/e2e/url-navigation.spec.ts、specs/e2e/navigation.spec.ts)与后端端点定义(specs/api/bruno)说明这些路由如何与 API 参数、测试契约一一咬合。读完本文,你可以直接照着规范落地一套可被官方测试套件验收的前端路由体系。
一、完整路由总览
原规范文档只列出了 7 组基础路由,而仓库中的 selectors 契约 把它们扩展成了一张更完整的"路由合同表"。两套资料合并后,一份合格的 RealWorld 前端实现必须支持以下全部 URL:
| 路由 | 页面 | 规范要求 |
|---|---|---|
/ | 首页 / Global Feed | 标签列表 + 文章列表 + 分页 |
/?feed=following | 首页 / Your Feed | 已认证用户的关注流 |
/?page=N | 分页 feed | 文章列表翻页 |
/tag/:tag | 按标签筛选的首页 | 从侧边栏点击标签进入 |
/tag/:tag?page=N | 标签视图分页 | 标签页翻页 |
/login | 登录页 | 提交后跳转/ |
/register | 注册页 | 提交后跳转/ |
/editor | 新建文章 | 编辑器页面 |
/editor/:slug | 编辑文章 | 带 slug 的编辑模式 |
/settings | 用户设置 | 表单 + 登出 |
/profile/:username | 用户资料页 | 基本信息 + My Articles |
/profile/:username/favorites | 资料页收藏 tab | 用户收藏的文章 |
/article/:slug | 文章详情页 | Markdown 渲染 + 评论区 |
这张表来自 SELECTORS.md 的 Routes 章节,它与 routing 规范文档完全一致,且额外明确了/?feed=following和?page=N这两个"查询参数式路由"——它们不是独立页面,而是首页的 URL 状态,这正是 Conduit 路由设计的精髓:用 URL 承载 feed 类型与页码,使分享、前进/后退、刷新都可用。
二、首页/:三种数据源与侧边栏标签
原规范对首页的三条要求是:
- 标签列表(List of tags)
- 文章列表,来源三选一:Feed、Global 或按 Tag
- 文章列表分页(Pagination)
与 API 的映射
对照 endpoints 规范 和 OpenAPI 定义(specs/api/openapi.yml),三种数据源分别落在不同端点上:
| 前端状态 | URL | API 调用 | 关键参数 |
|---|---|---|---|
| Global Feed | / | GET /api/articles | 默认按时间倒序 |
| Your Feed | /?feed=following | GET /api/articles/feed | 需要认证,返回被关注用户的文章 |
| 按标签筛选 | /tag/:tag | GET /api/articles | ?tag=AngularJS |
| 分页 | ?page=N | limit/offset | 文档默认limit=20、offset=0 |
补充两个容易遗漏的查询参数:?author=jake(按作者筛选)和?favorited=jake(按收藏者筛选)——后者正是资料页 favorites tab 的数据来源(见第六节)。标签侧边栏的数据来自GET /api/tags,无需认证,返回扁平标签数组。
Feed tab 的 URL 契约
E2E 测试把 feed 切换定义成了硬性断言(url-navigation.spec.ts):
- 未登录用户访问
/时,Global Feed链接必须带active类,URL 保持/; - 已登录用户点击
Your Feed,URL 必须变为/?feed=following,且Your Feed链接转为 active; - 未登录用户访问
/?feed=following必须被重定向到/login——这就是 routing 文档隐含的"feed 认证守卫"要求; - 空 feed 时应展示
.empty-feed-message提示(文案含 "Your feed is empty"),并提供返回 Global Feed 的链接。
从源码结构看,测试还断言了两个 tab 的href属性:Your Feed 固定指向/?feed=following,Global Feed 固定指向/。这意味着 tab 切换不是纯前端状态,而是真实的路由导航,浏览器"后退"按钮可以直接在两种 feed 之间切换。
标签导航
navigation.spec.ts 验证了标签筛选链路:点击侧边栏.sidebar .tag-list .tag-pill中的标签后,对应标签名出现在.nav-link中并处于 active 状态,且列表只显示带该标签的文章。而should paginate articles用例直接访问/tag/<uniqueTag>断言首页数量不超过 10 条——说明标签页也是可分页的独立 URL,可以直接通过地址栏深链接进入某一页。
三、/login与/register:JWT 存 localStorage
原规范原文:
Sign in/Sign up pages (URL:
/login,/register) Uses JWT (store the token in localStorage) Authentication can be easily switched to session/cookie based
三条要点拆解如下。
1. JWT 的存储位置是契约,不只是惯例
SELECTORS.md 明确规定 localStorage 的 key 必须是jwtToken,value 为 JWT 字符串。共享测试套件会直接读写这个 key 来注入/读取令牌(SPA 模式下通过page.route()拦截 API 流量),所以换 key 名会导致测试直接失败。登录后所有请求需携带请求头Authorization: Token <jwt>(格式见 endpoints 文档)。
2. 提交后的跳转行为
auth 辅助函数 展示了验收标准:register()在/register填写input[name="username"]、input[name="email"]、input[name="password"]后点击 submit,必须跳转到/;login()在/login同样以跳转到/为成功标志;登出则是从/settings点击 "Or click here to logout" 按钮并回到/。若表单校验失败,错误信息必须渲染在.error-messages列表中(如 "That email is already taken")。
3. 可切换到 session/cookie 的余地
规范特意说明认证"可轻松切换为 session/cookie 方案"。这对应测试套件的多形态设计:helpers/config.ts 定义了三种TEST_MODE——spa(浏览器直连 REST API,JWT 存客户端,可拦截 API 流量)、ssr(服务端代表浏览器调用外部 API,例如 httpOnly-cookie 认证的 SvelteKit/Next.js 应用)、fullstack(前后端一体,全部走 UI 驱动)。从源码结构看,路由规范刻意不绑定具体认证实现,前端只需要保证"令牌从登录/注册响应中获得、随请求发出、持久化在约定的位置"这三件事,测试套件就能同时验收三种架构。
四、/settings与/editor、/editor/:slug
设置页
/settings是唯一要求登录的"纯表单页",规范在 templates 文档 中给出了完整模板:头像 URL、姓名、bio、邮箱、新密码五个字段,一个 "Update Settings" 按钮(对应PUT /api/user,接受字段为email、username、password、image、bio),以及页底的 "Or click here to logout" 登出按钮。E2E 契约要求输入框带name属性(input[name="image"]、textarea[name="bio"]等),以便测试定位。
编辑器双模式
规范将/editor(新建)与/editor/article-slug-here(编辑)并列,要求同一页面组件承担两种模式:
/editor:空表单,含 "Article Title"(input[name="title"])、"What's this article about?"(input[name="description"])、Markdown 正文(textarea[name="body"])、标签输入框(placeholder 固定为 "Enter tags"),以及 "Publish Article" 按钮,提交走POST /api/articles;/editor/:slug:先用GET /api/articles/:slug拉取现有内容回填表单,提交走PUT /api/articles/:slug。
一个隐藏的细节来自 endpoints 文档:当title被修改时,slug也会被后端更新。这意味着编辑器提交成功后,前端应当用响应体中的新 slug 更新浏览器 URL(/editor/<new-slug>),否则用户下次手动刷新会 404。这也是把"slug 放 URL 里"这一路由设计的代价与收益:URL 天然幂等、可刷新,但要求前端在保存后同步地址栏。
五、/article/:slug:条件渲染与客户端 Markdown
原规范对文章详情页给出了四条要求,每一条都对应明确的测试断言:
- 删除按钮只展示给作者。模板中
Delete Article按钮(危险样式.btn-outline-danger)与Edit Article链接只应在"当前登录用户 == 文章作者"时渲染。跨用户越权的可行性在后端由 403 兜底(errors_authorization 测试组),但前端"只给作者显示按钮"是规范层面的 UI 要求。 - 客户端渲染 Markdown。API 返回的
body是原始 Markdown 字符串,前端负责在详情页渲染成 HTML,渲染结果包裹在.article-content中(selectors 契约)。 - 底部评论区。评论表单(
textarea[placeholder="Write a comment..."]+ "Post Comment" 按钮)对应POST /api/articles/:slug/comments,列表通过GET /api/articles/:slug/comments拉取,无需认证即可阅读。 - 删除评论按钮只展示给评论作者,点击调用
DELETE /api/articles/:slug/comments/:id;comments E2E 用例 与 selective-deletion Bruno 用例(创建两条、删一条、验证另一条仍在)分别验证了 UI 层与 API 层的"删除后其余内容不丢失"。
六、/profile/:username与/profile/:username/favorites
规范原文:
Profile page (URL:
/profile/:username,/profile/:username/favorites) Show basic user info List of articles populated from author's created articles or author's favorited articles
拆解为三个实现点:
- 基本信息:用户名、bio、头像(
.user-info/.user-img)、关注按钮(POST/DELETE /api/profiles/:username/follow,按钮文案在Follow {username}与Unfollow {username}间切换)。头像为null或空时必须回退到 default-avatar.svg——这是 SELECTORS 契约中"Default Avatar"一节明确断言的行为。 - My Articles tab:调用
GET /api/articles?author=:username; - Favorited Articles tab:调用
GET /api/articles?favorited=:username,并对应独立 URL/profile/:username/favorites,保证 tab 可直接深链接。
navigation.spec.ts 的 profile 用例 量化了验收标准:用户创建 2 篇文章并收藏其中 1 篇后,My Articles tab 必须显示 2 条.article-preview,切到 Favorited tab 后必须恰好 1 条。空资料页(0 篇文章)也要优雅处理而不是报错。
七、分页:?page=N的 URL 语义
routing 规范只写了 "Pagination for list of articles" 一句话,但 E2E 套件把它的语义钉得很死(url-navigation.spec.ts 的 Pagination 组):
- 页码进 URL:点击第 2 页后,URL 必须变为
/tag/<tag>?page=2,且第 2 页的.page-item带active类; - URL 可直达:直接访问
/tag/<tag>?page=2,页面应加载第 2 页并高亮页码 2; - 参数可组合:Your Feed 翻页时 URL 变为
/?feed=following&page=2,feed与page参数共存; - 切换 feed 重置页码:在
/tag/<tag>?page=2上点击 Global Feed,URL 回到/(页码参数被清除); - 每页条数:API 端
limit默认 20(endpoints 文档),而共享 E2E 套件按每页 10 条断言("15 articles = 2 pages with limit 10",第一页 10 条、第二页 5 条)。因此前端列表请求应显式传limit=10,不要依赖服务端默认值。
前端实现时的典型换算:page从 1 起(URL 语义),API 的offset从 0 起,即offset = (page - 1) * limit。后端对这套换算的验收可见 pagination Bruno 用例(limit=1时articles.length === 1且articlesCount为总数)。
八、用共享 E2E 套件验收你的路由实现
本仓库自带一套可直接复用的 Playwright 测试(specs/e2e),其中 navigation.spec.ts 与 url-navigation.spec.ts 正是路由规范的自动化验收脚本。接入方式(见 tests 文档 与 playwright.base.ts):
- 在实现仓库中继承
specs/e2e/playwright.base.ts,覆写baseURL指向本地 dev server,必要时配置webServer启动命令; - 通过环境变量
TEST_MODE声明架构形态:浏览器直连 API 的 SPA 用spa(默认),服务端渲染用ssr,前后端一体用fullstack;从源码结构看,测试内部按BROWSER_API/EXTERNAL_API两个能力开关分支,而非直接判断模式字符串; - 保证实现满足 SELECTORS.md 的全部契约:路由表、
name属性、.feed-toggle/.article-preview/.pagination等 CSS 类、.error-messages错误列表、localStorage 的jwtTokenkey,以及window.__conduit_debug__调试接口(提供getToken()、getAuthState()、getCurrentUser()三个方法); - 需要外部 demo API 时,测试默认调用 helpers/config.ts 中
API_BASE配置的官方 demo 地址(可用环境变量覆盖),该 demo API 仅允许 RealWorld 前端应用消费,具体限制见 API 文档。
小结
RealWorld 的路由规范看似只有一张短列表,实际隐含了五个可测试的约束:feed 类型与页码全部进 URL(/?feed=following、?page=N、/tag/:tag?page=N)、未认证访问 feed 重定向到/login、JWT 固定存于localStorage.jwtToken、/editor/:slug在改标题后需同步新 slug、文章/评论的删除按钮按作者身份条件渲染。仓库中的 E2E 测试与 Bruno 用例逐条印证了这些约束——对照 routing 规范、SELECTORS 契约 和 端点定义 三份文档实现,再用共享 Playwright 套件跑一遍导航与分页用例,即可完成路由层的全部验收。
【免费下载链接】realworld"The mother of all demo apps" — Exemplary fullstack Medium.com clone powered by React, Angular, Node, Django, and many more项目地址: https://gitcode.com/GitHub_Trending/re/realworld
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考