更新记录

1.0.0(2026-09-27) 下载此版本

全版本内容


平台兼容性

uni-app x(3.8.4)

Chrome Safari Android iOS 鸿蒙 微信小程序
√ √ - - - -

gibbs-network

uni-app 全平台网络交互插件,提供Http、WebSocket、EventBus、BLE蓝牙、UDP等服务;Vue2/Vue3双兼容,支持同步异步请求、请求缓存、失败自动重试、请求队列、自动loading、token无感刷新、防抖、日志、埋点上报等

✨ 功能清单

  • ✅ HTTP:GET / POST / PUT / DELETE,文件上传下载,进度监听
  • ✅ 同步/异步请求:异步 Promise;同步接口仅缓存命中可用
  • ✅ 请求拦截、响应拦截、超时、取消请求
  • ✅ 请求队列,防止并发重复请求;自动 loading
  • ✅ Token 自动无感刷新,防无限循环,支持手动清空等待队列
  • ✅ 请求防抖 debounce 参数(实例级隔离)
  • ✅ 请求缓存 cache:内存缓存 / storage 持久缓存,可配置过期时间
  • ✅ 自动重试 retry:失败自动重试,支持自定义重试判断规则
  • ✅ 日志打印开关、请求埋点上报钩子
  • ✅ WebSocket:自动重连、断线重连、消息收发
  • ✅ EventBus 全局事件总线(组件页面通信)
  • ✅ BLE 蓝牙(小程序/App),支持连接状态双向监听
  • ✅ UDP 通信(仅 App 端)
  • ✅ Http 多实例隔离,支持多域名接口

📦 安装

将 gibbs-network 文件夹放到项目 uni_modules/ 下

🚀 快速使用

Vue2 挂载

import Vue from 'vue'
import { createHttp, EventBus } from '@/uni_modules/gibbs-network'

const $http = createHttp({
  baseURL: 'https://api.xxx.com',
  timeout: 10000,
  loading: true,
  log: true
})

Vue.prototype.$http = $http
Vue.prototype.$EventBus = EventBus

Vue3 挂载

import { createHttp, EventBus } from '@/uni_modules/gibbs-network'

const $http = createHttp({
  baseURL: 'https://api.xxx.com',
  loading: true,
  log: true
})

export function setupHttp(app) {
  app.config.globalProperties.$http = $http
  app.config.globalProperties.$EventBus = EventBus
}

直接 import(setup / composition-api)

import { createHttp, EventBus } from '@/uni_modules/gibbs-network'

const $http = createHttp({ baseURL: 'https://api.xxx.com' })

📡 HTTP 使用示例

1. 创建实例

import { createHttp } from '@/uni_modules/gibbs-network'

const $http = createHttp({
  baseURL: 'https://api.xxx.com',
  timeout: 10000,
  header: { 'Content-Type': 'application/json;charset=utf-8' },
  log: true,
  loading: true,
  loadingText: '加载中...',
  loadingMask: true
})

2. 添加请求拦截器

$http.useRequestInterceptor(async (opts) => {
  // skipAuth 标记的请求跳过鉴权(如登录、注册)
  if (opts.skipAuth) return opts
  const token = uni.getStorageSync('token')
  if (token) {
    opts.header = opts.header || {}
    opts.header.Authorization = `Bearer ${token}`
  }
  return opts
})

3. 添加响应拦截器

$http.useResponseInterceptor(async (res) => {
  if (res.statusCode === 200) {
    const data = res.data
    // 防御非 JSON 响应
    if (data && typeof data === 'object' && data.code === 0) {
      return data.data
    }
    const errMsg = (data && typeof data === 'object' && data.msg) || '请求失败'
    throw new Error(errMsg)
  }
  const msg = (res.data && typeof res.data === 'object') ? res.data.msg : ''
  throw new Error(msg || `请求错误(${res.statusCode})`)
})

4. 设置埋点上报回调

$http.onTrack((trackData) => {
  console.log('埋点数据', trackData)
  // trackData 结构:{ type, url, method, statusCode, cost, success, error }
})

5. GET 请求

const res = await $http.get('/user/info')
console.log(res)

6. GET 带参数

const res = await $http.get('/user/list', { page: 1, size: 10 })

7. POST 请求

const res = await $http.post('/user/login', {
  phone: '1**',
  code: '1234'
})

8. PUT 请求

const res = await $http.put('/user/update', { nickname: '张三' })

9. DELETE 请求

const res = await $http.delete('/user/del', { id: 1 })

10. 通用 request 方法

const res = await $http.request({
  url: '/user/info',
  method: 'GET',
  data: {},
  loading: true,
  loadingText: '加载中...'
})

11. 跳过鉴权(skipAuth)

// 登录、注册等不需要 token 的请求,使用 skipAuth 跳过请求拦截器的 token 注入
const res = await $http.post('/auth/login', {
  account: 'admin',
  password: '123456'
}, { skipAuth: true })

12. 禁用 loading

// 单个请求禁用内置 loading(如按钮已有 loading 状态时避免双重显示)
const res = await $http.post('/auth/login', {
  account: 'admin',
  password: '123456'
}, { skipAuth: true, loading: false })

13. 单请求自定义超时

// 某些接口耗时较长,单独设置更长的超时时间
const res = await $http.get('/report/export', {}, { timeout: 60000 })

14. 单请求自定义 header

// 某些接口需要特殊的请求头
const res = await $http.get('/file/download', {}, {
  header: { 'Accept': 'application/octet-stream' }
})

15. 同步请求(仅缓存命中时可用)

try {
  const data = $http.getSync('/user/info', {}, {
    cache: { enable: true, ttl: 300000 }
  })
  console.log('同步获取缓存数据', data)
} catch (e) {
  console.log(e.message) // "syncRequest:缓存未命中..."
}

16. 请求开启缓存

const res = await $http.get('/user/info', {}, {
  cache: {
    enable: true,
    storage: false,  // true 则持久化到 uni.storage
    ttl: 60000       // 缓存有效期 60秒
  }
})

17. 请求开启防抖

// 300ms 内相同 url+method+data 的请求合并,只发一次
const res = await $http.get('/user/info', {}, { debounce: 300 })

18. 请求开启自动重试

const res = await $http.get('/user/info', {}, {
  retry: {
    count: 2,    // 最多重试2次
    delay: 1000  // 每次间隔1秒
  }
})

19. 自定义重试判断

const res = await $http.get('/user/info', {}, {
  retry: {
    count: 3,
    delay: 500,
    retryJudge: (err, res) => {
      // 5xx 状态码才触发重试
      return res && res.statusCode >= 500 && res.statusCode < 600
    }
  }
})

20. 文件上传

const res = await $http.upload('/upload', filePath, { type: 'avatar' }, {
  name: 'file',
  Update: (res) => {
    console.log('上传进度', res.progress + '%')
  }
})

21. 文件下载

const res = await $http.download('https://xxx.com/file.pdf', {
  Update: (res) => {
    console.log('下载进度', res.progress + '%')
  }
})

22. 取消指定请求

// 发起请求时指定 requestId
$http.request({ url: '/slow-api', requestId: 'req-001' })

// 取消
$http.cancel('req-001')

23. 取消所有请求

$http.cancelAll()
// 同时清空防抖计时器、请求队列、token等待队列

24. 清理指定缓存

$http.removeCacheByKey('/user/info', 'GET', { id: 1 })

25. 清理指定缓存(storage 持久缓存)

$http.removeCacheByKey('/user/info', 'GET', { id: 1 }, true)

26. 按 URL 前缀批量清理缓存

$http.removeCacheByPrefix('/user/')

27. 清理全部内存缓存

$http.clearAllCache()

28. 清理全部 storage 持久缓存

$http.clearAllCache(true)

29. 配置 Token 自动刷新

const $http = createHttp({
  baseURL: 'https://api.xxx.com',
  tokenRefresh: {
    enable: true,
    refreshUrl: '/refresh-token',
    refreshMethod: 'POST',
    maxRefreshRetries: 2,
    refreshHeader: () => ({
      Authorization: `Bearer ${uni.getStorageSync('refreshToken')}`
    }),
    isRefreshSuccess: (res) => {
      return res.statusCode === 200 && res.data && res.data.token
    },
    onRefreshSuccess: (res) => {
      // 刷新成功:存储新 token
      uni.setStorageSync('token', res.data.token)
      uni.setStorageSync('refreshToken', res.data.refreshToken)
    },
    onRefreshFail: (err) => {
      // 刷新失败:清除 token,跳转登录页
      uni.removeStorageSync('token')
      uni.removeStorageSync('refreshToken')
      uni.reLaunch({ url: '/pages/login/login' })
    }
  }
})

30. 手动清空 Token 等待队列

// 用户退出登录时调用,防止残留请求继续发送
$http.clearTokenWaitQueue()

31. 多实例隔离

import { createHttp } from '@/uni_modules/gibbs-network'

// 实例A:主业务接口
const apiMain = createHttp({
  baseURL: 'https://api.main.com',
  log: true,
  tokenRefresh: {
    enable: true,
    refreshUrl: '/user/refresh-token',
    onRefreshSuccess: (res) => {
      uni.setStorageSync('token', res.data.token)
    },
    onRefreshFail: () => {
      uni.removeStorageSync('token')
      uni.reLaunch({ url: '/pages/login/login' })
    }
  }
})

// 实例B:第三方接口
const apiThird = createHttp({
  baseURL: 'https://api.third.com',
  log: false,
  cache: { enable: true, ttl: 60000 }
})

// 各自添加拦截器,互不影响
apiMain.useRequestInterceptor(async (opts) => {
  if (opts.skipAuth) return opts
  opts.header.Authorization = `Bearer ${uni.getStorageSync('token')}`
  return opts
})

apiMain.get('/user/info')
apiThird.get('/other/data')

🔌 WebSocket 使用示例

1. 创建实例

import { createSocket } from '@/uni_modules/gibbs-network'

const socket = createSocket({
  reconnectCount: 5,
  reconnectDelay: 2000
})

2. 连接

await socket.connect('wss://api.xxx.com/ws')

3. 监听连接成功

socket.onOpen(() => {
  console.log('WebSocket 连接成功')
})

4. 监听消息

socket.onMessage((res) => {
  console.log('收到消息', res.data)
})

5. 发送消息

socket.send(JSON.stringify({ type: 'ping' }))

6. 监听关闭

socket.onClose(() => {
  console.log('WebSocket 已关闭')
})

7. 监听错误

socket.onError((err) => {
  console.error('WebSocket 错误', err)
})

8. 主动关闭连接

socket.close()
// 关闭后不会自动重连

📢 EventBus 使用示例

1. 导入

import { EventBus } from '@/uni_modules/gibbs-network'

2. 监听事件

EventBus.on('userLogin', (userInfo) => {
  console.log('用户登录', userInfo)
})

3. 一次性监听

const wrapper = EventBus.once('event', (data) => {
  console.log('只触发一次', data)
})
// 回调异常时也会自动移除监听,不会泄漏

4. 触发事件

EventBus.emit('userLogin', { name: '张三', id: 1 })

5. 触发事件(多参数)

EventBus.emit('update', '张三', 18, '北京')

6. 移除指定监听

EventBus.off('userLogin', callback)

7. 移除某事件全部监听

EventBus.off('userLogin')

8. 清空所有事件

EventBus.clear()

📡 BLE 蓝牙使用示例

1. 创建实例

import { createBle } from '@/uni_modules/gibbs-network'

const ble = createBle({
  services: ['0000FFF0-0000-1000-8000-00805F9B34FB']
})

2. 打开蓝牙适配器

await ble.openAdapter()

3. 开始搜索设备

await ble.startScan()

4. 监听发现设备

ble.onFound((devices) => {
  console.log('发现设备', devices)
})

5. 连接设备

await ble.connect(deviceId)

6. 获取服务

await ble.getServices()

7. 获取特征值

await ble.getCharacteristics()

8. 开启通知

await ble.notify()

9. 监听数据

ble.onData((value) => {
  console.log('收到数据', value)
})

10. 写入数据

const buffer = new ArrayBuffer(4)
const dataView = new DataView(buffer)
dataView.setUint8(0, 0x01)
dataView.setUint8(1, 0x02)
await ble.write(buffer)

11. 读取数据

await ble.read()

12. 监听连接状态

ble.onConnect((res) => {
  console.log('蓝牙已连接', res)
})

13. 监听断开状态

ble.onDisconnect((res) => {
  console.log('蓝牙已断开', res)
})

14. 监听错误

ble.onError((err) => {
  console.error('蓝牙错误', err)
})

15. 断开连接

await ble.disconnect()

16. 关闭适配器

await ble.closeAdapter()

17. 停止搜索

await ble.stopScan()

📡 UDP 使用示例(仅 App 端)

1. 创建实例

import { createUdp } from '@/uni_modules/gibbs-network'

const udp = createUdp({
  port: 8080,
  address: '192.168.1.100'
})

2. 创建 UDP Socket

await udp.create()

3. 绑定端口

const bindPort = await udp.bind()
console.log('绑定端口', bindPort)

4. 监听消息

udp.onMessage((res) => {
  console.log('收到UDP消息', res)
})

5. 发送数据

await udp.send('hello')

6. 发送数据(指定地址和端口)

await udp.send('hello', '192.168.1.200', 9090)

7. 监听错误

udp.onError((err) => {
  console.error('UDP错误', err)
})

8. 监听关闭

udp.onClose(() => {
  console.log('UDP已关闭')
})

9. 关闭连接

udp.close()

🔧 多实例隔离说明

原理说明

createHttp() 每次调用都会 new 一个全新 HttpRequest 实例,各实例配置独立、互不干扰:

  • 独立 baseURL、header、timeout、loading、log、cache、retry、tokenRefresh 配置
  • 独立请求队列 pendingMap
  • 独立 loading 计数器
  • 独立拦截器、埋点回调(onTrack)
  • 独立 token 刷新等待队列
  • 独立防抖计时器 debounceMap

适用场景:项目需要同时请求多个不同域名的后端接口(如主业务 API + 第三方 API)。

隔离边界

注意:EventBus、BLE、UDP、WebSocket 是单例,不支持多实例隔离;只有 HttpRequest 支持多实例。

模块 是否支持多实例 说明
HttpRequest ✅ 每次调用 createHttp() 创建独立实例
WebSocket ❌ 单例,多页面共用
EventBus ❌ 单例,全局唯一
BLE ❌ 单例,多页面共用
UDP ❌ 单例,多页面共用

缓存隔离

  • 内存缓存是全局 Map,多个 http 实例共享内存缓存。
  • 如需实例隔离缓存,建议开启 cache.storage: true,或自行改造缓存 key 增加实例标识。
  • 多实例下,各自实例的 clearTokenWaitQueue() 只会清空自己实例的等待队列,不会影响其他实例。

⚠️ 注意事项

  1. 同步请求限制:getSync / syncRequest 不能用来发起网络请求。仅当该请求已经命中有效缓存时,才可以同步返回;缓存不存在会直接抛出异常。网络请求在小程序 / uni 底层 API 本身不支持真正同步阻塞。

  2. 缓存 cache:

    • 默认内存缓存,页面销毁后内存缓存清空;开启 storage: true 持久化到 uni.storage。
    • POST 请求也可以缓存,但谨慎使用,一般仅 GET 查询接口开启缓存。
    • 缓存 key 由 method + url + data 生成(data 的 key 已做稳定排序),参数不同视为不同缓存。
  3. 自动重试 retry:

    • 默认只在网络层面失败(fail 回调)触发重试;默认不会对业务错误(statusCode 200,返回 code !== 200)重试。
    • 可通过 retry.retryJudge 自定义判断逻辑,例如 500/502 状态码也触发重试。
    • 重试会保留原请求的 loading、防抖、缓存逻辑。
    • POST 提交类接口禁止开启 retry,会有重复提交风险!
  4. Token 刷新:

    • 401 触发刷新时,同一实例所有并发请求会进入等待队列,刷新成功后批量重放。
    • maxRefreshRetries 防止新 token 仍无效时无限循环刷新(默认 2 次,超限后跳转登录页)。
    • 用户退出登录时务必手动调用 http.clearTokenWaitQueue(),防止残留队列继续请求。
    • 如果 refresh-token 接口本身 401,会触发登录跳转;要保证刷新接口白名单,不要带上过期 token。
  5. 防抖 debounce:

    • 防抖基于 requestKey,相同 url + method + data 才会合并防抖;不同参数不会合并。
    • 防抖计时器为实例级隔离,多实例之间互不影响。
  6. upload / download:

    • 已支持请求拦截器(如自动注入 token header),与 request 行为一致。
    • 响应拦截器不生效,如需处理响应数据请在 success 回调中自行处理。
  7. Vue2 / Vue3:插件原生 JS,不依赖 Vue,Vue2 挂载原型,Vue3 挂载 globalProperties,也可以直接 import 在 setup 中使用。

  8. 平台限制:

    • UDP:仅 App 端支持,H5、小程序直接抛错。
    • BLE 蓝牙:App + 微信小程序支持;H5 不支持。
    • WebSocket:全平台支持,但小程序需要后台配置业务域名。

⚠️ 已知坑点

  1. uni 底层限制:uni.request 本身不支持真正同步阻塞网络请求,本插件同步接口只能读取缓存,无法阻塞等待网络返回。
  2. 内存缓存是全局 Map,多个 http 实例共享内存缓存;如需实例隔离缓存,建议开启 storage: true 持久化缓存。
  3. 小程序环境:并发请求数量有限制(微信最多 6 个并发),超过会排队,这是原生限制,非插件问题。
  4. 重试逻辑:POST 提交类接口(新增/保存)开启重试,会有重复提交风险! 提交类接口禁止开启 retry。
  5. loading 是全局 uni loading,多个实例同时开启 loading 时,靠内部计数器管理;同时弹出多个 loading 会出现闪烁,属于 uni 原生 loading 限制。
  6. WebSocket、EventBus、BLE、UDP 为单例,多页面共用,不支持多实例隔离。
  7. 缓存 storage 持久化:storage 容量有限,大量大体积接口缓存会占用本地存储空间。
  8. token 刷新场景:如果 refresh-token 接口本身 401,会触发登录跳转;要保证刷新接口白名单,不要带上过期 token。

💡 提示

  1. EventBus、BLE、UDP、WebSocket 是单例,不是多实例隔离;只有 HttpRequest(http 请求)支持多实例。
  2. 多实例下,各自实例的 clearTokenWaitQueue() 只会清空自己实例的等待队列,不会影响其他实例。
  3. 缓存:内存缓存是全局共享;如果需要实例隔离缓存,建议开启 cache.storage: true,或者自行改造缓存 key 增加实例标识。
  4. 埋点回调 onTrack 是实例独立,每个实例可以单独配置埋点回调。

📋 API 速览

HttpRequest

方法 说明
request(options) 通用请求
get(url, data?, options?) GET 请求
post(url, data?, options?) POST 请求
put(url, data?, options?) PUT 请求
delete(url, data?, options?) DELETE 请求
getSync(url, data?, options?) 同步获取缓存
upload(url, filePath, formData?, options?) 文件上传
download(url, options?) 文件下载
useRequestInterceptor(fn) 添加请求拦截器
useResponseInterceptor(fn) 添加响应拦截器
onTrack(fn) 埋点上报回调
cancel(requestId) 取消指定请求
cancelAll() 取消所有请求
removeCacheByKey(url, method?, data?, useStorage?) 清理指定缓存
removeCacheByPrefix(urlPrefix) 清理前缀匹配缓存
clearAllCache(useStorage?) 清理全部缓存
clearTokenWaitQueue() 手动清空 token 等待队列

UniSocket

方法 说明
connect(url) 连接 WebSocket
send(data) 发送消息
close() 关闭连接
onMessage(fn) 监听消息
onOpen(fn) 监听连接成功
onClose(fn) 监听关闭
onError(fn) 监听错误

UniBle

方法 说明
openAdapter() 打开蓝牙适配器
closeAdapter() 关闭蓝牙适配器
startScan(options?) 开始搜索设备
stopScan() 停止搜索
onFound(fn) 监听发现设备
connect(deviceId) 连接设备
disconnect() 断开连接
getServices() 获取服务
getCharacteristics() 获取特征值
notify() 开启通知
onData(fn) 监听数据
write(buffer) 写入数据
read() 读取数据
onConnect(fn) 监听连接(含自动重连)
onDisconnect(fn) 监听断开
onError(fn) 监听错误

UniUdp

方法 说明
create() 创建 UDP Socket
bind(port?) 绑定端口
send(data, address?, port?) 发送数据
close() 关闭连接
onMessage(fn) 监听消息
onError(fn) 监听错误
onClose(fn) 监听关闭

EventBus

方法 说明
on(name, cb) 监听事件
once(name, cb) 一次性监听(异常安全)
emit(name, ...args) 触发事件
off(name, cb?) 移除监听
clear() 清空所有事件

📋 完整配置项

HttpConfig

参数 类型 默认值 说明
baseURL string '' 接口基础地址
timeout number 10000 请求超时时间(ms)
header object {Content-Type} 默认请求头
log boolean false 是否打印日志
loading boolean false 是否显示 loading
loadingText string '加载中...' loading 文字
loadingMask boolean true loading 遮罩
debounce number - 防抖时间(ms)
requestId string - 请求标识(用于取消)
tokenRefresh.enable boolean false 是否启用 token 刷新
tokenRefresh.refreshUrl string '/refresh-token' 刷新接口地址
tokenRefresh.refreshMethod string 'POST' 刷新请求方法
tokenRefresh.refreshHeader object/function/null null 自定义刷新请求头
tokenRefresh.isRefreshSuccess function/null null 自定义刷新成功判断
tokenRefresh.onRefreshSuccess function/null null 刷新成功回调(存储新 token 等)
tokenRefresh.onRefreshFail function/null null 刷新失败回调(清除 token、跳转登录页等)
tokenRefresh.maxRefreshRetries number 2 最大刷新重试次数
requestQueue.enable boolean true 是否启用请求队列
cache.enable boolean false 是否启用缓存
cache.storage boolean false 是否持久化到 storage
cache.ttl number 300000 缓存有效期(ms)
retry.count number 0 重试次数
retry.delay number 1000 重试间隔(ms)
retry.retryJudge function/null null 自定义重试判断

🌟 完整 HTTP 实例:登录获取 Token → 请求携带 Token → 退出登录

以下示例覆盖了真实业务中最核心的 HTTP 鉴权流程:用户在登录页提交账号密码获取 token → 后续所有请求自动携带 token → token 过期自动无感刷新 → 退出登录清除 token 并跳转。同时给出 Vue2 和 Vue3 两种写法。

第一步:创建 HTTP 实例并配置拦截器(公共逻辑,Vue2/Vue3 通用)

建议在项目根目录新建 utils/http.js(或 http/index.js),统一管理 http 实例:

// utils/http.js
import { createHttp } from '@/uni_modules/gibbs-network'

let isLoggingOut = false
export const setLoggingOut = (val) => { isLoggingOut = val }
export const getIsLoggingOut = () => isLoggingOut

const $http = createHttp({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  loading: true,
  loadingText: '加载中...',
  loadingMask: true,
  log: true,
  tokenRefresh: {
    enable: true,
    refreshUrl: '/auth/refresh-token',
    refreshMethod: 'POST',
    maxRefreshRetries: 2,
    refreshHeader: () => ({
      Authorization: `Bearer ${uni.getStorageSync('refreshToken')}`
    }),
    isRefreshSuccess: (res) => {
      return res.statusCode === 200 && res.data && res.data.token
    },
    onRefreshSuccess: (res) => {
      uni.setStorageSync('token', res.data.token)
      uni.setStorageSync('refreshToken', res.data.refreshToken)
    },
    onRefreshFail: (err) => {
      uni.removeStorageSync('token')
      uni.removeStorageSync('refreshToken')
      uni.reLaunch({ url: '/pages/login/login' })
    }
  }
})

// 请求拦截器:自动携带 token(skipAuth 标记的请求跳过鉴权)
$http.useRequestInterceptor(async (opts) => {
  if (opts.skipAuth) return opts
  const token = uni.getStorageSync('token')
  if (token) {
    opts.header = opts.header || {}
    opts.header.Authorization = `Bearer ${token}`
  }
  return opts
})

// 响应拦截器:统一处理业务错误(防御非JSON响应)
$http.useResponseInterceptor(async (res) => {
  if (res.statusCode === 200) {
    const data = res.data
    // 假设后端返回格式为 { code: 0, data: {}, msg: '' }
    if (data && typeof data === 'object' && data.code === 0) {
      return data.data
    }
    // 业务错误(退出中不弹提示,避免集中弹窗)
    const errMsg = (data && typeof data === 'object' && data.msg) || '请求失败'
    if (!getIsLoggingOut()) {
      uni.showToast({ title: errMsg, icon: 'none' })
    }
    throw new Error(errMsg)
  }
  // 其他状态码由 tokenRefresh 机制处理(401)或抛出错误
  const msg = (res.data && typeof res.data === 'object') ? res.data.msg : ''
  throw new Error(msg || `请求错误(${res.statusCode})`)
})

export default $http

第二步:登录页 — 提交账号密码获取 Token

Vue3 写法(<script setup>)

<!-- pages/login/login.vue -->
<template>
  <view class="login-page">
    <view class="logo">
      <image src="/static/logo.png" mode="aspectFit" />
    </view>
    <view class="form">
      <input v-model="form.account" placeholder="请输入账号" />
      <input v-model="form.password" type="password" placeholder="请输入密码" />
      <button :loading="loading" @click="handleLogin">登 录</button>
    </view>
  </view>
</template>

<script setup>
import { ref, reactive } from 'vue'
import $http from '@/utils/http'
import { EventBus } from '@/uni_modules/gibbs-network'

const form = reactive({
  account: '',
  password: ''
})
const loading = ref(false)

const handleLogin = async () => {
  if (!form.account || !form.password) {
    return uni.showToast({ title: '请输入账号和密码', icon: 'none' })
  }
  // 登录前清除旧 token,防止旧 token 被注入登录请求
  uni.removeStorageSync('token')
  uni.removeStorageSync('refreshToken')
  try {
    loading.value = true
    // 调用登录接口,获取 token
    // skipAuth: 跳过鉴权拦截器;loading: false: 避免与按钮 loading 双重显示
    const res = await $http.post('/auth/login', {
      account: form.account,
      password: form.password
    }, { skipAuth: true, loading: false })
    // 存储 token 和 refreshToken
    uni.setStorageSync('token', res.token)
    uni.setStorageSync('refreshToken', res.refreshToken)
    // 存储用户信息(可选)
    uni.setStorageSync('userInfo', res.userInfo)
    // 通知全局用户已登录
    EventBus.emit('userLogin', res.userInfo)
    // 跳转到首页
    uni.reLaunch({ url: '/pages/index/index' })
  } catch (e) {
    uni.showToast({ title: e.message || '登录失败', icon: 'none' })
  } finally {
    loading.value = false
  }
}
</script>

Vue2 写法(Options API)

<!-- pages/login/login.vue -->
<template>
  <view class="login-page">
    <view class="logo">
      <image src="/static/logo.png" mode="aspectFit" />
    </view>
    <view class="form">
      <input v-model="account" placeholder="请输入账号" />
      <input v-model="password" type="password" placeholder="请输入密码" />
      <button :loading="loading" @click="handleLogin">登 录</button>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      account: '',
      password: '',
      loading: false
    }
  },
  methods: {
    async handleLogin() {
      if (!this.account || !this.password) {
        return uni.showToast({ title: '请输入账号和密码', icon: 'none' })
      }
      // 登录前清除旧 token,防止旧 token 被注入登录请求
      uni.removeStorageSync('token')
      uni.removeStorageSync('refreshToken')
      try {
        this.loading = true
        // 调用登录接口,获取 token
        // skipAuth: 跳过鉴权拦截器;loading: false: 避免与按钮 loading 双重显示
        const res = await this.$http.post('/auth/login', {
          account: this.account,
          password: this.password
        }, { skipAuth: true, loading: false })
        // 存储 token 和 refreshToken
        uni.setStorageSync('token', res.token)
        uni.setStorageSync('refreshToken', res.refreshToken)
        // 存储用户信息(可选)
        uni.setStorageSync('userInfo', res.userInfo)
        // 通知全局用户已登录
        this.$EventBus.emit('userLogin', res.userInfo)
        // 跳转到首页
        uni.reLaunch({ url: '/pages/index/index' })
      } catch (e) {
        uni.showToast({ title: e.message || '登录失败', icon: 'none' })
      } finally {
        this.loading = false
      }
    }
  }
}
</script>

第三步:任意页面请求自动携带 Token

请求拦截器已统一处理 token 注入,业务页面无需手动传 token:

Vue3 写法

<!-- pages/user/profile.vue -->
<template>
  <view class="profile">
    <text>昵称:{{ userInfo.nickname }}</text>
    <text>手机:{{ userInfo.phone }}</text>
    <button @click="loadUserInfo">刷新信息</button>
    <button @click="handleLogout">退出登录</button>
  </view>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import $http, { getIsLoggingOut } from '@/utils/http'

const userInfo = ref({})

const loadUserInfo = async () => {
  try {
    // 请求会自动携带 token(拦截器注入)
    userInfo.value = await $http.get('/user/info')
  } catch (e) {
    if (!getIsLoggingOut()) {
      uni.showToast({ title: e.message || '获取失败', icon: 'none' })
    }
  }
}

onMounted(() => {
  loadUserInfo()
})
</script>

Vue2 写法

<!-- pages/user/profile.vue -->
<template>
  <view class="profile">
    <text>昵称:{{ userInfo.nickname }}</text>
    <text>手机:{{ userInfo.phone }}</text>
    <button @click="loadUserInfo">刷新信息</button>
    <button @click="handleLogout">退出登录</button>
  </view>
</template>

<script>
import { getIsLoggingOut } from '@/utils/http'

export default {
  data() {
    return {
      userInfo: {}
    }
  },
  mounted() {
    this.loadUserInfo()
  },
  methods: {
    async loadUserInfo() {
      try {
        // 请求会自动携带 token(拦截器注入)
        this.userInfo = await this.$http.get('/user/info')
      } catch (e) {
        // 退出中不弹提示,避免集中弹窗
        if (!getIsLoggingOut()) {
          uni.showToast({ title: e.message || '获取失败', icon: 'none' })
        }
      }
    }
  }
}
</script>

第四步:退出登录 — 清除 Token 并跳转

退出时需要:1) 标记退出状态(防止残留请求弹错误 toast);2) 先取消所有业务请求和清空等待队列;3) 再发退出请求;4) 清除本地存储和缓存;5) 跳转登录页。

⚠️ 顺序很重要:必须先 cancelAll 再发 logout 请求,否则 logout 请求可能被 cancelAll 中止。cancelAll 内部已自动调用 clearTokenWaitQueue。

Vue3 写法

<!-- pages/user/profile.vue(接上文的退出按钮) -->
<script setup>
import $http, { setLoggingOut } from '@/utils/http'
import { EventBus } from '@/uni_modules/gibbs-network'

const handleLogout = async () => {
  // 1. 标记退出中,防止残留请求 reject 时弹出错误 toast
  setLoggingOut(true)
  // 2. 先取消所有业务请求(内部已自动清空 token 等待队列)
  $http.cancelAll()
  // 3. 再发退出请求(此时只有这一个请求在跑)
  try {
    await $http.post('/auth/logout', {}, { skipAuth: false, loading: false })
  } catch (e) {
    // token 可能已过期,退出接口失败也继续清理本地状态
  }
  // 4. 清除本地存储
  uni.removeStorageSync('token')
  uni.removeStorageSync('refreshToken')
  uni.removeStorageSync('userInfo')
  // 5. 清理 http 缓存,防止切换账号后看到上一个用户的缓存数据
  $http.clearAllCache()
  $http.clearAllCache(true)
  // 6. 通知全局用户已退出
  EventBus.emit('userLogout')
  // 7. 跳转登录页
  uni.reLaunch({ url: '/pages/login/login' })
  // 8. 重置退出标记
  setLoggingOut(false)
}
</script>

Vue2 写法

<!-- pages/user/profile.vue(接上文的退出按钮) -->
<script>
import { setLoggingOut } from '@/utils/http'

export default {
  methods: {
    async handleLogout() {
      // 1. 标记退出中,防止残留请求 reject 时弹出错误 toast
      setLoggingOut(true)
      // 2. 先取消所有业务请求(内部已自动清空 token 等待队列)
      this.$http.cancelAll()
      // 3. 再发退出请求(此时只有这一个请求在跑)
      try {
        await this.$http.post('/auth/logout', {}, { skipAuth: false, loading: false })
      } catch (e) {
        // token 可能已过期,退出接口失败也继续清理本地状态
      }
      // 4. 清除本地存储
      uni.removeStorageSync('token')
      uni.removeStorageSync('refreshToken')
      uni.removeStorageSync('userInfo')
      // 5. 清理 http 缓存,防止切换账号后看到上一个用户的缓存数据
      this.$http.clearAllCache()
      this.$http.clearAllCache(true)
      // 6. 通知全局用户已退出
      this.$EventBus.emit('userLogout')
      // 7. 跳转登录页
      uni.reLaunch({ url: '/pages/login/login' })
      // 8. 重置退出标记
      setLoggingOut(false)
    }
  }
}
</script>

第五步:Vue2 入口挂载 vs Vue3 入口挂载

Vue2 — main.js

import Vue from 'vue'
import App from './App'
import $http, { setLoggingOut } from '@/utils/http'
import { EventBus } from '@/uni_modules/gibbs-network'

Vue.prototype.$http = $http
Vue.prototype.$EventBus = EventBus

const app = new Vue({ ...App })
app.$mount()

Vue3 — main.js

import { createSSRApp } from 'vue'
import App from './App.vue'
import $http, { setLoggingOut } from '@/utils/http'
import { EventBus } from '@/uni_modules/gibbs-network'

export function createApp() {
  const app = createSSRApp(App)
  app.config.globalProperties.$http = $http
  app.config.globalProperties.$EventBus = EventBus
  return { app }
}

第六步:全局监听登录/退出状态 + 启动时自动恢复登录态

在 App.vue 中监听 EventBus,并在 onLaunch 时检查 token 决定是否跳转登录页:

Vue3 写法

<!-- App.vue -->
<script setup>
import { onLaunch } from '@dcloudio/uni-app'
import { EventBus } from '@/uni_modules/gibbs-network'

onLaunch(() => {
  const token = uni.getStorageSync('token')
  if (!token) {
    uni.reLaunch({ url: '/pages/login/login' })
  }

  EventBus.on('userLogin', (userInfo) => {
    console.log('用户已登录', userInfo)
  })
  EventBus.on('userLogout', () => {
    console.log('用户已退出')
  })
})
</script>

Vue2 写法

<!-- App.vue -->
<script>
export default {
  onLaunch() {
    // 启动时检查 token,无 token 则跳转登录页
    // 有 token 则正常进入首页;如果 token 过期,后续请求的 401 会自动触发 tokenRefresh
    const token = uni.getStorageSync('token')
    if (!token) {
      uni.reLaunch({ url: '/pages/login/login' })
    }

    this.$EventBus.on('userLogin', (userInfo) => {
      console.log('用户已登录', userInfo)
    })
    this.$EventBus.on('userLogout', () => {
      console.log('用户已退出')
    })
  }
}
</script>

🔑 流程总结

┌──────────────────────────────────────────────────────────┐
│  登录页                                                   │
│  1. 用户输入账号 + 密码                                    │
│  2. 清除旧 token(防御性,防止旧 token 被注入)              │
│  3. POST /auth/login { skipAuth, loading:false }          │
│     → 获取 token + refreshToken                           │
│  4. uni.setStorageSync('token', token)                    │
│  5. uni.setStorageSync('refreshToken', refreshToken)      │
│  6. EventBus.emit('userLogin') → 跳转首页                  │
└──────────────────────────────────────────────────────────┘
                          ↓
┌──────────────────────────────────────────────────────────┐
│  App 启动(onLaunch)                                      │
│  检查 storage 中是否有 token                               │
│  → 有 token:正常进入首页(过期则 401 自动刷新)            │
│  → 无 token:跳转登录页                                    │
└──────────────────────────────────────────────────────────┘
                          ↓
┌──────────────────────────────────────────────────────────┐
│  任意业务页面                                               │
│  请求拦截器自动注入: header.Authorization = Bearer ${token}  │
│  (skipAuth 标记的请求跳过鉴权,如登录/注册)                │
│  → GET/POST/PUT/DELETE 正常请求                            │
│                                                            │
│  若返回 401:                                               │
│    → tokenRefresh 自动触发 POST /auth/refresh-token        │
│    → 用 refreshToken 换取新 token                          │
│    → 刷新成功:onRefreshSuccess 存储新 token                │
│    → 等待队列中的请求自动重放                                │
│    → 刷新失败/超限:onRefreshFail 清除token→跳转登录页       │
└──────────────────────────────────────────────────────────┘
                          ↓
┌──────────────────────────────────────────────────────────┐
│  退出登录                                                   │
│  1. setLoggingOut(true)   ← 标记退出,抑制错误 toast       │
│  2. $http.cancelAll()     ← 取消所有请求+清空等待队列       │
│  3. POST /auth/logout { loading:false } ← 再发退出请求     │
│  4. uni.removeStorageSync('token')                        │
│  5. uni.removeStorageSync('refreshToken')                 │
│  6. $http.clearAllCache() + clearAllCache(true) ← 清缓存  │
│  7. EventBus.emit('userLogout')                           │
│  8. uni.reLaunch({ url: '/pages/login/login' })           │
│  9. setLoggingOut(false) ← 重置标记                      │
└──────────────────────────────────────────────────────────┘

隐私、权限声明

1. 本插件需要申请的系统权限列表:

无

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

无

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无

许可协议

MIT协议