yuwen
2025-11-12 cddf6501b6546d78dacb3a57faa34ae7e74293b2
feat: add axios store and request utility hook
修改1個檔案
新增2個檔案
203 ■■■■■ 已變更過的檔案
package.json 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
src/hooks/useRequest.ts 162 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
src/stores/axios.ts 40 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
package.json
@@ -19,6 +19,7 @@
  },
  "dependencies": {
    "@tailwindcss/vite": "^4.1.16",
    "axios": "^1.13.2",
    "pinia": "^3.0.3",
    "tailwind-merge": "^3.4.0",
    "tailwindcss": "^4.1.16",
src/hooks/useRequest.ts
比對新檔案
@@ -0,0 +1,162 @@
import { AxiosError } from 'axios'
import { useAxiosStore } from '@/stores/axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
const baseUrl = import.meta.env.VITE_BASE_API
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
/** API 模式 */
export enum API_MODE {
  PROD = 'prod',
  TEST = 'test',
}
/** 請求成功後,後端 API 統一回傳的格式 */
export type ResponseWrapper<D> = {
  code: string
  msg: string
  data: D
  sysDate: string
}
/** 回傳給調用者的成功資料 */
type SuccessResponse<Data> = {
  success: true
  response: AxiosResponse
  data: Data
  msg: string
  code: string
  sysDate: string
}
/** 回傳給調用者的失敗資料格式 */
type FailureResponse = {
  success: false
  response: AxiosResponse
  msg: string
  data: null
  code: string
  sysDate: string
}
export type ApiResponse<Data = unknown> = SuccessResponse<Data> | FailureResponse
interface RequestInfo<ResponseData> {
  /** 執行請求的函式 */
  executor: (axiosInstance: AxiosInstance) => Promise<AxiosResponse<ResponseWrapper<ResponseData>>>
  /** 請求位址 */
  url: string
  /** HTTP 方法 */
  method: HttpMethod
  /** 失敗時是否跳出彈窗 */
  showFailurePopup: boolean
}
function getMode(apiModeEnum?: API_MODE) {
  let modeValue: API_MODE[keyof API_MODE]
  switch (true) {
    case import.meta.env.PROD:
      modeValue = API_MODE.PROD
      break
    case !!apiModeEnum:
      modeValue = apiModeEnum
      break
    default:
      modeValue = API_MODE.TEST
      break
  }
  return modeValue === API_MODE.PROD ? null : API_MODE.TEST
}
/**
 * 方法: 回傳一個可以由 useRequest().apiRequest 使用,並進行非同步請求的函式
 * @param axiosRequestConfig 應至少包含:
 * @ url: 路徑
 * @ method: http 方法
 * @ mode: 是否啟用 mock server worker, 應為空字串 (正式環境) 或 'test' (測試環境)
 * @example
 * function doMyRequest(data) {
 *  return defineRequest(
 *   { url: '/myRequestUrl', method: 'POST', mode: API_MODE.PROD, data })}
 */
export function defineRequest<ResponseData>(
  axiosRequestConfig: AxiosRequestConfig & {
    url: string
    method: HttpMethod
    mode?: API_MODE
  },
  additionalConfig?: {
    showFailurePopup?: boolean
  },
): RequestInfo<ResponseData> {
  const mode = getMode(axiosRequestConfig.mode)
  return {
    executor: (axiosInstance) => {
      return axiosInstance.request({
        ...axiosRequestConfig,
        url: `${baseUrl}${axiosRequestConfig.url}`,
        data: axiosRequestConfig.data ?? {},
        params: { ...axiosRequestConfig.params, mode },
      })
    },
    url: axiosRequestConfig.url,
    method: axiosRequestConfig.method,
    showFailurePopup: additionalConfig?.showFailurePopup ?? true,
  }
}
export default function useRequest() {
  const axiosStore = useAxiosStore()
  /** 方法: 執行已透過 defineRequest 定義好的非同步函式,並取得回傳結果
   * @ 成功時,可以解構出 success, data, msg, code, sysDate, response 等屬性,其中 data 為 response.data.data
   * @example
   * const { success, data, msg, code, response } = await apiRequest(doMyRequest(payload))
   * if (success) {...}
   * else {...}
   */
  async function apiRequest<ResponseData>(
    request: RequestInfo<ResponseData>,
  ): Promise<ApiResponse<ResponseData>> {
    return (
      request
        // 執行請求
        .executor(axiosStore.axiosInstance)
        .then((response) => {
          const { data, msg, code, sysDate } = response.data
          // 若 code 為 200 ,則回傳資料
          if (code === '200') {
            return {
              success: true,
              data,
              msg,
              code,
              sysDate,
              response,
            } as SuccessResponse<ResponseData>
          } else {
            // 否則回傳失敗
            return { success: false, data: null, msg, code, sysDate, response } as FailureResponse
          }
        })
        // 請求失敗時,回傳失敗
        .catch((err: AxiosError) => {
          const sysDate = new Date().toISOString()
          const msg = err.message
          const code = err.code ?? ''
          return {
            success: false,
            data: null,
            msg,
            code,
            sysDate,
            response: err.response,
          } as FailureResponse
        })
    )
  }
  return { apiRequest }
}
src/stores/axios.ts
比對新檔案
@@ -0,0 +1,40 @@
import { defineStore } from 'pinia'
import axios from 'axios'
import { ref } from 'vue'
import type {
  CreateAxiosDefaults,
  InternalAxiosRequestConfig,
  AxiosResponse,
  AxiosError,
} from 'axios'
/** 請求逾時豪秒數 */
const TIMEOUT_MILLISECONDS = 3000 * 60
/** axios 設定 */
const axiosConfig: CreateAxiosDefaults = {
  timeout: TIMEOUT_MILLISECONDS,
  headers: { 'Content-Type': 'application/json;charset=UTF-8' },
}
export const useAxiosStore = defineStore('axios', () => {
  const axiosInstance = ref(axios.create(axiosConfig))
  const axiosInterceptor = {
    onRequest(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig {
      return config
    },
    onResponse(response: AxiosResponse): AxiosResponse {
      return response
    },
    onError(error: AxiosError): Promise<AxiosError> {
      return Promise.reject(error)
    },
  }
  axiosInstance.value.interceptors.request.use(axiosInterceptor.onRequest, axiosInterceptor.onError)
  axiosInstance.value.interceptors.response.use(
    axiosInterceptor.onResponse,
    axiosInterceptor.onError,
  )
  return { axiosInstance }
})