更新记录
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()只会清空自己实例的等待队列,不会影响其他实例。
⚠️ 注意事项
-
同步请求限制:
getSync / syncRequest不能用来发起网络请求。仅当该请求已经命中有效缓存时,才可以同步返回;缓存不存在会直接抛出异常。网络请求在小程序 / uni 底层 API 本身不支持真正同步阻塞。 -
缓存 cache:
- 默认内存缓存,页面销毁后内存缓存清空;开启
storage: true持久化到 uni.storage。 - POST 请求也可以缓存,但谨慎使用,一般仅 GET 查询接口开启缓存。
- 缓存 key 由
method + url + data生成(data 的 key 已做稳定排序),参数不同视为不同缓存。
- 默认内存缓存,页面销毁后内存缓存清空;开启
-
自动重试 retry:
- 默认只在网络层面失败(fail 回调)触发重试;默认不会对业务错误(statusCode 200,返回 code !== 200)重试。
- 可通过
retry.retryJudge自定义判断逻辑,例如 500/502 状态码也触发重试。 - 重试会保留原请求的 loading、防抖、缓存逻辑。
- POST 提交类接口禁止开启 retry,会有重复提交风险!
-
Token 刷新:
- 401 触发刷新时,同一实例所有并发请求会进入等待队列,刷新成功后批量重放。
maxRefreshRetries防止新 token 仍无效时无限循环刷新(默认 2 次,超限后跳转登录页)。- 用户退出登录时务必手动调用
http.clearTokenWaitQueue(),防止残留队列继续请求。 - 如果 refresh-token 接口本身 401,会触发登录跳转;要保证刷新接口白名单,不要带上过期 token。
-
防抖 debounce:
- 防抖基于 requestKey,相同 url + method + data 才会合并防抖;不同参数不会合并。
- 防抖计时器为实例级隔离,多实例之间互不影响。
-
upload / download:
- 已支持请求拦截器(如自动注入 token header),与 request 行为一致。
- 响应拦截器不生效,如需处理响应数据请在 success 回调中自行处理。
-
Vue2 / Vue3:插件原生 JS,不依赖 Vue,Vue2 挂载原型,Vue3 挂载 globalProperties,也可以直接 import 在 setup 中使用。
-
平台限制:
- UDP:仅 App 端支持,H5、小程序直接抛错。
- BLE 蓝牙:App + 微信小程序支持;H5 不支持。
- WebSocket:全平台支持,但小程序需要后台配置业务域名。
⚠️ 已知坑点
- uni 底层限制:
uni.request本身不支持真正同步阻塞网络请求,本插件同步接口只能读取缓存,无法阻塞等待网络返回。 - 内存缓存是全局 Map,多个 http 实例共享内存缓存;如需实例隔离缓存,建议开启
storage: true持久化缓存。 - 小程序环境:并发请求数量有限制(微信最多 6 个并发),超过会排队,这是原生限制,非插件问题。
- 重试逻辑:POST 提交类接口(新增/保存)开启重试,会有重复提交风险! 提交类接口禁止开启 retry。
- loading 是全局 uni loading,多个实例同时开启 loading 时,靠内部计数器管理;同时弹出多个 loading 会出现闪烁,属于 uni 原生 loading 限制。
- WebSocket、EventBus、BLE、UDP 为单例,多页面共用,不支持多实例隔离。
- 缓存 storage 持久化:storage 容量有限,大量大体积接口缓存会占用本地存储空间。
- token 刷新场景:如果 refresh-token 接口本身 401,会触发登录跳转;要保证刷新接口白名单,不要带上过期 token。
💡 提示
- EventBus、BLE、UDP、WebSocket 是单例,不是多实例隔离;只有 HttpRequest(http 请求)支持多实例。
- 多实例下,各自实例的
clearTokenWaitQueue()只会清空自己实例的等待队列,不会影响其他实例。 - 缓存:内存缓存是全局共享;如果需要实例隔离缓存,建议开启
cache.storage: true,或者自行改造缓存 key 增加实例标识。 - 埋点回调
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) ← 重置标记 │
└──────────────────────────────────────────────────────────┘

收藏人数:
下载插件并导入HBuilderX
下载插件ZIP
赞赏(0)
下载 5
赞赏 0
下载 12641763
赞赏 1950
赞赏
京公网安备:11010802035340号