学习问答记录
开发相关的问答记录,便于后续复习。
Q1 - 2026/05/10
问题: 当前的 Nuxt 项目是 TypeScript 的吗?如果不是怎么转?
回答摘要: 项目已经是 TypeScript 的,不需要转换。
要点:
nuxt.config.ts和tsconfig.json已存在,Nuxt 4 内置 TS 支持.vue文件中加<script setup lang="ts">即可使用 TypeScriptnuxt prepare会自动生成类型声明文件
Q2 - 2026/05/10
问题: 所有页面两边留白 20%,应该做布局组件还是直接在 app.vue 写?
回答摘要: 所有页面共用同一布局时,直接在 app.vue 里做,不需要 layouts。
要点:
app.vue是所有页面的最外层壳,直接用 Tailwind 的max-w-[80%] mx-auto包裹<NuxtPage />- Nuxt Layouts(
app/layouts/)适用于不同页面需要不同布局结构的场景 - 不要提前抽象,等真正需要多布局时再加 layouts
Q3 - 2026/05/10
问题: 如何设置统一的背景主题色?主题色要不要封装到 Tailwind?
回答摘要: 背景色在 body 上设置,主题色用 Tailwind v4 的 @theme 统一管理。
要点:
- 背景色必须在
body层级设置(main.css中body { @apply bg-background; min-height: 100vh; }),不能放在 app.vue 内容容器里 - Tailwind v4 用
@theme指令定义主题色(--color-background、--color-primary等),不是 v3 的tailwind.config.js - 主题色集中在
main.css管理,后续改主题只改这一个文件
Q4 - 2026/05/10
问题: 导航栏需要用到布局(layouts)吗?
回答摘要: 不需要,直接在 app.vue 里加导航栏。
要点:
- 所有页面共用导航栏 + 内容区域 = 同一种布局 = 在 app.vue 里做
- Layouts 适用于不同页面有不同外层结构的场景(如博客页有侧边栏、文章页全宽)
- 单一布局用 app.vue,多布局才用 layouts
Q5 - 2026/05/10
问题: 导航栏要不要封装成组件?
回答摘要: 要,放在 app/components/NavBar.vue,Nuxt 自动导入。
要点:
- 导航栏作为独立组件,职责清晰,方便维护
app/components/下的组件 Nuxt 自动注册,模板里直接用<NavBar />,不需要手动 import- 组件命名用大驼峰(
NavBar.vue),这是 Vue 社区惯例
Q6 - 2026/05/10
问题: Nuxt 里页面跳转也用 router-link 吗?
回答摘要: 不用,用 <NuxtLink>。
要点:
<NuxtLink>是 Nuxt 对router-link的封装,额外支持预加载(鼠标悬停时预加载目标页面)- 用法:
<NuxtLink to="/about">关于</NuxtLink> - 路由基于文件系统自动生成:
app/pages/index.vue→/,app/pages/about.vue→/about
Q7 - 2026/05/10
问题: 导航栏当前页面高亮怎么做?
回答摘要: <NuxtLink> 激活时自动添加 CSS class,直接写样式即可。
要点:
- 激活时自动添加
router-link-active(前缀匹配)和router-link-exact-active(精确匹配) - 导航栏一般用
router-link-exact-active,在main.css中用@apply统一处理样式 - 示例:
.router-link-exact-active { @apply text-primary font-bold; }
Q8 - 2026/05/10
问题: Prisma 配置好了,可以开始写 Nuxt 的接口了吗?
回答摘要: 可以。Prisma 实例在 server/utils/ 下会被自动导入,直接在 server/api/ 目录写接口即可。
要点:
- 接口放在
server/api/目录,Nuxt 基于文件名自动生成路由 - 文件名后缀区分 HTTP 方法:
posts.get.ts(GET)、posts.post.ts(POST)、[id].get.ts(动态路由) server/utils/下的变量在 server 端自动导入,不需要手动 import- 处理函数用
defineEventHandler()导出
Q9 - 2026/05/13
问题: 通用 API 响应接口(data、status 字段)应该写在哪?每个接口的 data 类型不同怎么处理?
回答摘要: 通用响应接口用 TypeScript 泛型定义,放在 app/types/api.ts;各接口通过泛型参数指定自己的 data 类型。
要点:
app/types/是 Nuxt 4 自动导入目录,前后端都能直接使用,适合放通用类型- 用泛型
ApiResponse<T>定义通用结构,T占位具体数据类型:interface ApiResponse<T = unknown> { status: ApiStatus; message?: string; data: T } - 每个业务接口只需定义自己的数据类型(如
PostList),然后组合:type PostListResponse = ApiResponse<PostList[]> - 服务端返回类型标注为
Promise<ApiResponse<PostList[]>>,前端useFetch<PostListResponse>()也有完整类型推导 - 核心思路:写一次公共结构 + 每个接口只关心自己的 data 类型,避免重复
Q10 - 2026/05/13
问题: 在 app/types/api.ts 定义了 APIResponse<T> 后,怎么在 server/api/ 的接口中使用?
回答摘要: 给 defineEventHandler 的返回值标注 Promise<APIResponse<具体类型>>,类型会自动导入。
要点:
app/types/下的类型 Nuxt 自动导入,APIResponse、PostList等不需要手动 import- 用法:
defineEventHandler(async (): Promise<APIResponse<PostList[]>> => { ... }),返回类型标注确保返回值结构正确 - 接口返回值必须包含
APIResponse的所有必填字段(data、status、message),缺少字段 TS 会报错 - 泛型参数
PostList[]决定了data字段的类型约束
Q11 - 2026/05/13
问题: app/types/ 下的类型在 server/api/ 中自动导入不生效,VSCode 报错找不到类型?
回答摘要: app/types/ 只在前端自动导入,server 端需要手动 import,或者把共享类型放到 shared/ 目录。
要点:
- Nuxt 前端自动导入范围:
app/下的组件、composables、types - Nuxt 后端自动导入范围:只有
server/utils/,不包含app/types/ - server 端使用
app/types/的类型需要手动 import:import type { APIResponse } from '~/types/api' - 如果类型前后端都要用,更推荐放到
shared/目录,Nuxt 4 专为前后端共享设计
Q12 - 2026/05/13
问题: 数据库 BigInt 类型的 id 用函数转成了 string,但 TS 仍然报类型错误?
回答摘要: serializeBigInt 函数签名是 <T>(data: T): T,输入输出类型相同,TS 不知道运行时类型变了。改用双泛型 <T, U> 声明输入输出类型不同即可。
要点:
- 问题根源:泛型
<T>(data: T): T表示输入什么类型就输出什么类型,Prisma 返回BigInt,TS 认为输出也是BigInt - 解决方案:函数签名改为
<T, U>(data: T): U,用两个泛型参数分别表示输入和输出类型 - 调用时指定两个类型:
serializeBigInt<typeof posts, PostList[]>(posts),TS 就知道返回的是PostList[] - 本质是类型断言——告诉 TS "我相信运行时转换后的类型是 U",
JSON.parse本身返回any,所以赋值给U不会报错
Q13 - 2026/05/13
问题: 接口返回值的 status 和 message 写死在每个接口里,这样做对吗?
回答摘要: 不推荐写死,应封装 success() / error() 工具函数统一构造响应,放在 server/utils/api/response.ts。
要点:
- 每个接口手写
status: '200', message: 'success'重复且容易出错(拼写不一致、遗漏字段) - 封装
success<T>(data, message?)和error(status, message)两个函数,所有接口统一调用 - 好处:格式统一、改响应结构只改一处、
server/utils/自动导入无需手动 import
Q14 - 2026/05/13
问题: 封装了 error() 函数后,什么场景下会触发错误响应?
回答摘要: 常见场景有参数校验失败(400)、资源不存在(404)、数据库异常(500),在接口逻辑中主动判断并返回对应错误。
要点:
- 参数校验失败:缺少必填参数、格式不对 →
error('400', '缺少文章 ID') - 资源不存在:查询结果为空 →
error('404', '文章不存在') - 数据库异常:
try/catch捕获 →error('500', '服务器内部错误') - 这些都是接口逻辑中主动判断并返回的,不是自动触发的
Q15 - 2026/05/13
问题: serializeBigInt<typeof posts, PostList[]>(posts) 在 PostList 增加字段后为什么不报类型错误?
回答摘要: serializeBigInt 内部用 JSON.parse 返回 any,双泛型 <T, U> 本质是类型断言,TS 不校验数据结构是否真的匹配 U。
要点:
JSON.parse返回any,赋值给任何类型都不会报错,所以<T, U>等价于as U- 这意味着即使查询结果缺少字段,TS 也不会提示——类型安全是假的
- 正确做法:
serializeBigInt只保留<T>(data: T): T做 BigInt 转换,类型映射在接口里用.map()显式处理 .map()逐字段赋值,缺字段 TS 立刻报错,类型检查才真正生效- 用了显式
.map()后serializeBigInt就不需要了,post.id.toString()直接处理 BigInt
Q16 - 2026/05/14
问题: 为什么 published_at 需要非空断言而 id、title 不需要?
回答摘要: Prisma schema 中 published_at 有 ?(可为空),生成类型是 Date | null,而接口类型定义为 Date(不可空),类型冲突。id 和 title 在 schema 中没有 ?,本身就是非空类型,不冲突。
要点:
- Prisma schema 的
?标记决定字段是否可为null:DateTime?→Date | null,String→string - 类型冲突发生在 Prisma 生成类型与手写接口类型不一致时:
Date | null不能赋给Date - 解决方案:非空断言
!(确信数据不为空时)、改接口类型为Date | null、或给默认值?? new Date() - 本例中只查
post_status: 'published'的数据,published_at理论上必有值,用!断言合理
Q17 - 2026/05/15
问题: 博客详情页的路由用 id 还是 slug?RESTful 接口和 Nuxt 文件结构怎么安排?
回答摘要: 用 id 查询最简单直接,URL 也用 id。RESTful 接口为 GET /api/posts/:id,Nuxt 前后端都用 [id] 动态参数,结构对称。
要点:
- 三种路由方案:纯 id(
/posts/42)、纯 slug(/posts/my-first-post,需 slug 加唯一索引)、id+slug 组合(/posts/42-my-first-post,需截取 id) - 个人博客推荐纯 id 方案,简单高效,
WHERE id = 42主键查询性能最好 - RESTful 约定:资源名用复数,操作用 HTTP 方法区分——
GET /api/posts(列表)、GET /api/posts/:id(详情)、POST /api/posts(创建)等 - Nuxt 服务端
server/api/posts/[id].get.ts用getRouterParam(event, 'id')获取动态参数,prisma.findUnique({ where: { id } })查单条 - Nuxt 前端
app/pages/posts/[id].vue用useRoute().params.id获取参数,useFetch('/api/posts/' + id)请求数据 - 前后端文件结构对称:
server/api/posts/[id].get.ts↔app/pages/posts/[id].vue,清晰一致
Q18 - 2026/05/15
问题: server/api/posts/[id].get.ts 详情页接口的写法原理是什么?
回答摘要: Nuxt 通过文件名约定生成路由,[id].get.ts 匹配 GET /api/posts/:id,处理器内用 H3 的方法获取参数、查询数据、返回响应。
要点:
- 文件名即路由:
[id]是动态参数,.get限定 HTTP 方法,所以server/api/posts/[id].get.ts处理GET /api/posts/:id defineEventHandler:H3 提供的请求处理器定义函数,event参数包含请求的所有信息(headers、params、body 等)getRouterParam(event, 'id'):从 URL 路径中取动态参数,'id'对应文件名[id],返回的是字符串prisma.findUnique:按唯一字段(主键或唯一索引)精确查一条记录,需要Number(id)把字符串转成数字createError({ statusCode: 404 }):H3 提供的错误抛出方法,自动返回对应的 HTTP 错误响应- 与列表接口对比:列表用
findMany查多条,详情用findUnique查一条,其余结构(prisma 查询、success()包装响应)模式一致
Q19 - 2026/05/15
问题: route.params 的类型是 string | string[],但 API 参数类型是 string,应该改 API 类型还是转换 params?
回答摘要: 在调用处转换 params 类型,不改 API 类型。API 参数就应该是 string,类型不匹配是调用方的问题。
要点:
route.params的值类型是string | string[],这是 Vue Router 的设计(参数可能重复),不是 API 的问题- API 函数参数定义
string是正确的,不应为了迁就调用方放宽类型 - 在页面组件中用
as string断言(单参数路由运行时必定是 string)或String()转换即可 - 核心原则:类型转换发生在边界处(页面组件获取路由参数的地方),而不是侵入到业务逻辑层
Q20 - 2026/05/19
问题: 映射 postDetail 为 Posts 类型时,content_html 字段有 undefined 的可能,但 content 没有,为什么?
回答摘要: 由 Prisma schema 中字段是否标注 ? 决定。content 没有 ? 是必填字段,content_html 有 ? 是可选字段。
要点:
- Prisma schema 中
content String @db.LongText(无?)→ 生成的 TS 类型是string,不可能为 null - Prisma schema 中
content_html String? @db.LongText(有?)→ 生成的 TS 类型是string | null,可能为 null ?对应数据库中该字段允许NULL,content_html允许为空是因为 HTML 可能在后续渲染步骤才生成- 当前
[slug].get.ts的映射代码中缺少contentHTML字段的赋值,需要补充如contentHTML: postDetail.content_html ?? ''
Q21 - 2026/05/19
问题: 开发阶段用 npx prisma migrate dev 可以吗?dev 是什么意思?
回答摘要: 可以,migrate dev 就是开发阶段专用的命令,会自动生成迁移文件、应用到数据库、并重新 generate Client。
要点:
migrate dev的完整流程:检测 schema 差异 → 生成迁移 SQL 文件 → 应用到数据库 → 自动执行prisma generatedev表示开发环境专用,生产环境对应prisma migrate deploy(只应用已有迁移,不创建新的)migrate devvsdb push:前者生成迁移文件有历史记录可回溯,后者不生成迁移文件适合快速实验- 即使开发阶段也推荐用
migrate dev,积累的迁移文件上线时会顺序执行到生产数据库,保证结构一致
Q22 - 2026/05/25
问题: Nuxt 项目给每个页面设置 title 的最佳实践是什么?
回答摘要: 使用 useHead() 或 useSeoMeta() composable,在 <script setup> 中直接调用即可,配合 titleTemplate 实现全局标题模板。
要点:
useHead({ title: '页面标题' })设置简单静态标题,Nuxt 自动导入无需手动 import- 响应式标题用
computed:useHead({ title: computed(() => data.value?.title) }) useSeoMeta()更适合 SEO 场景,可同时设置title、description、ogTitle等元标签- 在
app.vue或 layout 中用titleTemplate设全局模板:titleTemplate: (title) => title ? '${title} - Gerden Blog' : 'Gerden Blog' - 子页面只需
useHead({ title: 'xxx' }),最终显示为xxx - Gerden Blog
Q23 - 2026/05/26
问题: 某个页面不想带上 app.vue 中的 NavBar,应该怎么做?
回答摘要: 使用 Nuxt Layouts 机制,将 NavBar 移入 default layout,不需要 NavBar 的页面(如登录页)指定 blank layout。
要点:
- Nuxt Layouts 是控制页面外框布局的机制,适合区分有/无导航栏的页面
- 将
app.vue中的 NavBar 移到app/layouts/default.vue,用<slot />插入页面内容 - 创建
app/layouts/blank.vue只含<slot />,不包含 NavBar app.vue简化为<NuxtLayout><NuxtPage /></NuxtLayout>- 不需要 NavBar 的页面用
definePageMeta({ layout: 'blank' })指定空白布局 - 不指定 layout 的页面默认使用
defaultlayout(带 NavBar)
Q24 - 2026/05/27
问题: Prisma Json 类型字段赋值 TocItem[] 时 TS 报错"不能将类型 TocItem[] 分配给类型 InputJsonValue",如何解决?
回答摘要: Prisma 的 Json 字段期望 InputJsonObject 类型(带 string 索引签名的对象),TS 数组类型缺少该签名,需要通过 JSON.parse(JSON.stringify(...)) 转为普通对象并断言类型。
要点:
- Prisma schema 中
Json?类型对应的 TS 输入类型是InputJsonValue,它要求对象有[key: string]: ...索引签名 - TypeScript 数组类型
TocItem[]只有数字索引,缺少字符串索引签名,所以赋值报错 - 修复:
toc: JSON.parse(JSON.stringify(toc)) as object,先序列化再反序列化得到普通 JS 对象,断言为object让 Prisma 接受 - 运行时数组本身就是合法 JSON 值,这只是 TS 类型层面的兼容问题,不影响数据存储
Q25 - 2026/05/27
问题: 参照 posts.ts 的规范,登录的 API 模块怎么写?登录用到了 POST 方法,和 GET 不一样。
回答摘要: useAPI 底层是 useFetch,第二个参数支持 method 和 body,POST 请求只需显式指定即可。
要点:
useAPI是对useFetch的封装,第二个参数接受所有useFetch标准选项(method、body、headers等)- GET 请求可省略
method(默认 GET),POST 需要显式传{ method: 'POST', body: { ... } } - 泛型参数对应后端返回类型:登录接口返回
{ ok: true },写useAPI<APIResponse<{ ok: boolean }>> - 示例:
login(username, password) { return useAPI<...>('admin/login', { method: 'POST', body: { username, password } }) }
Q26 - 2026/05/27
问题: 登录页面的错误处理最佳实践是什么?已有拦截器但不清楚如何配合使用
回答摘要: 错误处理分两层——拦截器处理全局通用逻辑,组件处理业务特定错误。关键是 useFetch(即 useAPI)的 HTTP 错误不会 throw,需要检查返回的 error.value
要点:
useFetch的错误机制: HTTP 错误(400/401/500)不会抛异常,而是返回{ data, error },其中error是 ref。try/catch 只能捕获网络断连等非 HTTP 异常,捕获不到"用户名或密码错误"- 拦截器职责(
app/plugins/api.ts): 处理全局通用逻辑。比如非登录页收到 401 → 跳转到登录页;所有接口错误统一上报日志。拦截器对请求是"拦截"不是"消费",错误仍会传到组件 - 组件职责(
login.vue): 从useAPI返回值中解构error,检查error.value是否存在,提取error.value.data?.message显示给用户 - 具体改法:
- 组件中:
const { data, error } = await loginAPI.login(username, password),然后if (error.value) { logMsg = error.value.data?.message || '登录失败'; logError = true; return } - 拦截器中:
onResponseError里判断 URL 不是登录接口时,401 才跳转登录页,避免登录页自身 401 被误跳转
- 组件中:
- 不要在拦截器里替登录页处理登录错误: 登录失败(401)是登录页的正常业务逻辑,拦截器不应跳转或弹窗,应交给组件展示
Q27 - 2026/05/27
问题: navigateTo('/admin') 有什么用?
回答摘要: navigateTo 是 Nuxt 提供的路由导航函数,用于在代码中程序化地跳转页面,等同于用户点击了 <NuxtLink to="/admin">。
要点:
- 用途: 在 JS/TS 代码中触发页面跳转,而不是依赖用户点击链接
- 常见场景: 登录成功后跳转到管理页、表单提交完成后跳转到详情页、权限校验失败跳转到登录页
- 在登录页的具体问题: 当前登录成功后只设了
logSuccess = true显示 "Redirecting...",但没有调用navigateTo('/admin'),用户会一直卡在登录页 - 用法:
await navigateTo('/admin')——直接传目标路径即可,支持服务端和客户端
Q28 - 2026/05/27
问题: Nuxt 4 项目配置路由守卫的最佳实践是什么?
回答摘要: 使用 Route Middleware(路由中间件)实现,分为命名中间件(按需声明)和全局中间件(自动生效)。项目只有 /admin/* 需要鉴权,建议用命名中间件。
要点:
- 中间件目录: 在
app/middleware/下创建文件,Nuxt 自动注册。命名中间件文件名如auth.ts,全局中间件文件名加.global后缀如auth.global.ts - 命名中间件: 只对声明了
definePageMeta({ middleware: 'auth' })的页面生效,适合只有部分页面需要守卫的场景 - 全局中间件: 每次路由变化都执行,适合大部分页面都需要守卫的场景,在中间件内用
to.path判断排除特定页面 - 核心 API:
defineNuxtRouteMiddleware((to, from) => { ... }),返回navigateTo('/path')跳转,返回abortNavigation()阻止导航,不返回值则放行 - 执行时机: SSR 时服务端执行一次,客户端导航时客户端执行
- 推荐做法: 你的项目只有 admin 页面需要登录,用命名中间件在每个 admin 页面声明即可,不需要全局中间件
Q29 - 2026/05/28
问题: 为什么定义了防止重复登录的路由守卫但没有生效?
回答摘要: 路由中间件中调用 navigateTo() 必须 return,否则跳转不会生效。
要点:
- 根本原因: 中间件中
if (loggedIn.value) navigateTo('/')没有 return,navigateTo()调用了但返回值被丢弃,导航不会被中断,页面正常渲染 - 正确写法:
if (loggedIn.value) return navigateTo('/')——return 告诉 Nuxt 中间件要中断当前导航并执行跳转 - 中间件返回值规则:
return navigateTo('/path')= 跳转;return abortNavigation()= 阻止导航;不 return = 放行,继续正常渲染
Q30 - 2026/05/28
问题: 通过 session 判断用户登录,session 的生命周期是怎样的?
回答摘要: 项目使用 nuxt-auth-utils 模块(底层 h3 的 useSession),session 默认存在服务端内存中,通过 cookie 传递 session ID。生命周期:创建(登录)→ 携带(每次请求)→ 读取(鉴权)→ 销毁(登出/过期)。
要点:
- 创建:
setUserSession(event, { user: {...} })时,h3 生成 session ID,数据存服务端内存,通过Set-Cookie写入浏览器 - 携带: 浏览器每次请求自动带 cookie,h3 从 cookie 中取 session ID 查出 session 数据
- 读取:
requireUserSession(event)服务端鉴权;useUserSession()客户端组合式函数获取登录状态 - 销毁:
clearUserSession(event)清除服务端数据 + 删除浏览器 cookie - 过期: 默认存在内存中,服务重启 session 全部丢失,用户需重新登录
- 生产环境注意: 默认内存存储不适合生产,需要配置持久化(Redis/数据库)和过期时间(
nuxt.config.ts中session: { maxAge: 秒数 })
Q31 - 2026/05/28
问题: 调用 logout 后跳转成功,但路由守卫没生效,仍然能访问 admin 页面(session 没被清除)?
回答摘要: 服务端 session 已清除,但客户端 useUserSession() 的状态仍缓存在内存中,路由守卫读到的是旧状态。登出后需调用 useUserSession() 返回的 clear() 方法清除客户端缓存。
要点:
- 根本原因: 服务端
clearUserSession清了 session,但客户端useUserSession()组合式函数在内存中缓存了旧的loggedIn和user状态,路由守卫读的是客户端缓存而非实时请求服务端 - 登出两步缺一不可: ① 服务端清除 session(
clearUserSession(event))② 客户端清除状态(useUserSession().clear()) - 修复方式:
handleLogout中在调用loginAPI.logout()后,再调用const { clear } = useUserSession(); await clear()清除客户端缓存,然后再navigateTo('/') - 教训: Nuxt 的 session 是双层状态——服务端真实数据 + 客户端缓存快照,登出时两边都要清
Q32 - 2026/05/28
问题: 登录页面登录成功后 navigateTo('/admin') 没有执行,无法跳转到管理页面?
回答摘要: loginAPI.login() 在服务端创建了 session,但客户端 useUserSession() 的状态还是 loggedIn=false。navigateTo('/admin') 触发 admin 路由守卫时,守卫读到的是旧的客户端状态,将用户重定向回了登录页。
要点:
- 根本原因: 和登出问题一样——服务端 session 变了但客户端缓存没同步。登录成功后客户端
loggedIn仍为false - 修复: 登录成功后、跳转前,调用
await useUserSession().fetch()从服务端拉取最新 session 状态同步到客户端 - 代码:
// 登录成功分支 else { logSuccess.value = true const { fetch } = useUserSession() await fetch() // 同步客户端状态 navigateTo('/admin') // 现在守卫能读到 loggedIn=true } - 通用规律: 任何改变服务端 session 的操作(登录/登出)之后,都必须显式同步客户端状态——登录用
fetch(),登出用clear()
Q33 - 2026/05/28
问题: 想给管理页面做新增/修改博客的弹窗,应该怎么做?
回答摘要: 创建一个 Modal 组件,通过 props 控制显示/隐藏和模式(新增/编辑),表单提交时构造 FormData 匹配现有后端接口。编辑模式需要额外新增 PUT 接口。
要点:
- 组件结构:
AdminPostModal.vue,props 包含visible(是否显示)和post(编辑时传入已有数据),emit 包含close和success - 表单字段: 标题、摘要、slug、标签(逗号分隔)、状态(draft/published/hidden)、Markdown 文件上传,与
createPost的 Zod schema 对应 - 提交方式: 使用
FormData,file字段放 md 文件,meta字段放 JSON 字符串(含 title/summary/slug/post_status/tags) - 父组件集成:
admin/index.vue中维护modalVisible和editingPost状态,"+"按钮打开新增弹窗,UPDATE 按钮打开编辑弹窗,提交成功后重新拉取列表 - 修改功能前提: 目前后端只有
POST /posts(创建),修改功能需要新增PUT /posts/[id]接口 - 弹窗 UI 关键: 固定定位遮罩层(
fixed inset-0 bg-black/50)、点击遮罩关闭(@click.self)、表单在白色卡片中、z-index 确保在最上层
Q34 - 2026/06/04
问题: 如何编写根据 id 删除博客的接口?
回答摘要: 创建 server/api/posts/[id].delete.ts 路由文件,在 service 层新增 deletePost 函数,用 Prisma 的 delete 方法按 id 删除。关联的 post_tags 因 schema 中 onDelete: Cascade 会自动级联删除。
要点:
- 文件命名:
server/api/posts/[id].delete.ts,[id]是动态路由参数,.delete限定只响应 DELETE 请求 - 路由层:
getRouterParam(e, 'id')从 URL 路径中取出 id 字符串,如DELETE /api/posts/123得到"123" - BigInt 转换: posts 表主键是
@db.UnsignedBigInt,Prisma 生成的类型是bigint,需用BigInt(id)将字符串转换 - Service 层: 先用
findUnique检查文章是否存在(不存在抛 404),再用prisma.posts.delete({ where: { id } })删除 - 级联删除: schema 中
post_tags表的posts关系已设onDelete: Cascade,删 posts 时关联的 post_tags 记录会被数据库自动删除,无需手动处理 - 前端调用: 在
services/posts.ts中新增delete(id)方法,调用useAPI('posts/' + id, { method: 'DELETE' })
Q35 - 2026/06/05
问题: Nuxt SSR 项目中想在 script 里写 setInterval,但没有 onUnmounted,应该怎么清除避免内存泄漏?
回答摘要: Nuxt 中 onUnmounted 是可用的,它和 onMounted 一样只在客户端执行。setInterval 放在 onMounted 里启动,onUnmounted 里清除即可。
要点:
- 常见误解: 以为 SSR 没有
onUnmounted,实际上它一直存在,只是 SSR 阶段不会触发(和onMounted一样只在客户端运行) - 标准写法:
onMounted里setInterval,onUnmounted里clearInterval,两边都只在客户端执行,SSR 完全不涉及 - composable 场景: 推荐用
onScopeDispose代替onUnmounted,它绑定的是 Vue 的 effect scope 而非组件实例,更适合 composable 夽数 - 核心原则: 所有浏览器 API(
setInterval、setTimeout、document等)都放在onMounted里启动,onUnmounted/onScopeDispose里清理 - 类型技巧:
ReturnType<typeof setInterval>用来声明 timer 变量类型,兼容浏览器和 Node 环境
Q36 - 2026/06/06
问题: Nuxt 中已经封装了全局请求 API:app/composables/useAPI.ts。后来发现自己只是封装了 useFetch(),而 useFetch 不适合客户端交互请求,比如增删改。useFetch() 封装里有 $fetch: useNuxtApp().$api,这是 $fetch 吗?useNuxtApp().$api 又是什么?
回答摘要: useNuxtApp().$api 是项目里通过 Nuxt 插件注入的自定义 $fetch 实例,它来自 app/plugins/api.ts 中的 $fetch.create()。useAPI() 本质上仍然是一个带默认配置的 useFetch,只是告诉 useFetch 内部请求时使用自定义的 $api。读取页面初始数据适合用 useAPI/useFetch,按钮点击、表单提交、增删改等交互请求更适合直接用 $api/$fetch。
要点:
$fetch是什么:$fetch是 Nuxt 内置的请求函数,基于ofetch,可以在客户端和服务端发 HTTP 请求$api是什么:$api不是 Nuxt 原生变量,而是项目里在app/plugins/api.ts通过provide: { api }注入出来的自定义请求实例- 为什么能写
$api: Nuxt 插件provide的字段会自动加$挂到 NuxtApp 上,所以provide: { api }对应的访问方式是useNuxtApp().$api useAPI的本质:useAPI = createUseFetch(...),所以它仍然返回data/error/status/refresh这些useFetch风格的响应式状态$fetch: useNuxtApp().$api的含义: 这不是直接调用$fetch,而是把useFetch底层默认使用的$fetch替换为项目自定义的$api- 场景区分: 页面初始化、SSR 数据读取、列表详情读取适合
useAPI/useFetch;点击按钮、登录、登出、上传、删除、更新适合$api/$fetch - 错误处理差异:
useFetch/useAPI的错误通常在error.value中;$fetch/$api遇到非 2xx 响应通常会throw,更适合try/catch
项目里的对应关系:
// app/plugins/api.ts
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const api = $fetch.create({
baseURL: config.public.apiBase,
onRequest() {
// 请求拦截器
},
onResponse() {
// 响应拦截器
},
onResponseError({ response }) {
console.log('响应错误!错误代码:', response.status, ' 错误内容:', response._data)
}
})
return {
provide: {
api
}
}
})
// app/composables/useAPI.ts
export const useAPI = createUseFetch((callerOptions) => {
const config = useRuntimeConfig()
return {
baseURL: config.public.apiBase,
$fetch: useNuxtApp().$api as typeof $fetch,
...callerOptions,
}
})
推荐写法:
// 页面初始化 / SSR 数据读取:用 useAPI
const { data, error, refresh } = await useAPI('posts')
// 点击按钮 / 表单提交 / 增删改:用 $api
const { $api } = useNuxtApp()
const result = await $api('posts', {
method: 'POST',
body: formData
})
实际改造方向:
PostApi.getList()、PostApi.getDetail(slug)这类读取接口可以继续保留useAPIloginAPI.login()、loginAPI.logout()、PostApi.create()、PostApi.deletePost()这类交互请求更适合改成直接调用useNuxtApp().$apiBaseModal.vue里已有try/catch,如果 API 方法改成$api,错误捕获方式会更自然