第一步:先用 Vite 搭建 React 前端项目骨架, 然后配置路由、样式、测试工具, 再根据 Figma 写登录页和注册页组件, 最后补 Axios API 基础配置和自动测试。
Commit 1#
- [[#0. Git 分支规范]]
- [[#1. 路由]]
- [[#2. 测试]]
- [[#3. App 页面保护]]
正文#
0. Git 分支规范#
#git #版本控制 #分支策略
先初始化,再创建分支#
git initbash把当前文件夹初始化成 Git 仓库
git add . # 记得先写 .gitignorebash把当前所有项目文件加入暂存区
git commit -m "chore: initialize synctalk project"bash创建第一次提交,作为项目初始保存点。
git branch -M mainbash把当前分支命名为 main。
然后创建一个开发分支:
git checkout -b feat/setup-authbash创建并切换到开发分支 feat/setup-auth
1. 路由#
#路由 #React-Router #页面导航
router.tsx#
在 main.tsx 里面的 router 是整个前端应用的路由管理器。例如:
用户访问某个网址
→ router 判断这个网址对应哪个页面
→ React 渲染对应组件plaintext这是一个重要的功能,它告诉前端:
- 访问
/auth/login→ 显示登录页 - 访问
/auth/register→ 显示注册页 - 访问
/app/discover→ 如果没登录,跳回登录页
写清楚了网址和页面的对应关系。
main.tsx 本身不关心有哪些页面,它只是把路由系统挂到 React 里。
页面目录结构#
目前页面存储在 features 下:
features/auth/ 放登录注册这个业务模块
├── auth-shell.tsx 登录/注册页共用的大背景和布局
├── login-page.tsx 登录表单
└── register-page.tsx 注册表单plaintext2. 测试#
#测试 #Vitest #单元测试
__tests__ 是一种常见约定,意思是”这里放测试文件”。例如:
auth-pages.test.tsx测的是登录/注册相关 UI:- 登录页是否显示 SYNCTALK
- 登录页是否显示 Welcome Back
api-client.test.ts测的是 API 请求配置:- Axios 是否开启
withCredentials,也就是确认以后请求后端时会自动带 Cookie
- Axios 是否开启
文件名里的 .test.tsx / .test.ts 是测试文件标记。Vitest 会自动找这些文件来运行。
3. App 页面保护#
#路由保护 #权限控制 #项目结构
项目分成了两大区域:auth 和 app,分别对应登录注册等公开页面和登录后才能访问的页面。
所以目录是:
src/app/router.tsx 总路由配置
src/app/routes/app/ 登录后的应用页面
src/features/auth/ Auth 模块自己的组件plaintextCommit 2#
- [[#概念辨析]]
- [[#Express 是什么]]
- [[#Express 与 props 的区别]]
- [[#中间件思想]]
- [[#红绿测试流程]]
- [[#test 测试用例说明]]
- [[#第一次尝试运行:验证后端逻辑]]
- [[#前后端两套路由]]
- [[#Prettier 代码规范化]]
正文#
概念辨析#
#后端概念 #MVC #REST
| 名称 | 它是什么 | 主要解决什么问题 |
|---|---|---|
| MVC | 项目分层/代码组织模式 | 代码怎么分层 |
| REST | API 接口设计风格 | 前后端怎么通信 |
| DDD | 复杂业务建模思想 | 复杂业务怎么拆、怎么表达 |
| Express | Node.js 后端框架 | 怎么快速写后端服务 |
简单来说就是:
- Express:用来写后端服务的框架
- REST:接口应该怎么设计
- MVC:代码应该怎么分层
- DDD:复杂业务应该怎么建模和拆分
Express 是什么#
#Express #Node.js #Web框架
Express 是 Node.js 生态中非常常见的极简 Web 框架。NestJS 则是更偏工程化、架构化的后端框架,底层可以使用 Express 或 Fastify。
Express 官方介绍它是一个”极简、灵活的 Node.js Web 应用框架”,提供了构建 Web 应用和 API 的基础能力。MDN 也称它是运行在 Node.js 环境中的、不强制项目结构的 JavaScript Web 框架。
简单来说,Express 更像一个”交通调度系统”:
请求来了
↓
看它要去哪条路由
↓
送到对应的处理函数
↓
处理函数拿 req 里的数据
↓
处理完后用 res 返回结果plaintext专业的说,Express 提供了处理 HTTP 请求和响应的框架能力。开发者提前定义路由规则,Express 根据请求方法和路径匹配对应的处理函数,并通过 req 传入请求数据,通过 res 返回响应数据。
比如:
app.get('/users/:id', (req, res) => {
const id = req.params.id
res.json({
id,
name: '张三'
})
})js当前端访问 GET /users/1,Express 会做:
- 接收到
GET /users/1请求 - 根据路由规则匹配到
app.get('/users/:id', ...) - 把请求信息放进
req - 执行你写的函数
- 你通过
res.json()返回数据
其中:req.params.id 就是前端 URL 里传来的 1。而 res.json(...) 就是后端返回给前端的数据。
进一步说,Express 本质上也是用 JavaScript 函数、对象、回调这一套逻辑实现的。
例如,原生的 Node.js 写法可能是:
if (req.url === '/users' && req.method === 'GET') {
// 获取用户列表
}
if (req.url === '/login' && req.method === 'POST') {
// 登录
}
if (req.url.startsWith('/users/') && req.method === 'GET') {
// 获取某个用户
}jsExpress 帮你变成:
app.get('/users', getUserList)
app.post('/login', login)
app.get('/users/:id', getUserById)js也就是说:
- 原生 Node.js:你自己判断 URL 和 method
- Express:帮你把 URL 和 method 匹配好
Express 与 props 的区别#
#req-res #路由匹配 #类比理解
可以类比,但不能完全等同。
React 是父组件直接传 props 给子组件。
而 Express 就是接收请求,根据内部路由规则找到对应处理函数,把请求数据放进 req,把响应能力放进 res,然后让处理函数通过 res 返回数据给前端。
把 Express 想象成内部大概维护了一个”路由表”:
Express = 接请求 → 匹配路由 → 执行函数 → 用 res 返回响应plaintext中间件思想#
#中间件 #请求流水线
在 app.js 里面,有一条中间件流水线(middleware pipeline),也就是 Express 的典型设计思想。
依据这个思想,请求会按顺序经过一层层函数。例如:
入口:
// server.js
import { app } from './app.js';
app.listen(8000);js把 app.js 里定义好的 Express 应用启动在 8000 端口。启动以后,真正处理请求的是 app.js 里的这条流水线:
cors
→ express.json()
→ cookieParser()
→ healthRouter // 路由
→ 404 handler
→ error handlerplaintext在这里,中间件是提前写好的一些请求处理函数,请求进来后会按 app.use(...) 的顺序依次经过这些中间件,然后匹配路由。
- 普通中间件通常接收
req、res、next; - 错误处理中间件通常接收
error、req、res、next。
匹配到路由后,路由 handler 通过 res 把响应发回去。
另外,路由 handler 也是一种处理函数,只是它绑定了具体 method + path,比如:
healthRouter.get('/health', handler)jsserver.js 只负责启动 HTTP 服务,不参与每次请求的具体业务处理。例如:
app.listen(env.port, () => {
console.log(`SyncTalk backend listening on http://127.0.0.1:${env.port}`);
});js让 app.js 里定义好的 Express 应用开始监听某个端口。
启动以后,浏览器地址栏输入:http://127.0.0.1:8000/health
浏览器默认用 GET 方法请求这个地址。
然后 Express 才会处理这个请求,走到 app.js 里的中间件和路由。
相当于:
你运行 node src/server.js
→ Node.js 进程启动
→ 执行 server.js
→ server.js 调用 app.listen(...)
→ HTTP 服务开始监听 8000 端口plaintext不是 server 启动 Node.js 进程,而是 Node.js 进程运行了 server.js。
启动以后,别人就可以在浏览器访问:
GET http://127.0.0.1:8000/healthplaintext然后 Express 匹配:
healthRouter.get('/health', handler)js红绿测试流程#
#TDD #红绿测试 #测试驱动
红绿测试流程来自 TDD,也就是测试驱动开发。它的核心顺序是:
- 红:先写测试,运行后失败
- 绿:写最少代码,让测试通过
- 重构:在测试保护下整理代码
在这一版里面,红绿测试的流程是:
-
先写 health 测试 预期
GET /health返回 200 和固定 JSON。 -
第一次运行测试失败 因为还没有
app.js。 -
补最小 Express app 再跑测试,失败变成 404。 这说明测试已经真正请求到了服务,只是还没有
/health路由。 -
补
/health路由 再跑测试,通过。
这个流程的意义是:测试不是事后装饰,而是真的证明”没有这个功能时会失败,有这个功能后会通过”。这样以后有人不小心改坏 /health,测试会立刻报出来。
test 测试用例说明#
#npm #Vitest #测试配置
npm run test 的意思不是”npm 自带一个 test 函数”。它的真实含义是:
让 npm 去
package.json里找scripts.test,然后执行那条命令
例如,在 ../frontend/package.json 里面:
"scripts": {
"test": "vitest run"
}json当我们执行 npm run test 的时候,等价于执行 vitest run。
后端也类似,在 ../backend/package.json 里面是:
"scripts": {
"test": "vitest run"
}json所以它会启动 Vitest,然后 Vitest 根据配置去找测试文件,比如:
*.test.js
*.test.ts
*.test.tsx
__tests__/plaintext找到以后运行里面写好的测试用例。
相当于:跑一遍项目里面的所有 test 文件写好的测试用例,来保证测试覆盖到的行为没有坏。
第一次尝试运行:验证后端逻辑#
#后端启动 #路由验证 #Express
通过 npm 运行后端,打印输出如下:
Restarting 'src/server.js'
SyncTalk backend listening on http://127.0.0.1:8000plaintext如果直接访问 http://127.0.0.1:8000,会返回 {"error":"Not found"}。
因为访问的这个路径还没有对应的路由,而前面 app.js 又写了没有对应路由时的处理方式:
app.use((req, res) => {
res.status(404).json({ error: 'Not found' });
});js访问 http://127.0.0.1:8000/health 的时候,则会在浏览器看见:
{"status":"ok","service":"synctalk-backend"}json因为有路由,所以 ok。
在 ./frontend 路径下运行 npm dev 启动前端,进入以后是 http://localhost:5173/auth/login,因为这里被前端路由了。具体代码在 frontend/src/app/router.tsx,里面定义了类似于:
/auth/login -> LoginPage
/auth/register -> RegisterPage
/app/discover -> ProtectedRoute -> DiscoverPageplaintext前后端两套路由#
#前后端协作 #路由分工 #登录流程
也就是说,现在项目里面有两套路由:
- 后端路由:Express 管,返回 JSON 数据
- 前端路由:React Router 管,返回页面 UI
以后登录的时候会发生两层配合:
用户打开 /auth/login
→ 前端 React Router 显示登录页
→ 用户提交表单
→ 前端 Axios 请求后端 POST /api/auth/login
→ 后端 Express 校验账号密码
→ 后端返回结果 / 写 Cookie
→ 前端根据结果跳转到 /app/discoverplaintextPrettier 代码规范化#
#Prettier #代码格式化 #工程化
添加 Prettier,规范化代码:
- 新增根目录配置:
.prettierrc.json.prettierignore
- 给前端和后端都加了脚本:
npm run formatnpm run format:check
- 给前后端都安装了 Prettier
- 更新了
frontend/package-lock.json和backend/package-lock.json - 跑了一次 Prettier 机械格式化,让新增的格式检查能通过
Commit 3#
- [[#后端:MongoDB 用户模型]]
- [[#后端:JWT 机制]]
- [[#后端:Axios 请求客户端]]
- [[#前端:Axios 与 Express 的联动]]
正文#
这一版重点是 Auth 认证模块:
-
后端新增真实用户系统 新增了 User 模型,使用 MongoDB/Mongoose 保存用户。
-
注册功能 用户可以提交
username/email/password,后端会校验输入、加密密码,然后创建用户。 -
登录功能 用户可以用邮箱或用户名登录,后端会校验密码。
-
密码安全处理 密码不会明文保存,而是通过
bcryptjs哈希后保存成passwordHash。 -
JWT + HttpOnly Cookie 登录态 登录/注册成功后,后端签发 JWT,并写入
synctalk_sessionCookie。前端不直接保存 token。 -
恢复登录态 新增
/api/auth/me,刷新页面后前端可以请求当前用户,判断是否仍然登录。 -
退出登录 新增
/api/auth/logout,会清除 Cookie。 -
前端接入真实 API 登录页、注册页现在会真的调用后端接口,成功后跳转到
/app/discover。 -
保护
/app/*页面ProtectedRoute会先请求当前用户;没登录就跳回/auth/login。 -
新增认证相关测试 后端测试覆盖注册、登录失败、读取当前用户、未登录访问、退出登录。前端测试也补了登录/注册交互状态。
后端:MongoDB 用户模型#
#MongoDB #Mongoose #数据模型
预留并定义了 MongoDB 的用户数据模型。
后端:JWT 机制#
#JWT #认证 #Cookie
JWT 不是加密敏感请求的工具。JWT 是后端签发的身份凭证。
前端访问需要登录身份的接口时,浏览器会自动带上保存了 JWT 的 HttpOnly Cookie。保存或携带它,后端验证它,验证通过后知道当前用户是谁。
- 敏感数据传输靠 HTTPS
- 密码存储靠 bcrypt 哈希
- JWT 安全靠签名、过期时间、HttpOnly Cookie、HTTPS 和足够强的
JWT_SECRET
后端:Axios 请求客户端#
#Axios #HTTP请求 #前端配置
Axios 不是后端框架,它是一个 HTTP 请求客户端库。
- 后端框架是 Express
- 前端请求工具是 Axios
React 页面
→ Axios 发 HTTP 请求
→ Express 后端接收 HTTP 请求plaintext// frontend/src/lib/api-client.ts
import axios from 'axios';
export const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL ?? 'http://127.0.0.1:8000/api',
withCredentials: true,
headers: {
'Content-Type': 'application/json',
},
});ts这里的 axios.create() 是在前端创建一个统一的请求实例,这样以后所有 API 请求都不用重复写完整地址和配置。
前端:Axios 与 Express 的联动#
#Axios #Express #前后端联动
Axios 不是直接调用 Express 函数,而是发真实 HTTP 请求;Express 根据 URL 和 method 匹配 route;route 调 service;service 返回结果给 route;route 用 res.cookie 和 res.json 组成 HTTP response;Axios 再从 response 里取 data 给前端用。
Commit 4#
- [[#路由守卫测试]]
- [[#冒烟测试]]
- [[#Profile 表单数据流]]
- [[#跨域请求]]
正文#
这一版做了 Profile 资料读取、编辑、保存和路由守卫,并且补了对应的 单测和 Playwright smoke/debug 测试。
路由守卫测试#
#路由守卫 #ProtectedRoute #测试
路由守卫测试就是测试”用户能不能进入某个路由”的规则。
在这个项目里,ProtectedRoute 负责守住 /app/*:
- 没登录:访问
/app/profile、/app/discover会跳回/auth/login - 已登录但资料没完善:访问
/app/discover会跳到/app/profile - 已登录且资料已完善:可以进入
/app/discover - 访问
/app/profile本身不会因为资料未完善而循环跳转
所以测试里会 mock /auth/me 和 /profile/me 的返回值,模拟”没登录 / 已登录 / 资料未完善 / 资料已完善”这些状态,确认页面跳转是否正确。
冒烟测试#
#冒烟测试 #SmokeTest #测试策略
英文叫 Smoke Test,意思是:先用最少的测试检查系统有没有”冒烟”坏掉。
它不是完整测试,不会把所有细节都测一遍。它只检查”这个功能有没有严重坏掉”。例如:
未登录访问 /app/profile,会不会回到登录页
mock 一个已登录用户后,/app/profile 能不能打开
页面上有没有 Complete your profile
有没有 Native language 字段
有没有 Save profile 按钮plaintext- 冒烟测试 = 快速确认”主流程没炸”
- 单元测试 = 精细确认”一小块逻辑对不对”
- 手动验收 = 像真实用户一样完整走一遍
Profile 表单数据流#
#表单 #数据流 #状态管理
编辑阶段(前端内存,不请求后端)#
用户操作控件
→ 浏览器产生 event
→ React 从 event.target.value 取值
→ 调用 updateField(field, value)
→ setForm 更新前端 React state
→ React 重新渲染页面plaintext这一阶段 不会请求后端,也 不会写数据库。
其中 event 是浏览器在用户操作时临时产生的事件对象,里面带着当前控件的新值:event.target.value。
保存阶段(前端 → 后端 → 数据库)#
点击 Finish Setup 保存时:
点击提交
→ handleSubmit
→ updateProfileMutation.mutateAsync(form)
→ Axios PATCH /api/profile/me
→ 浏览器自动携带已有 Cookie
→ Express 收到请求
→ requireAuth 从 Cookie 里读取 JWT
→ 验证 JWT,得到当前用户
→ profile route 调用 profileService
→ 保存 profile 到数据库
→ 后端返回 profile
→ 前端 TanStack Query 更新缓存
→ navigate('/app/discover')plaintext保存 profile 时不会重新写入 Cookie,因为 Cookie 是在登录或注册时写入的,而 Profile 保存接口 PATCH /api/profile/me 只会读取 Cookie 来鉴权,不会重新写 Cookie。
这里 PATCH 是 HTTP 的方法,表示局部更新一份资源。
我一开始以为所有的操作都会进行写 cookie,但是这里是:
- 登录/注册:后端写 Cookie
- 保存 Profile:后端读 Cookie,验证身份
职责分工#
- React Router 负责页面路由,不负责发请求
- Axios 负责 HTTP 请求
- 浏览器负责跨域规则和携带 Cookie
- Express Router 负责后端接口路由
所以保存时不是 React Router 把数据发给后端,而是:
updateProfileMutation.mutateAsync(form)
→ apiClient.patch('/profile/me', input)
→ Axios 把请求发到 http://127.0.0.1:8000/api/profile/meplaintext总结#
用户填写 Profile 表单时,浏览器事件把当前控件的新值交给 React 事件处理函数,React 通过
setForm更新本地 form state,并重新渲染页面。这个阶段数据只在前端内存里,不会进入后端。当用户点击 Finish Setup 提交表单时,React 调用 TanStack Query mutation,mutation 内部通过 Axios 发起
PATCH /api/profile/me请求。因为 Axios 配置了withCredentials,浏览器会携带登录时已经写好的 HttpOnly Cookie。Express 后端通过requireAuth中间件读取 Cookie 里的 JWT,验证用户身份,然后 profile route 调用 profileService 保存资料到数据库。保存成功后,后端返回 profile,前端更新 TanStack Query 缓存,并跳转到/app/discover。
三个阶段一句话概括:
| 阶段 | 流程 |
|---|---|
| 表单编辑 | event → setForm → React render |
| 表单保存 | form → Axios → Express route → auth middleware → service → database |
| 登录注册 | Express → JWT → Set-Cookie |
跨域请求#
#跨域 #CORS #浏览器安全
跨域请求就是:浏览器页面所在的地址,去请求另一个”源”的接口。
“源”由三部分决定:
协议 + 域名/IP + 端口plaintext任意一个不同 = 跨域。
比如你的前端是 http://127.0.0.1:5175,后端是 http://127.0.0.1:8000,它们协议和 IP 一样,但端口不同:5175 !== 8000,所以它们是不同源。前端请求后端:
http://127.0.0.1:5175 → http://127.0.0.1:8000/api/auth/meplaintext浏览器默认会限制跨域请求,避免一个恶意网站随便读取另一个网站的数据。所以后端必须明确允许前端访问。
Commit 5#
- [[#Docker 启动 MongoDB]]
- [[#环境变量配置]]
- [[#MongoDB Compass 连接]]
正文#
Docker 启动 MongoDB#
#Docker #MongoDB #容器
docker run --name synctalk-mongo -p 27017:27017 -v synctalk-mongo-data:/data/db -d mongo:7bash各参数含义:
mongo:7— 用 mongo:7 镜像启动一个容器--name synctalk-mongo— 容器名字叫synctalk-mongo-p 27017:27017— 把本机 27017 端口映射到容器 27017-v synctalk-mongo-data:/data/db— 把 MongoDB 数据目录/data/db保存到synctalk-mongo-data这个 volume-d— 让它在后台运行
环境变量配置#
#环境变量 #.env #数据库连接
为了将本地的项目与 Docker 的 MongoDB 连接起来,需要在 .env 写上:
MONGODB_URI=mongodb://127.0.0.1:27017/synctalkplaintext因为容器已经映射了端口 0.0.0.0:27017->27017/tcp,所以填写本地 27017 端口即可连接到 synctalk。
各部分含义:
MONGODB_URI— 变量名,后端代码会用process.env.MONGODB_URI读取它mongodb://— 协议头,告诉 MongoDB 驱动:我要用 MongoDB 标准连接协议连接数据库127.0.0.1:27017— 本地映射的端口synctalk— 数据库名
后端启动时,用这条 MongoDB 连接字符串去连接数据库。
MongoDB Compass 连接#
#MongoDBCompass #GUI #数据库管理
下载 MongoDB Compass,由于端口已经映射在本地,所以在 connections 填写:
mongodb://127.0.0.1:27017/synctalkplaintext即可连接数据库。
Commit 5#
Discovery 模块的后端接线和前端体验升级:#
- 后端补齐了 /api/users/recommendations 和 /api/users/search,把用户推荐、搜索、关系状态读取串起来了。
- 新增了好友关系相关模型和 repository 能力,用来判断 stranger、request_sent、request_received、friend。
- 前端新增 Discovery API/hooks,使用 Axios + TanStack Query 请求推荐和搜索数据。
- /app/discover 从占位页改成真实页面:支持推荐列表、搜索、loading、empty、error、用户卡片、匹配理由、关系状态展示。
- UI 按 Figma 方向重新做了一版,并进一步统一成毛玻璃材质。
- 新增开发模式 demo 用户,方便本地测试多卡片、多关系状态。
- 把容易误解的 Connect 按钮改成不可点击状态徽标;真正好友操作留到 Friends 模块。
- Recommended / Search 改成可交互分段控件,推荐可清空搜索,搜索可聚焦输入框。
- 增加了前端 Discover 单测和 Playwright smoke 覆盖。
验证结果:frontend npm run lint、frontend npm run test、npx playwright test —project=Smoke 都通过。