uni-app + ts请求封装最佳实践(GET/POST + 加载态 + 错误兜底)

确保项目目录结构如下(和封装代码对应):

bash 复制代码
utils/
└── request/
    ├── index.ts  // 上面的封装代码
    └── config.ts // 基础地址配置

config.ts 参考代码

javascript 复制代码
// 声明process类型,解决TS警告(不用安装任何依赖)
declare const process: {
  env: {
    NODE_ENV: 'development' | 'production'
  }
}

// 区分开发/生产环境(小程序可通过process.env判断,HBuilderX会自动注入)
const isProd = process.env.NODE_ENV === 'production'

// 基础接口地址
export const BASE_URL = isProd 
  ? 'https://你的生产环境域名' 
  : 'https://你的测试环境域名'

// 导出默认值(兼容你原来的import写法)
export default BASE_URL

request.ts

javascript 复制代码
// utils/request/index.ts
import BASE_URL from './config'

// 仅保留核心的GET/POST方法
type HttpMethod = 'GET' | 'POST'

// 对齐uni-app规范的配置项:loading默认值改为true
interface RequestOptions {
  header?: Record<string, string>
  timeout?: number
  baseURL?: string
  loading?: boolean // 默认true,可手动传false关闭
  loadingText?: string
  showErrorToast?: boolean
}

// 统一的响应数据格式
interface ResponseData<T = any> {
  code: number
  message: string
  data: T
  success: boolean
}

// 声明uni全局对象,避免TS报错
declare const uni: any

/**
 * 构建完整URL(处理baseURL和路径拼接)
 */
const buildURL = (base: string, url: string): string => {
  if (/^https?:\/\//i.test(url)) return url
  const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base
  const normalizedUrl = url.startsWith('/') ? url : `/${url}`
  return `${normalizedBase}${normalizedUrl}`
}

/**
 * 序列化GET请求参数
 */
const serializeQuery = (params: Record<string, any> = {}): string => {
  const parts: string[] = []
  Object.keys(params).forEach((key) => {
    const val = params[key]
    if (val === undefined || val === null) return

    if (Array.isArray(val)) {
      val.forEach((v) => parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`))
    } else {
      parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(val))}`)
    }
  })
  return parts.join('&')
}

/**
 * 显示加载提示
 */
const showLoading = (text: string = '加载中...'): void => {
  uni.showLoading({
    title: text,
    mask: true
  })
}

/**
 * 🌟 细化HTTP错误信息
 * @param statusCode HTTP状态码
 * @returns 友好的错误提示文本
 */
const getHttpErrorMessage = (statusCode: number): string => {
  const errorMap: Record<number, string> = {
    400: '请求参数错误,请检查输入',
    401: '登录已过期,请重新登录',
    403: '暂无权限访问该资源',
    404: '请求的接口不存在',
    408: '请求超时,请稍后重试',
    500: '服务器内部错误,请稍后重试',
    502: '网关错误,服务暂不可用',
    503: '服务维护中,请稍后重试',
    504: '网关超时,请稍后重试'
  }
  return errorMap[statusCode] || `请求失败 (${statusCode})`
}

/**
 * 处理401未授权
 */
const handleUnauthorized = (): void => {
  uni.removeStorageSync('token')
  uni.showToast({
    title: '登录已过期,请重新登录',
    icon: 'none',
    duration: 2000
  })
  setTimeout(() => {
    uni.reLaunch({
      url: '/pages/login/index'
    })
  }, 1500)
}

/**
 * 核心请求方法
 */
const request = <T = any>(
  url: string,
  method: HttpMethod,
  data?: any,
  options: RequestOptions = {}
): Promise<ResponseData<T>> => {
  const finalBaseURL = options.baseURL ?? BASE_URL

  // 🌟 关键修改:loading默认值设为true(未传时默认显示加载态)
  const isLoading = options.loading ?? true
  if (isLoading) {
    showLoading(options.loadingText)
  }

  let fullURL = buildURL(finalBaseURL, url)
  if (method === 'GET' && data && Object.keys(data).length > 0) {
    fullURL += `?${serializeQuery(data)}`
  }

  const token = uni.getStorageSync ? uni.getStorageSync('token') : ''
  const finalHeader: Record<string, string> = {
    'Content-Type': 'application/json',
    ...(token ? { Authorization: `Bearer ${token}` } : {}),
    ...(options.header || {})
  }

  return new Promise<ResponseData<T>>((resolve, reject) => {
    uni.request({
      url: fullURL,
      method,
      data: method === 'GET' ? undefined : data,
      header: finalHeader,
      timeout: options.timeout ?? 15000,
      success: (res: any) => {
        // 🌟 同步修改:用isLoading判断是否隐藏
        if (isLoading) {
          uni.hideLoading()
        }

        if (res.statusCode >= 200 && res.statusCode < 300) {
          const responseData = res.data as any

          if (responseData.code === 401) {
            handleUnauthorized()
            reject({
              code: 401,
              message: '登录已过期',
              data: null,
              success: false
            })
            return
          }

          if (responseData.code === 0 || responseData.success) {
            resolve({
              code: responseData.code || 0,
              message: responseData.message || '操作成功',
              data: responseData.data || responseData,
              success: true
            })
          } else {
            const errorMsg = responseData.message || '请求失败'
            if (options.showErrorToast !== false) {
              uni.showToast({
                title: errorMsg,
                icon: 'none'
              })
            }
            reject({
              code: responseData.code || -1,
              message: errorMsg,
              data: responseData.data,
              success: false
            })
          }
        } else {
          // 🌟 使用细化的HTTP错误信息
          const errorMsg = getHttpErrorMessage(res.statusCode)
          if (options.showErrorToast !== false) {
            uni.showToast({
              title: errorMsg,
              icon: 'none'
            })
          }
          reject({
            code: res.statusCode,
            message: errorMsg,
            data: res.data,
            success: false
          })
        }
      },
      fail: (err: any) => {
        // 🌟 同步修改:用isLoading判断是否隐藏
        if (isLoading) {
          uni.hideLoading()
        }
        uni.showToast({
          title: err.errMsg || '网络连接失败,请检查网络',
          icon: 'none'
        })
        reject({
          code: -2,
          message: err.errMsg || '网络错误',
          data: err,
          success: false
        })
      },
      complete: () => {
        // 🌟 同步修改:用isLoading判断是否隐藏(兜底)
        if (isLoading) {
          uni.hideLoading()
        }
      }
    })
  })
}

// 封装GET/POST快捷方法
export const get = <T = any>(url: string, params?: Record<string, any>, options?: RequestOptions) => {
  return request<T>(url, 'GET', params, options)
}

export const post = <T = any>(url: string, body?: any, options?: RequestOptions) => {
  return request<T>(url, 'POST', body, options)
}

// 导出默认对象
export default {
  get,
  post
}

基础使用

typescript 复制代码
// 在页面/组件中导入
import { get, post } from '@/utils/request'

// 示例1:GET请求获取用户列表(默认显示"加载中...")
const fetchUserList = async () => {
  try {
    // 泛型<T>指定返回数据类型,TS会自动提示字段
    const res = await get<{ list: Array<{ id: string; name: string }>; total: number }>(
      '/api/user/list', // 接口路径
      { page: 1, size: 10 } // GET参数
    )
    
    // 请求成功(res.success === true)
    if (res.success) {
      console.log('用户列表:', res.data.list)
      console.log('总条数:', res.data.total)
    }
  } catch (err) {
    // 请求失败(HTTP错误/业务错误/网络错误)
    console.error('获取用户列表失败:', err)
  }
}

// 示例2:POST请求实现登录(默认显示"加载中...")
const login = async (username: string, password: string) => {
  try {
    const res = await post<{ token: string; userInfo: { id: string; name: string } }>(
      '/api/login', // 接口路径
      { username, password } // POST请求体
    )
    
    if (res.success) {
      // 登录成功,保存token到本地
      uni.setStorageSync('token', res.data.token)
      // 提示用户
      uni.showToast({ title: '登录成功', icon: 'success' })
      // 跳转到首页
      uni.reLaunch({ url: '/pages/index/index' })
    }
  } catch (err) {
    console.error('登录失败:', err)
  }
}
相关推荐
ttod_qzstudio13 小时前
深入理解 TypeScript 数组的 find 与 filter 方法:精准查找的艺术
javascript·typescript·filter·find
2501_9160074717 小时前
苹果手机iOS应用管理全指南与隐藏功能详解
android·ios·智能手机·小程序·uni-app·iphone·webview
锦瑟弦音19 小时前
Luban + Cocos3.8.7 + Typescript + Json
笔记·游戏·typescript
2501_9151063220 小时前
全面理解 iOS 帧率,构建从渲染到系统行为的多工具协同流畅度分析体系
android·ios·小程序·https·uni-app·iphone·webview
2501_916008891 天前
iOS 能耗检测的工程化方法,构建多工具协同的电量分析与性能能效体系
android·ios·小程序·https·uni-app·iphone·webview
济南壹软网络科技有限公司1 天前
综合社交服务平台的技术架构与实践:构建高可用、多端覆盖的互动生态
uni-app·php·开源源码·陪玩陪聊h5
2501_915921431 天前
重新理解 iOS 的 Bundle Id 从创建、管理到协作的工程策略
android·ios·小程序·https·uni-app·iphone·webview
2501_915106321 天前
当 altool 退出历史舞台,iOS 上传链路的演变与替代方案的工程实践
android·ios·小程序·https·uni-app·iphone·webview
00后程序员张1 天前
Transporter 的局限与替代路径,iOS 上传流程在多平台团队中的演进
android·ios·小程序·https·uni-app·iphone·webview