nuxt3模块化API架构

nuxt3模块化API架构

bash 复制代码
composables/useApi.js (核心 composable)
    ↓ 统一底层
api/modules/
  ├── projects.js    →  基于 useApi() 实现
  ├── categories.js →  基于 useApi() 实现
  ├── types.js      →  类型定义
  └── api.js        →  统一导出入口
    ↓ Vue 组件调用
pages/*.vue → import { getProjects } from '~/api/projects'

优势

符合 Nuxt3 最佳实践 - 使用 composable 模式

模块化组织 - 按业务模块分离

向后兼容 - Vue 组件调用方式无需修改

代码精简 - 删除了冗余的未使用模块

统一底层 - 所有 API 调用都基于 useApi()

Vue 组件中的调用方式:

javascript 复制代码
import { getProjects } from '~/api/projects'
const response = await getProjects(params)

附上composables/useApi.js

js 复制代码
// composables/useApi.js
// 使用 Composable 模式封装 API 请求,符合 Nuxt3 最佳实践
import { ofetch } from 'ofetch'

/**
 * API Composable Hook
 * @returns {Object} API 请求方法和工具函数
 */
export const useApi = () => {
  const config = useRuntimeConfig()
  const API_BASE_URL = config.public.expressApiUrl || 'http://localhost:3001'

  // 从 localStorage 获取 token
  const getToken = () => {
    if (import.meta.client) {
      return localStorage.getItem('token')
    }
    return null
  }

  // 创建请求选项
  const createRequestOptions = (customOptions = {}) => {
    const token = getToken()
    const options = {
      baseURL: API_BASE_URL,
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json',
        ...(token && { Authorization: `Bearer ${token}` })
      },
      onRequestError: (error) => {
        console.error('❌ Request Error:', error)
        throw error
      },
      onResponseError: ({ response, error }) => {
        console.error('❌ Response Error:', error)

        // 统一错误处理
        if (response) {
          const { status } = response
          const data = response._data || {}

          switch (status) {
            case 401:
              // 未授权,清除 token
              if (import.meta.client) {
                localStorage.removeItem('token')
              }
              break
            case 403:
              console.error('没有权限访问')
              break
            case 404:
              console.error('请求的资源不存在')
              break
            case 500:
              console.error('服务器内部错误')
              break
          }

          error.message = data?.message || error.message
        } else {
          error.message = '网络连接失败,请检查网络设置'
        }

        throw error
      }
    }

    return { ...options, ...customOptions }
  }

  // 通用 API 请求函数
  const request = async (config) => {
    const { method, url, params, data, ...otherOptions } = config
    const options = createRequestOptions(otherOptions)

    // 构建完整 URL
    let fullUrl = url
    if (params) {
      const searchParams = new URLSearchParams(params)
      const queryString = searchParams.toString()
      const separator = fullUrl.includes('?') ? '&' : '?'
      fullUrl = `${url}${separator}${queryString}`
    }

    // 添加请求体
    if (data && ['post', 'put', 'patch'].includes(method?.toLowerCase())) {
      options.body = data
    }

    // 开发环境日志
    if (import.meta.client && window.location.hostname === 'localhost') {
      console.log(`🚀 API Request: ${method?.toUpperCase()} ${url}`, {
        params,
        data,
      })
    }

    try {
      const response = await ofetch(fullUrl, {
        ...options,
        method: method?.toLowerCase()
      })

      // 开发环境日志
      if (import.meta.client && window.location.hostname === 'localhost') {
        console.log(`✅ API Response: ${method?.toUpperCase()} ${url}`, response)
      }

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

  // HTTP 方法封装
  const get = (endpoint, params) => request({ method: 'GET', url: endpoint, params })
  const post = (endpoint, data) => request({ method: 'POST', url: endpoint, data })
  const put = (endpoint, data) => request({ method: 'PUT', url: endpoint, data })
  const del = (endpoint) => request({ method: 'DELETE', url: endpoint })
  const patch = (endpoint, data) => request({ method: 'PATCH', url: endpoint, data })

  // Token 管理
  const tokenManager = {
    get: getToken,
    set: (token) => {
      if (import.meta.client) {
        localStorage.setItem('token', token)
      }
    },
    remove: () => {
      if (import.meta.client) {
        localStorage.removeItem('token')
      }
    }
  }

  // 导出 ofetch 实例以供特殊情况使用
  const apiFetch = ofetch

  return {
    get,
    post,
    put,
    del,
    patch,
    tokenManager,
    request,
    apiFetch,
    getBaseUrl: () => API_BASE_URL
  }
}
相关推荐
We་ct11 分钟前
LeetCode 226. 翻转二叉树:两种解法(递归+迭代)详解
前端·算法·leetcode·链表·typescript
叫我一声阿雷吧12 分钟前
JS实现无限滚动加载列表|适配多端+性能优化【附完整可复用源码】
开发语言·javascript·性能优化
哆啦A梦158816 分钟前
Vue3魔法手册 作者 张天禹 013_pinia
前端·vue.js·typescript
哆啦A梦158819 分钟前
Vue3魔法手册 作者 张天禹 014_组件通信
前端·vue.js·typescript
木斯佳22 分钟前
前端八股文面经大全:有赞前端一面二面HR面(2026-1-13)·面经深度解析
前端·状态模式
码云数智-园园40 分钟前
Vue 3 + TypeScript 企业级项目架构实战:从0到1打造可维护的前端工程体系
前端·vue.js·typescript
CappuccinoRose1 小时前
CSS 语法学习文档(十五)
前端·学习·重构·渲染·浏览器
Marshall1511 小时前
DC-SDK 实战指南:基于 Cesium 的三维数字孪生大屏开发 前言 在当今数字孪生、智慧城市等领域的开发中,三维地图可视化已经成为核心需求。
前端
少云清2 小时前
【UI自动化测试】5_web自动化测试 _元素操作和元素信息获取
前端·web自动化测试
lyyl啊辉3 小时前
2. Vue数据双向绑定
前端·vue.js