Skip to content

开发指南

前端开发

技术栈

  • Vue 3 Composition API - 使用现代化的组合式 API 编写组件
  • TypeScript - 严格模式,确保类型安全
  • Element Plus - 基于 Material Design 的 UI 组件库
  • Tailwind CSS - 原子化 CSS 框架
  • Nuxt 4 - 全栈框架,提供 SSR 和 API 能力

组件开发

创建组件

vue
<!-- app/components/MyComponent.vue -->
<script setup lang="ts">
// 使用 TypeScript 类型
interface Props {
  title: string
  count?: number
}

// 定义 Props
const props = withDefaults(defineProps<Props>(), {
  count: 0
})

// 定义 Emits
const emit = defineEmits<{
  update: [value: number]
  delete: []
}>()

// 响应式数据
const message = ref('Hello World')
const items = ref<string[]>([])

// 计算属性
const doubledCount = computed(() => props.count * 2)

// 方法
const handleClick = () => {
  emit('update', props.count + 1)
}

// 生命周期
onMounted(() => {
  console.log('组件已挂载')
})
</script>

<template>
  <div class="my-component">
    <h1>{{ title }}</h1>
    <p>{{ message }}</p>
    <el-button @click="handleClick">
      Count: {{ count }}
    </el-button>
  </div>
</template>

<style scoped lang="css">
.my-component {
  /* 样式 */
}
</style>

使用组合式函数

typescript
// app/composables/useCounter.ts
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const doubledCount = computed(() => count.value * 2)
  
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  
  return {
    count,
    doubledCount,
    increment,
    decrement,
    reset
  }
}
vue
<!-- 在组件中使用 -->
<script setup lang="ts">
const { count, increment, decrement, reset } = useCounter(0)
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">Reset</button>
  </div>
</template>

API 请求封装

typescript
// app/composables/useApi.ts
export const useApi = () => {
  const config = useRuntimeConfig()

  const request = async <T>(url: string, options?: RequestInit): Promise<T> => {
    try {
      const response = await fetch(url, {
        ...options,
        headers: {
          'Content-Type': 'application/json',
          ...options?.headers
        }
      })

      const data = await response.json()

      if (!response.ok || !data.success) {
        throw new Error(data.message || '请求失败')
      }

      return data.data
    } catch (error) {
      console.error('API 请求错误:', error)
      throw error
    }
  }

  return { request }
}

页面开发

vue
<!-- app/pages/articles/[slug].vue -->
<script setup lang="ts">
// 获取路由参数
const route = useRoute()
const slug = route.params.slug

// 获取文章数据
const { data: article, pending, error } = await useFetch(`/api/articles/${slug}`)

// SEO 优化
useSeoMeta({
  title: article.value?.title || '文章详情',
  description: article.value?.summary,
  ogTitle: article.value?.title,
  ogImage: article.value?.cover_image
})
</script>

<template>
  <div>
    <div v-if="pending">加载中...</div>
    <div v-else-if="error">加载失败</div>
    <article v-else-if="article">
      <h1>{{ article.title }}</h1>
      <p>{{ article.summary }}</p>
      <div v-html="article.content"></div>
    </article>
  </div>
</template>

后端开发

API 路由开发

Nuxt 3 使用文件系统路由,API 文件放在 server/api/ 目录下。

typescript
// server/api/hello.get.ts
export default defineEventHandler(async (event) => {
  return {
    success: true,
    message: 'Hello World'
  }
})

带参数的 API

typescript
// server/api/articles/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  
  // 获取文章
  const article = await ArticleModel.findById(id)
  
  if (!article) {
    throw createError({
      statusCode: 404,
      statusMessage: '文章不存在'
    })
  }
  
  return {
    success: true,
    data: article
  }
})

POST 请求

typescript
// server/api/articles/create.post.ts
export default defineEventHandler(async (event) => {
  try {
    // 读取请求体
    const body = await readBody(event)
    
    // 获取当前用户(需要认证中间件)
    const user = event.context.user
    
    // 参数验证
    if (!body.title || !body.content) {
      return {
        success: false,
        message: '标题和内容不能为空'
      }
    }
    
    // 创建文章
    const article = await ArticleModel.create({
      ...body,
      author_id: user.userId
    })
    
    return {
      success: true,
      data: article,
      message: '创建成功'
    }
  } catch (error) {
    console.error('创建文章失败:', error)
    return {
      success: false,
      message: '创建失败'
    }
  }
})

中间件开发

typescript
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  // 跳过登录相关路径
  if (['/api/auth/login'].includes(event.path)) {
    return
  }

  // 获取 Token
  const cookies = parseCookies(event)
  const token = cookies.auth_token

  if (token) {
    const payload = verifyToken(token)
    if (payload) {
      // 验证用户状态
      const user = await User.getSafeUserById(payload.userId)
      if (user && user.status == 'active') {
        event.context.user = user
        return
      }
    }
  }

  // 认证失败
  if (event.path.startsWith('/api/admin/')) {
    throw createError({
      statusCode: 401,
      message: '无权限访问管理API'
    })
  }
})

数据模型开发

typescript
// server/models/Article.ts
import { db } from 'server/database/config'

export const ArticleModel = {
  // 查找所有文章
  async findAll(limit: number = 20, offset: number = 0) {
    const [rows] = await db.query(`
      SELECT a.*, c.name as category_name
      FROM articles a
      LEFT JOIN categories c ON a.category_id = c.id
      ORDER BY a.created_at DESC
      LIMIT ? OFFSET ?
    `, [limit, offset])
    return rows
  },

  // 根据 ID 查找文章
  async findById(id: number) {
    const [rows] = await db.query(`
      SELECT a.*, c.name as category_name
      FROM articles a
      LEFT JOIN categories c ON a.category_id = c.id
      WHERE a.id = ?
    `, [id])
    return rows[0]
  },

  // 创建文章
  async create(data: any) {
    const [result] = await db.query(`
      INSERT INTO articles (title, content, summary, author_id, category_id)
      VALUES (?, ?, ?, ?, ?)
    `, [data.title, data.content, data.summary, data.author_id, data.category_id])
    
    return result.insertId
  },

  // 更新文章
  async update(id: number, data: any) {
    const [result] = await db.query(`
      UPDATE articles 
      SET title = ?, content = ?, summary = ?, category_id = ?, updated_at = NOW()
      WHERE id = ?
    `, [data.title, data.content, data.summary, data.category_id, id])
    
    return result.affectedRows > 0
  },

  // 删除文章
  async delete(id: number) {
    const [result] = await db.query('DELETE FROM articles WHERE id = ?', [id])
    return result.affectedRows > 0
  }
}

数据库操作

使用 MySQL 插件

typescript
import { db } from 'server/database/config'

export default defineEventHandler(async (event) => {
  // 查询
  const [users] = await db.query('SELECT * FROM users WHERE status = ?', ['active'])
  
  // 插入
  const [result] = await db.query(
    'INSERT INTO users (username, email, password) VALUES (?, ?, ?)',
    ['test', 'test@example.com', 'hashed_password']
  )
  
  // 更新
  await db.query('UPDATE users SET nickname = ? WHERE id = ?', ['New Name', userId])
  
  // 删除
  await db.query('DELETE FROM users WHERE id = ?', [userId])
  
  return { success: true }
})

使用事务

typescript
import { db } from 'server/database/config'

export default defineEventHandler(async (event) => {
  const connection = await db.getConnection()
  
  try {
    await connection.beginTransaction()
    
    // 执行多个操作
    await connection.query('INSERT INTO articles (...) VALUES (...)', [...])
    await connection.query('UPDATE categories SET count = count + 1 WHERE id = ?', [categoryId])
    
    await connection.commit()
    return { success: true }
  } catch (error) {
    await connection.rollback()
    throw error
  } finally {
    connection.release()
  }
})

代码规范

TypeScript 规范

typescript
// ✅ 好的做法
interface User {
  id: number
  username: string
  email: string
}

const getUser = async (id: number): Promise<User> => {
  // ...
}

// ❌ 避免
const getUser = async (id) => {
  // 缺少类型注解
}

命名规范

类型规范示例
组件PascalCaseUserProfile.vue
工具函数camelCaseformatDate()
常量UPPER_SNAKE_CASEAPI_BASE_URL
接口/类型PascalCaseUserProfile
文件名kebab-caseuser-profile.ts

注释规范

typescript
/**
 * 用户认证服务
 * 提供用户登录、注册、Token 验证等功能
 */
export class AuthService {
  /**
   * 用户登录
   * @param username 用户名
   * @param password 密码
   * @returns 登录结果
   */
  static async login(username: string, password: string): Promise<LoginResult> {
    // ...
  }
}

调试技巧

开发工具

bash
# 类型检查
pnpm run type-check

# 代码检查
pnpm run lint

# 自动修复
pnpm run lint:fix

日志调试

typescript
export default defineEventHandler(async (event) => {
  console.log('请求路径:', event.path)
  console.log('请求参数:', getQuery(event))
  console.log('请求体:', await readBody(event))
  
  // ...
})

测试

TODO: 添加测试框架和示例

下一步