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
  }
}
相关推荐
吴声子夜歌5 分钟前
ES6——二进制数组详解
前端·ecmascript·es6
码事漫谈33 分钟前
手把手带你部署本地模型,让你Token自由(小白专属)
前端·后端
ZC跨境爬虫38 分钟前
【爬虫实战对比】Requests vs Scrapy 笔趣阁小说爬虫,从单线程到高效并发的全方位升级
前端·爬虫·scrapy·html
爱上好庆祝38 分钟前
svg图片
前端·css·学习·html·css3
橘子编程1 小时前
JavaScript与TypeScript终极指南
javascript·ubuntu·typescript
王夏奇1 小时前
python中的__all__ 具体用法
java·前端·python
叫我一声阿雷吧1 小时前
JS 入门通关手册(45):浏览器渲染原理与重绘重排(性能优化核心,面试必考
javascript·前端面试·前端性能优化·浏览器渲染·浏览器渲染原理,重排重绘·reflow·repaint
大家的林语冰2 小时前
《前端周刊》尤大开源 Vite+ 全家桶,前端工业革命启动;尤大爆料 Void 云服务新产品,Vite 进军全栈开发;ECMA 源码映射规范......
前端·javascript·vue.js
jiayong232 小时前
第 8 课:开始引入组合式函数
前端·javascript·学习
田八2 小时前
聊聊AI的发展史,AI的爆发并不是偶然
前端·人工智能·程序员