全栈论坛笔记:异步、HTTP 与安全基础

8 阅读6分钟

前言

这份笔记是我在完成自己的全栈论坛项目之后,写给自己的一个技术底稿。

项目从零开始: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
  • 如果不 awaitdata 会是一个 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 模型,让论坛真正拥有持久化存储——那会是我下一次追问"为什么"的起点。