07-网络请求封装

4 阅读2分钟

🌐 Taro+Vue3 入门(七):网络请求封装

系列导读:小程序请求和 Web 的 fetch/axios 不同,需要用 Taro.request。 本文教你封装统一请求层、自动鉴权和 API 模块化。


🔧 1. 统一封装

// src/utils/request.ts
import Taro from '@tarojs/taro'
import { useAuthStore } from '@/stores/auth'

const BASE_URL = 'https://api.example.com'

interface RequestConfig {
  url: string
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
  data?: any
  loading?: boolean
  toast?: boolean
}

interface ApiResponse<T = any> {
  code: number
  message: string
  data: T
}

export async function request<T = any>(config: RequestConfig): Promise<T> {
  const { url, method = 'GET', data, loading = false, toast = true } = config

  if (loading) Taro.showLoading({ title: '加载中...', mask: true })

  // 自动注入 Token
  const header: Record<string, string> = { 'Content-Type': 'application/json' }
  const authStore = useAuthStore()
  if (authStore.token) {
    header['Authorization'] = `Bearer ${authStore.token}`
  }

  try {
    const res = await Taro.request<ApiResponse<T>>({
      url: `${BASE_URL}${url}`,
      method,
      data,
      header,
      timeout: 10000,
    })

    if (loading) Taro.hideLoading()

    if (res.statusCode !== 200) throw new Error(`HTTP ${res.statusCode}`)

    const { code, message, data: resData } = res.data

    if (code === 401) {
      authStore.logout()
      Taro.reLaunch({ url: '/pages/login/index' })
      throw new Error('登录已过期')
    }

    if (code !== 0) throw new Error(message || '请求失败')
    return resData
  } catch (error: any) {
    if (loading) Taro.hideLoading()
    if (toast) Taro.showToast({ title: error?.message || '网络异常', icon: 'none' })
    throw error
  }
}

export const http = {
  get: <T>(url: string, data?: any, config?: Partial<RequestConfig>) =>
    request<T>({ url, method: 'GET', data, ...config }),
  post: <T>(url: string, data?: any, config?: Partial<RequestConfig>) =>
    request<T>({ url, method: 'POST', data, ...config }),
  put: <T>(url: string, data?: any, config?: Partial<RequestConfig>) =>
    request<T>({ url, method: 'PUT', data, ...config }),
  delete: <T>(url: string, data?: any, config?: Partial<RequestConfig>) =>
    request<T>({ url, method: 'DELETE', data, ...config }),
}

📡 2. API 模块化

// src/api/product.ts
import { http } from '@/utils/request'

export interface Product {
  id: string
  name: string
  price: number
  image: string
  description: string
}

export const productApi = {
  getList: (params: { page: number; pageSize: number; category?: string }) =>
    http.get<{ list: Product[]; total: number }>('/api/products', params),

  getDetail: (id: string) =>
    http.get<Product>(`/api/products/${id}`, undefined, { loading: true }),

  search: (keyword: string) =>
    http.get<Product[]>('/api/products/search', { keyword }),
}

🪝 3. 请求 Composable

// src/composables/useRequest.ts
import { ref, type Ref } from 'vue'

export function useRequest<T>(fetcher: () => Promise<T>) {
  const data: Ref<T | null> = ref(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)

  async function run() {
    loading.value = true
    error.value = null
    try {
      data.value = await fetcher() as any
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  run()
  return { data, loading, error, refresh: run }
}
<!-- 使用 -->
<script setup lang="ts">
import { useRequest } from '@/composables/useRequest'
import { productApi } from '@/api/product'

const { data, loading, refresh } = useRequest(() =>
  productApi.getList({ page: 1, pageSize: 20 })
)

usePullDownRefresh(async () => {
  await refresh()
  Taro.stopPullDownRefresh()
})
</script>

<template>
  <nut-skeleton :loading="loading" :row="5" animated>
    <ProductCard v-for="p in data?.list" :key="p.id" v-bind="p" />
  </nut-skeleton>
</template>

✅ 本篇小结 Checklist

  • 封装统一请求层(Token/错误处理/Loading)
  • 按模块组织 API
  • 会写请求 Composable

本文是「Taro+Vue3 入门」系列第 7 篇,共 10 篇。


📝 作者:NIHoa | 系列:Taro+Vue3入门系列 | 更新日期:2024-05-07