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)
  }
}
相关推荐
cz追天之路5 小时前
华为机考 ------ 识别有效的IP地址和掩码并进行分类统计
javascript·华为·typescript·node.js·ecmascript·less·css3
小恒恒6 小时前
2025 Vibe Coding 有感
前端·uni-app·trae
一颗小青松7 小时前
uniapp使用uni-im
uni-app
2501_9160074710 小时前
iPhone APP 性能测试怎么做,除了Instruments还有什么工具?
android·ios·小程序·https·uni-app·iphone·webview
2501_9151063210 小时前
Windows 环境下有哪些可用的 iOS 上架工具, iOS 上架工具的使用方式
android·ios·小程序·https·uni-app·iphone·webview
星光不问赶路人11 小时前
TypeScript 架构实践:从后端接口到 UI 渲染数据流的完整方案
前端·vue.js·typescript
码界奇点11 小时前
基于React与TypeScript的后台管理系统设计与实现
前端·c++·react.js·typescript·毕业设计·源代码管理
CodeCaptain11 小时前
一个快速校验地图资源是否符合兼容要求的小脚本(Cocos Creator3.8.0)
游戏·typescript·cocos2d
一颗小青松13 小时前
uniapp vue3中app端使用腾讯云点播上传
uni-app·云计算·腾讯云
树叶会结冰13 小时前
TypeScript---循环:要学会原地踏步,更要学会跳出舒适圈
前端·javascript·typescript