07-网络请求与数据处理

3 阅读2分钟

🌐 Taro 从零到一(七):网络请求与数据处理

系列导读:小程序连接后端从 Taro.request 开始。 本文教你封装统一请求层,处理 Token、刷新、错误、loading。


🛠 1. Taro.request 基础

import Taro from '@tarojs/taro'

// 基础请求
const res = await Taro.request({
  url: 'https://api.example.com/products',
  method: 'GET',
  header: { 'Content-Type': 'application/json' },
})
console.log(res.data)

🔧 2. 统一封装

// src/utils/request.ts
import Taro from '@tarojs/taro'
import useAuthStore from '@/store/useAuthStore'

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

interface RequestOptions {
  url: string
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
  data?: any
  header?: Record<string, string>
  loading?: boolean    // 是否显示 loading
  toast?: boolean      // 是否显示错误提示
}

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

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

  // 自动注入 Token
  const token = useAuthStore.getState().token
  if (token) {
    header['Authorization'] = `Bearer ${token}`
  }

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

  try {
    const res = await Taro.request<ApiResponse<T>>({
      url: `${BASE_URL}${url}`,
      method,
      data,
      header: { 'Content-Type': 'application/json', ...header },
      timeout: 10000,
    })

    // 隐藏 loading
    if (loading) Taro.hideLoading()

    // HTTP 状态码检查
    if (res.statusCode !== 200) {
      throw new Error(`HTTP ${res.statusCode}`)
    }

    // 业务码检查
    const { code, message, data: resData } = res.data

    if (code === 401) {
      // Token 过期,跳转登录
      useAuthStore.getState().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()

    const msg = error?.message || '网络异常,请稍后重试'
    if (toast) {
      Taro.showToast({ title: msg, icon: 'none', duration: 2000 })
    }
    throw error
  }
}

// 快捷方法
export const http = {
  get: <T>(url: string, data?: any, options?: Partial<RequestOptions>) =>
    request<T>({ url, method: 'GET', data, ...options }),

  post: <T>(url: string, data?: any, options?: Partial<RequestOptions>) =>
    request<T>({ url, method: 'POST', data, ...options }),

  put: <T>(url: string, data?: any, options?: Partial<RequestOptions>) =>
    request<T>({ url, method: 'PUT', data, ...options }),

  delete: <T>(url: string, data?: any, options?: Partial<RequestOptions>) =>
    request<T>({ url, method: 'DELETE', data, ...options }),
}

export default request

📡 3. API 模块化

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

export interface Product {
  id: string
  name: string
  price: number
  image: string
  description: string
  sales: number
  tags: string[]
}

interface ProductListParams {
  page: number
  pageSize: number
  category?: string
  keyword?: string
}

interface PaginatedList<T> {
  list: T[]
  total: number
  page: number
}

export const productApi = {
  // 商品列表
  getList: (params: ProductListParams) =>
    http.get<PaginatedList<Product>>('/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 }),
}
// src/api/auth.ts
import { http } from '@/utils/request'

export const authApi = {
  // 微信登录
  loginByWechat: (code: string) =>
    http.post<{ token: string; user: User }>('/api/auth/wechat', { code }),

  // 手机号登录
  loginByPhone: (phone: string, smsCode: string) =>
    http.post<{ token: string; user: User }>('/api/auth/phone', { phone, smsCode }),

  // 获取用户信息
  getUserInfo: () =>
    http.get<User>('/api/user/info'),
}

🪝 4. 自定义请求 Hook

// src/hooks/useRequest.ts
import { useState, useEffect, useCallback } from 'react'

interface UseRequestResult<T> {
  data: T | null
  loading: boolean
  error: Error | null
  refresh: () => Promise<void>
}

export function useRequest<T>(
  fetcher: () => Promise<T>,
  options: { immediate?: boolean } = { immediate: true }
): UseRequestResult<T> {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<Error | null>(null)

  const run = useCallback(async () => {
    setLoading(true)
    setError(null)
    try {
      const result = await fetcher()
      setData(result)
    } catch (e) {
      setError(e as Error)
    } finally {
      setLoading(false)
    }
  }, [])

  useEffect(() => {
    if (options.immediate) run()
  }, [])

  return { data, loading, error, refresh: run }
}

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

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

  if (loading) return <Skeleton rows={5} />

  return (
    <View>
      {data?.list.map(p => <ProductCard key={p.id} product={p} />)}
    </View>
  )
}

✅ 本篇小结 Checklist

  • 掌握 Taro.request 基础用法
  • 封装统一请求层(Token 注入、错误处理、loading)
  • 按模块组织 API(productApi / authApi)
  • 会写自定义请求 Hook

下一篇预告:《多端适配与条件编译》


本文是「Taro 从零到一」系列第 7 篇,共 10 篇。