前言
这份笔记是我在完成自己的全栈论坛项目之后,写给自己的一个技术底稿。
项目从零开始:Vue3 前端用 localStorage 模拟一切,到后来搭起 Express 后端,把帖子、评论、用户注册登录一个个接口联调通过,中间踩过数不清的坑。写到最后,我发现自己会写 async/await、会拼接请求头、会处理错误,但如果被问到"为什么必须加 await?"、"JSON.stringify 漏了会发生什么?"、"res.ok 到底判断了什么?",我其实说不清楚。
我知道怎么用,但不知道底层逻辑——这种感觉让我对自己写出的代码一直缺少真正的掌控感。
所以,我用自己项目里的真实代码做例子,把那些天天用却一直模糊的概念拆开,记成这篇笔记。它不追求全面,只围绕我在论坛项目中必须理解的异步通信、HTTP 基础和安全习惯展开。每一条都绑定着我亲手敲过的代码,是我从"能用"到"能解释"的一个存档。
1. 为什么必须用 async/await 以及它的底层是什么
我的真实代码
async function handleLogin(){
const result = await userStore.userLogin(snoId.value, numId.value)
if(result.success){
router.push('/')
}else{
alert(result.msg)
}
}
底层解释
- JS 单线程:一个时间点只能做一件事。网络请求要等几百毫秒,如果傻等,页面就卡死了。
- 事件循环:JS 把网络请求丢给浏览器底层去处理,自己继续执行后面的代码。等底层拿到结果,再把回调推进队列。
async:声明函数返回的一定是Promise。await:暂停当前函数的执行,等右边的Promise完成,拿到结果再继续往下。它本质是.then()的语法糖。
如果不加 await 会怎样
// 不加 await,result 是 Promise 对象,不是 {success, msg}
const result = userStore.userLogin(snoId.value, numId.value)
// result.success 是 undefined,永远进不去 if
原则:调用任何 async 函数,只要我想用它的返回值,就加 await。
2. headers:{'Content-Type':'application/json'} 和 body:JSON.stringify
我的真实代码
const res = await fetch('http://localhost:3001/api/users/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sno: snoId, password: numId, name: nameId })
})
为什么这两个必须同时出现
headers告诉后端:“我发的是 JSON 格式的数据,不是表单或纯文本”。后端express.json()看到这个头,才会解析req.body。JSON.stringify把 JS 对象变成 JSON 字符串。我写的{sno: '...', password: '...'}是 JS 对象,不能直接放进网络请求体,必须转成字符串。
常见错误
忘记 JSON.stringify,直接传对象,会变成 [object Object] 发过去,后端无法解析,直接报错。
3. res.ok 和 errorData
我的真实代码
if (!res.ok) {
const errorData = await res.json()
return { success: false, msg: errorData.error || '登录失败' }
}
原理
res.ok是fetch的内置属性,当 HTTP 状态码在 200-299 范围内时为true。否则为false。- 后端所有错误响应都遵循同一个契约:
{ error: '具体错误信息' }。前端从errorData.error取出字符串,展示给用户。错误信息从后端来,前端不再自己编。
4. const data = await res.json()
我的真实代码
const data = await res.json()
currentUser.value = data.user
为什么还要 await
res.json()是异步方法,它需要读取完整响应体,然后解析为 JS 对象。必须加await。- 如果不
await,data会是一个 Promise,而不是对象。
5. try/catch 的作用
我的错误处理模式
async function userLogin(snoId, numId){
try{
const res = await fetch(...)
if(!res.ok){
const errorData = await res.json()
return { success: false, msg: errorData.error || '登录失败' }
}
const data = await res.json()
currentUser.value = data.user
return { success: true, msg: '' }
}catch(err){
console.error('用户登录失败', err)
return { success: false, msg: '网络错误,请稍后重试' }
}
}
为什么必须这样写
try块:放可能出错的代码(网络请求、JSON 解析、状态码错误)。catch块:捕获所有网络层面的错误,比如后端根本没响应、网络断线。这些错误前端不能控制,但必须友好处理,防止页面崩溃。
原则:所有 async/await 请求函数,都包在 try/catch 里,UI 永远不崩。
6. const { password: _, ...safeUser } = key 剔除密码
我的真实代码
const key = users.find(u => u.sno === sno)
if (!key) return res.status(404).json({ error: '学号不存在' })
const { password: _, ...safeUser } = key // 剔除密码
res.json({ user: safeUser })
解释
password: _把key.password提取出来,重命名成_(下划线表示不用的变量)。...safeUser(rest 运算符)把key对象中除password外的所有属性收集到新对象safeUser中。- 等价于手写
{ sno: key.sno, name: key.name }。
为什么这样写:安全红线,后端永远不把密码字段返回给前端。
7. res.status(201).json(...)
我的真实代码
res.status(201).json({
user: {
sno: newUser.sno,
name: newUser.name
}
})
为什么是 201 不是 200
- 201 Created:表示资源创建成功。
- 200 OK:请求成功但语义模糊。
- RESTful 惯例:POST 创建资源返回 201。
8. app.use(cors()) 和 app.use(express.json())
我的 index.js 骨架
const express = require('express')
const cors = require('cors')
const app = express()
app.use(cors()) // 允许跨域
app.use(express.json()) // 解析 JSON 请求体
为什么必须放在所有路由之前
- 中间件执行顺序:Express 从上往下执行。请求先经过
cors()(加响应头,告诉浏览器允许跨域),再经过express.json()(解析请求体),然后才是我的路由。 cors():不加,前端发来的请求会被浏览器拦截,报 CORS 错误。express.json():不加,req.body是undefined,后端拿不到任何请求数据。
9. await 在 Pinia 的 action 里不起作用?这是已经踩过的坑
我在 Pinia setup store 的顶层写了一个立即执行的异步 IIFE,试图初始化时拉取后端数据:
(async () => {
const res = await fetch('http://localhost:3001/api/posts')
const data = await res.json()
})()
这个写法导致 Vue 响应式初始化错乱,ref 报错。根源:Pinia 的 defineStore 回调函数是同步执行的,不能在里面直接放异步 IIFE。异步逻辑应放在 action 里,或者在组件 onMounted 里调用。
结尾
写完这篇笔记的时候,我的论坛已经跑通了帖子 CRUD、评论的嵌套路由、用户注册和登录,所有接口遵循统一的错误契约,密码不会返回给前端,currentUser 存在 Pinia 里,退出登录时只清零本地状态。数据还放在内存数组里,服务重启就丢——这是我接下来要用 MongoDB 解决的最后一块拼图。
回头去看,刚动手时连 CORS 为什么报错都不明白,现在至少能对着 app.use(cors()) 和 express.json() 说出它们在中介链条里的位置。再遇到类似的异步调用,我不需要去翻以前的代码抄一遍,而是能根据请求的目的主动决定:这里要不要 await,那里要不要 JSON.stringify,错误信息该从后端哪个字段拿。
如果非要总结一条最重要的收获,那就是:全栈开发中,最大的安全感不是来自背下所有 API,而是知道自己遇到的每一个报错、每一个必须加的语法,都有底层原因。 当这些原因被你一个个亲手拆开,那些之前看似"模仿"的操作,就变成了你可以随时复用的工具。
这份笔记会随着项目一起更新。下一步,我要把 data/posts.js 换成 Mongoose 模型,让论坛真正拥有持久化存储——那会是我下一次追问"为什么"的起点。