Joshua Chen Personal Blog

Back

📝 Note 🚧 In Progress · agent typescript react

SyncTalk平台搭建笔记

Vibe coding平台的学习记录,记录边学边做的过程

views | comments

第一步:先用 Vite 搭建 React 前端项目骨架, 然后配置路由、样式、测试工具, 再根据 Figma 写登录页和注册页组件, 最后补 Axios API 基础配置和自动测试。

Commit 1#

  • [[#0. Git 分支规范]]
  • [[#1. 路由]]
  • [[#2. 测试]]
  • [[#3. App 页面保护]]

正文#


0. Git 分支规范#

#git #版本控制 #分支策略

先初始化,再创建分支#

git init
bash

把当前文件夹初始化成 Git 仓库

git add .  # 记得先写 .gitignore
bash

把当前所有项目文件加入暂存区

git commit -m "chore: initialize synctalk project"
bash

创建第一次提交,作为项目初始保存点。

git branch -M main
bash

把当前分支命名为 main

然后创建一个开发分支:

git checkout -b feat/setup-auth
bash

创建并切换到开发分支 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    注册表单
plaintext

2. 测试#

#测试 #Vitest #单元测试

__tests__ 是一种常见约定,意思是”这里放测试文件”。例如:

  • auth-pages.test.tsx 测的是登录/注册相关 UI:
    • 登录页是否显示 SYNCTALK
    • 登录页是否显示 Welcome Back
  • api-client.test.ts 测的是 API 请求配置:
    • Axios 是否开启 withCredentials,也就是确认以后请求后端时会自动带 Cookie

文件名里的 .test.tsx / .test.ts 是测试文件标记。Vitest 会自动找这些文件来运行。


3. App 页面保护#

#路由保护 #权限控制 #项目结构

项目分成了两大区域:authapp,分别对应登录注册等公开页面和登录后才能访问的页面。

所以目录是:

src/app/router.tsx          总路由配置
src/app/routes/app/         登录后的应用页面
src/features/auth/          Auth 模块自己的组件
plaintext

Commit 2#

  • [[#概念辨析]]
  • [[#Express 是什么]]
  • [[#Express 与 props 的区别]]
  • [[#中间件思想]]
  • [[#红绿测试流程]]
  • [[#test 测试用例说明]]
  • [[#第一次尝试运行:验证后端逻辑]]
  • [[#前后端两套路由]]
  • [[#Prettier 代码规范化]]

正文#


概念辨析#

#后端概念 #MVC #REST

名称它是什么主要解决什么问题
MVC项目分层/代码组织模式代码怎么分层
RESTAPI 接口设计风格前后端怎么通信
DDD复杂业务建模思想复杂业务怎么拆、怎么表达
ExpressNode.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 会做:

  1. 接收到 GET /users/1 请求
  2. 根据路由规则匹配到 app.get('/users/:id', ...)
  3. 把请求信息放进 req
  4. 执行你写的函数
  5. 你通过 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') {
  // 获取某个用户
}
js

Express 帮你变成:

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 handler
plaintext

在这里,中间件是提前写好的一些请求处理函数,请求进来后会按 app.use(...) 的顺序依次经过这些中间件,然后匹配路由。

  • 普通中间件通常接收 reqresnext
  • 错误处理中间件通常接收 errorreqresnext

匹配到路由后,路由 handler 通过 res 把响应发回去。

另外,路由 handler 也是一种处理函数,只是它绑定了具体 method + path,比如:

healthRouter.get('/health', handler)
js

server.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/health
plaintext

然后 Express 匹配:

healthRouter.get('/health', handler)
js

红绿测试流程#

#TDD #红绿测试 #测试驱动

红绿测试流程来自 TDD,也就是测试驱动开发。它的核心顺序是:

  • :先写测试,运行后失败
  • 绿:写最少代码,让测试通过
  • 重构:在测试保护下整理代码

在这一版里面,红绿测试的流程是:

  1. 先写 health 测试 预期 GET /health 返回 200 和固定 JSON。

  2. 第一次运行测试失败 因为还没有 app.js

  3. 补最小 Express app 再跑测试,失败变成 404。 这说明测试已经真正请求到了服务,只是还没有 /health 路由。

  4. /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:8000
plaintext

如果直接访问 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 -> DiscoverPage
plaintext

前后端两套路由#

#前后端协作 #路由分工 #登录流程

也就是说,现在项目里面有两套路由:

  • 后端路由:Express 管,返回 JSON 数据
  • 前端路由:React Router 管,返回页面 UI

以后登录的时候会发生两层配合:

用户打开 /auth/login
  → 前端 React Router 显示登录页
  → 用户提交表单
  → 前端 Axios 请求后端 POST /api/auth/login
  → 后端 Express 校验账号密码
  → 后端返回结果 / 写 Cookie
  → 前端根据结果跳转到 /app/discover
plaintext

Prettier 代码规范化#

#Prettier #代码格式化 #工程化

添加 Prettier,规范化代码:

  • 新增根目录配置:
    • .prettierrc.json
    • .prettierignore
  • 给前端和后端都加了脚本:
    • npm run format
    • npm run format:check
  • 给前后端都安装了 Prettier
  • 更新了 frontend/package-lock.jsonbackend/package-lock.json
  • 跑了一次 Prettier 机械格式化,让新增的格式检查能通过

Commit 3#

  • [[#后端:MongoDB 用户模型]]
  • [[#后端:JWT 机制]]
  • [[#后端:Axios 请求客户端]]
  • [[#前端:Axios 与 Express 的联动]]

正文#

这一版重点是 Auth 认证模块:

  1. 后端新增真实用户系统 新增了 User 模型,使用 MongoDB/Mongoose 保存用户。

  2. 注册功能 用户可以提交 username/email/password,后端会校验输入、加密密码,然后创建用户。

  3. 登录功能 用户可以用邮箱或用户名登录,后端会校验密码。

  4. 密码安全处理 密码不会明文保存,而是通过 bcryptjs 哈希后保存成 passwordHash

  5. JWT + HttpOnly Cookie 登录态 登录/注册成功后,后端签发 JWT,并写入 synctalk_session Cookie。前端不直接保存 token。

  6. 恢复登录态 新增 /api/auth/me,刷新页面后前端可以请求当前用户,判断是否仍然登录。

  7. 退出登录 新增 /api/auth/logout,会清除 Cookie。

  8. 前端接入真实 API 登录页、注册页现在会真的调用后端接口,成功后跳转到 /app/discover

  9. 保护 /app/* 页面 ProtectedRoute 会先请求当前用户;没登录就跳回 /auth/login

  10. 新增认证相关测试 后端测试覆盖注册、登录失败、读取当前用户、未登录访问、退出登录。前端测试也补了登录/注册交互状态。


后端: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.cookieres.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/me
plaintext

总结#

用户填写 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/me
plaintext

浏览器默认会限制跨域请求,避免一个恶意网站随便读取另一个网站的数据。所以后端必须明确允许前端访问。


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:7
bash

各参数含义:

  • 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/synctalk
plaintext

因为容器已经映射了端口 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/synctalk
plaintext

即可连接数据库。

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 都通过。

🗂️ This is a 📝 note in the knowledge base.

Content may be incomplete or work-in-progress.

← Back

Comment seems to stuck. Try to refresh?✨