10.1 项目架构概览
本章实战构建一个任务管理应用(类似简化版 Linear),包含:用户注册/登录、任务的 CRUD、文件附件上传、响应式暗色 UI。
# 创建项目
npx nuxi@latest init taskapp
cd taskapp
# 安装依赖
npm install @nuxt/ui @pinia/nuxt
npm install drizzle-orm better-sqlite3
npm install nuxt-auth-utils
npm install --save-dev drizzle-kit @types/better-sqlite3
export default defineNuxtConfig({
modules: [
'@nuxt/ui',
'@pinia/nuxt',
'nuxt-auth-utils'
],
runtimeConfig: {
sessionPassword: process.env.NUXT_SESSION_PASSWORD,
public: {
appName: 'TaskApp'
}
}
})
10.2 数据库:Drizzle ORM
Drizzle ORM 是一个轻量级、类型安全的 TypeScript ORM,在 Nuxt 服务端完美运行:
import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core'
// 用户表
export const users = sqliteTable('users', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text('email').notNull().unique(),
name: text('name').notNull(),
password: text('password').notNull(), // 存储 bcrypt hash
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => new Date())
})
// 任务表
export const tasks = sqliteTable('tasks', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
title: text('title').notNull(),
description: text('description'),
status: text('status', { enum: ['todo', 'in_progress', 'done'] })
.default('todo'),
priority: text('priority', { enum: ['low', 'medium', 'high'] })
.default('medium'),
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
dueDate: integer('due_date', { mode: 'timestamp' }),
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => new Date())
})
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
// 单例模式(server/ 目录的工具函数可自动导入)
let _db: ReturnType<typeof drizzle> | null = null
export function useDb() {
if (!_db) {
const sqlite = new Database('./data.db')
_db = drizzle(sqlite)
}
return _db
}
10.3 用户认证:nuxt-auth-utils
import { eq } from 'drizzle-orm'
import { hash } from 'bcrypt'
export default defineEventHandler(async (event) => {
const { email, name, password } = await readBody(event)
// 检查邮箱是否已注册
const db = useDb()
const existing = await db.select().from(users)
.where(eq(users.email, email)).get()
if (existing) {
throw createError({ statusCode: 409, message: '邮箱已注册' })
}
// 创建用户
const hashedPassword = await hash(password, 12)
const newUser = await db.insert(users).values({
email, name, password: hashedPassword
}).returning().get()
// 设置 Session(nuxt-auth-utils 提供)
await setUserSession(event, {
user: { id: newUser.id, name: newUser.name, email: newUser.email }
})
return { success: true }
})
import { eq } from 'drizzle-orm'
import { compare } from 'bcrypt'
export default defineEventHandler(async (event) => {
const { email, password } = await readBody(event)
const db = useDb()
const user = await db.select().from(users)
.where(eq(users.email, email)).get()
if (!user || !(await compare(password, user.password))) {
throw createError({ statusCode: 401, message: '邮箱或密码错误' })
}
await setUserSession(event, {
user: { id: user.id, name: user.name, email: user.email }
})
return { success: true }
})
// middleware/auth.ts
export default defineNuxtRouteMiddleware(async () => {
const { loggedIn } = useUserSession()
if (!loggedIn.value) {
return navigateTo('/login')
}
})
// pages/dashboard.vue — 使用中间件
definePageMeta({ middleware: 'auth' })
10.4 任务 API
import { eq, and, desc } from 'drizzle-orm'
export default defineEventHandler(async (event) => {
const session = await getUserSession(event)
if (!session.user) throw createError({ statusCode: 401 })
const query = getQuery(event)
const { status, priority } = query
const db = useDb()
// 动态构建查询条件
const conditions = [eq(tasks.userId, session.user.id)]
if (status) conditions.push(eq(tasks.status, status as string))
if (priority) conditions.push(eq(tasks.priority, priority as string))
return db.select().from(tasks)
.where(and(...conditions))
.orderBy(desc(tasks.createdAt))
.all()
})
10.5 文件上传
import { writeFile, mkdir } from 'node:fs/promises'
import { join } from 'node:path'
export default defineEventHandler(async (event) => {
const session = await getUserSession(event)
if (!session.user) throw createError({ statusCode: 401 })
// 读取 multipart 表单数据
const formData = await readFormData(event)
const file = formData.get('file') as File
if (!file) throw createError({ statusCode: 400, message: '未提供文件' })
// 验证文件类型和大小
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
if (!allowedTypes.includes(file.type)) {
throw createError({ statusCode: 400, message: '不支持的文件类型' })
}
if (file.size > 5 * 1024 * 1024) { // 5MB 限制
throw createError({ statusCode: 400, message: '文件不能超过 5MB' })
}
// 保存文件(生产环境应使用 S3 等对象存储)
const ext = file.name.split('.').pop()
const filename = `${crypto.randomUUID()}.${ext}`
const uploadDir = join(process.cwd(), 'public/uploads')
await mkdir(uploadDir, { recursive: true })
await writeFile(join(uploadDir, filename), Buffer.from(await file.arrayBuffer()))
return { url: `/uploads/${filename}` }
})
<script setup lang="ts">
const uploading = ref(false)
const uploadedUrl = ref('')
async function handleFileChange(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0]
if (!file) return
const formData = new FormData()
formData.append('file', file)
uploading.value = true
try {
const { url } = await $fetch<{ url: string }>('/api/upload', {
method: 'POST',
body: formData
})
uploadedUrl.value = url
} finally {
uploading.value = false
}
}
</script>
<template>
<div>
<input type="file" accept="image/*" @change="handleFileChange" />
<div v-if="uploading">上传中...</div>
<img v-if="uploadedUrl" :src="uploadedUrl" alt="已上传图片" />
</div>
</template>
10.6 部署到 Vercel
# 安装 Vercel 适配器
npm install --save-dev @vercel/nuxt
# 本地构建测试
npm run build
npm run preview
export default defineNuxtConfig({
// Vercel 边缘函数(可选,更快响应)
nitro: {
preset: 'vercel-edge'
}
})
{
"buildCommand": "npm run build",
"outputDirectory": ".output/public",
"framework": "nuxtjs",
"env": {
"NUXT_SESSION_PASSWORD": "@nuxt-session-password"
}
}
# 方式一:通过 Vercel CLI 部署
npm install -g vercel
vercel --prod
# 方式二:连接 GitHub 仓库(推荐)
# 在 vercel.com 中导入仓库,每次 push 自动部署
git add .
git commit -m "feat: initial commit"
git push origin main
10.7 Netlify 部署
[build]
command = "npm run build"
publish = ".output/public"
[build.environment]
NODE_VERSION = "20"
[[redirects]]
from = "/*"
to = "/.netlify/functions/server"
status = 200
10.8 生产检查清单
环境变量
所有敏感信息(数据库连接、JWT 密钥、API Keys)通过环境变量传入,不提交到 git。在 Vercel/Netlify 的 Environment Variables 中配置。
错误处理
创建 error.vue 处理全局错误,API 路由使用 createError() 返回标准错误格式,客户端用 useError() 捕获。
SEO 优化
每个页面使用 useSeoMeta() 设置 title/description/og:image,配置 robots.txt 和 sitemap(@nuxtjs/sitemap 模块)。
性能优化
使用 <NuxtImage> 自动优化图片,路由懒加载,合理设置 routeRules 缓存策略,开启 Nuxt DevTools 分析打包体积。
数据库迁移
生产环境使用 PostgreSQL(Neon、Supabase)或 MySQL(PlanetScale)替代 SQLite,Drizzle Kit 管理迁移文件:npx drizzle-kit migrate。
学习路径完成
恭喜完成 Vue 3 / Nuxt 全栈开发教程!你已经掌握了从 Composition API 基础到 Nuxt 全栈部署的完整技能链。建议接下来:深入阅读 Vue 3 官方文档、实践 Nuxt 官方示例,并尝试将所学知识应用到真实项目中。