更新记录
1.0.0(2026-08-11)
- 1.0.0:首版,包含 HTTP / WebSocket / MQTT / SSE / TCP / UDP 六协议与拦截器、重连、Token 刷新、流式输出等能力。
- 更新时修改
uni_modules/net-client/package.json的version,并在changelog.md追加记录,重新执行node scripts/sync-uni-module.js同步后再次发布。
平台兼容性
uni-app(3.8.1)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | √ | √ | √ | √ | √ | √ | √ |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| √ | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ |
uni-app x(3.8.1)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| - | - | - | - | - | - |
net-client
uni-app 跨端通用接口连接工具库 —— HTTP / WebSocket / MQTT / SSE / TCP / UDP 六种协议,一套 API 全部搞定。 100% TypeScript 编写,面向对象架构,开箱即用的拦截器、心跳重连、Token 自动刷新与 AI 流式打字机。
[]()
[
]()
[
]()
为什么需要它
在 uni-app 项目里,网络层通常是这样长起来的:utils/request.js 里塞一个 uni.request 封装,
再来个 socket.js 写一堆 setInterval 心跳,MQTT 在小程序上跑不起来只好换协议,
接入大模型时又发现流式输出在小程序端一片乱码……
本库把这些坑一次性填平:
| 能力 | 说明 |
|---|---|
| 六协议统一 | HTTP / WebSocket / MQTT / SSE / TCP / UDP,同一套实例管理与事件模型 |
| 拦截器体系 | 请求 / 响应 / 错误三段式,支持条件执行 runWhen 与 eject 卸载 |
| Token 自动刷新 | 内置单飞(single-flight)机制,并发 10 个 401 只刷新一次并自动重放 |
| 心跳 + 指数退避重连 | pong 超时判定链路假死,网络恢复立即重连,离线消息自动补发 |
| AI 流式打字机 | 自动处理中文分块乱码、跨 chunk 事件、[DONE] 标记,兼容 OpenAI / DeepSeek / Claude / 通义 |
| 小程序跑 MQTT | 内置 uni.connectSocket → W3C WebSocket 垫片,mqtt.js 零改造运行 |
| 类型安全 | 完整 .d.ts,事件回调参数自动推导,错误码枚举可穷举 |
| 单例 + 多实例 | 默认单例开箱即用,createInstance() 创建互不干扰的隔离实例 |
安装
方式一:uni_modules(推荐)
在 DCloud 插件市场 点击「下载插件并导入 HBuilderX」,
插件会自动放到项目的 uni_modules/net-client 目录,无需其它配置。
import net from '@/uni_modules/net-client'
方式二:npm
npm i net-client
# 需要 MQTT 时再装(可选依赖,不用就不会进包体)
npm i mqtt@4
import net from 'net-client'
兼容性
| 平台 | HTTP | WebSocket | MQTT | SSE | TCP/UDP |
|---|---|---|---|---|---|
| H5 | 支持 | 支持 | 支持 | 支持(fetch 流) | 不支持 |
| App (iOS / Android) | 支持 | 支持 | 支持 | 支持(chunked) | 支持 |
| 微信小程序 | 支持 | 支持 | 支持(wss) | 支持(chunked) | 支持 ¹ |
| 支付宝 / 百度 / 抖音小程序 | 支持 | 支持 | 支持(wss) | 视平台 ² | 视平台 |
¹ 微信小程序需在后台配置 TCP/UDP 合法域名。 ² 取决于各家小程序是否实现
enableChunked,未实现时自动降级为一次性返回。
快速上手
1. 全局初始化(main.ts)
import net from 'net-client'
net.http.setBaseURL('https://api.your-domain.com')
net.http.defaults.timeout = 20000
net.setDebug(process.env.NODE_ENV !== 'production') // 调试日志总开关
// Token 自动注入 + 401 自动刷新
net.http.setAuth({
getToken: () => uni.getStorageSync('access_token'),
refresh: async () => {
const { data } = await net.http.post('/auth/refresh',
{ refreshToken: uni.getStorageSync('refresh_token') },
{ skipInterceptors: true, skipAuth: true }) // 避免递归
uni.setStorageSync('access_token', data.accessToken)
return data.accessToken
},
onRefreshFailed: () => uni.reLaunch({ url: '/pages/login/index' })
})
// 业务码拆包
net.http.interceptors.response.use((res) => {
if (res.data?.code === 0) return { ...res, data: res.data.data }
throw new Error(res.data?.message || '请求失败')
})
2. 发请求
const { data } = await net.http.get<User>('/user/profile', { params: { id: 1 } })
await net.http.post('/order', { skuId: 1 })
// 取消
const task = net.http.get('/slow')
task.abort()
// 重试(指数退避)
net.http.get('/flaky', { retry: { retries: 3, baseDelay: 500 } })
// 搜索防抖:同 dedupeKey 的旧请求自动取消
net.http.get('/search', { params: { kw }, dedupeKey: 'search' })
3. WebSocket
const ws = net.ws('chat', {
url: 'wss://your-domain.com/ws',
query: () => ({ token: uni.getStorageSync('access_token') }),
heartbeat: { interval: 25000, timeout: 10000, isPong: (d) => d?.type === 'pong' },
reconnect: { baseDelay: 1000, maxDelay: 30000, factor: 1.8 }
})
ws.onOpen(({ reconnected }) => console.log(reconnected ? '重连成功' : '连接成功'))
ws.onMessage<{ content: string }>(({ data }) => console.log(data.content))
ws.connect()
ws.send({ type: 'chat', content: 'hello' }) // 未连接时自动进离线队列
4. AI 流式输出
const { result, abort } = net.aiStream({
url: 'https://api.deepseek.com/chat/completions',
header: { Authorization: `Bearer ${key}` },
data: { model: 'deepseek-chat', stream: true, messages },
onDelta: (delta, full) => (answer.value = full) // 打字机效果
})
// 停止生成
abort()
5. MQTT
import mqtt from 'mqtt/dist/mqtt.min.js'
const client = net.mqtt('device', { url: 'wss://broker:8084/mqtt', mqttLib: mqtt })
await client.connect()
await client.onTopic('device/+/telemetry', (payload) => console.log(payload))
await client.publish('device/1001/cmd', { cmd: 'light', value: 1 }, { qos: 1 })
6. 局域网设备发现(TCP / UDP)
// UDP 广播发现
const udp = net.udp()
udp.bind()
const devices = await udp.discover({
port: 6000,
probe: 'WHO_IS_THERE',
duration: 3000,
identify: (m) => m.remoteInfo.address
})
// TCP 直连
const tcp = net.tcp()
await tcp.connect({ host: '192.168.1.100', port: 8899 })
tcp.on('data', ({ text }) => console.log(text))
tcp.write('AT+STATUS\r\n')
完整可运行示例见 examples/ 目录:
net.config.ts(全局配置)、http-demo.vue、websocket-demo.vue、ai-stream-demo.vue、mqtt-demo.vue。
架构
src/
├── index.ts 统一导出入口(默认单例 net + 全部类型)
├── core/
│ ├── client.ts NetClient 门面:托管全部协议实例与生命周期
│ ├── http.ts HttpClient:拦截器 / 重试 / 取消 / Token 刷新
│ ├── websocket.ts WebSocketClient:心跳 / 退避重连 / 离线队列
│ ├── mqtt.ts MqttClient:订阅管理 / 通配符路由 / 断线恢复
│ ├── sse.ts SSEClient + SSEParser + createAIStream
│ ├── socket.ts TcpSocketClient / UdpSocketClient
│ └── interceptor.ts 拦截器管理器
├── adapters/
│ └── ws-polyfill.ts uni.connectSocket → W3C WebSocket 垫片
├── shared/
│ ├── emitter.ts 类型安全的发布订阅基类
│ ├── errors.ts NetError + 错误码枚举 + 文案国际化
│ ├── logger.ts 分级日志 / 全局开关 / 自定义上报
│ ├── platform.ts 运行时与平台探测、网络状态监听
│ ├── cancel.ts CancelToken
│ └── utils.ts URL / 退避 / 流式 UTF-8 解码
└── types/index.ts 全部对外 Interface 定义
单例与多实例:
import net, { createInstance } from 'net-client'
net.http.get('/a') // 默认单例
const b = createInstance({ http: { baseURL: 'https://b.com' } }) // 隔离实例
b.http.get('/c')
net.stats() // 查看当前所有连接状态,方便做调试面板
await net.destroy() // 退出登录:一键回收全部连接
API 参考
HttpClient
| 方法 | 说明 |
|---|---|
request<T>(config) |
通用请求,返回带 abort() 的 Promise |
get / post / put / patch / delete / head |
快捷方法 |
upload(config) / download(config) |
文件上传下载,支持 onProgress |
cancel(requestId) / cancelAll() |
取消指定 / 全部请求 |
setBaseURL / setHeaders / setAuth / setDebug |
链式配置 |
interceptors.request.use(onFulfilled, onRejected, { runWhen }) |
请求拦截器,返回 id 可 eject |
interceptors.response.use(...) |
响应 / 错误拦截器 |
extend(options) |
派生新实例 |
RequestConfig 常用字段
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
url |
string |
— | 相对或绝对地址 |
method |
HttpMethod |
GET |
请求方法 |
params |
object |
— | query 参数,自动序列化(数组 → a=1&a=2) |
data |
any |
— | 请求体 |
timeout |
number |
30000 |
含平台不支持时的定时器兜底 |
retry |
number \| RetryConfig |
0 |
指数退避重试 |
dedupeKey |
string \| fn |
— | 同 key 的旧请求自动取消 |
signal |
CancelToken \| AbortSignal |
— | 外部中断信号 |
skipAuth / skipInterceptors |
boolean |
false |
刷新 Token 等场景使用 |
validateStatus |
(code) => boolean |
2xx |
状态码校验 |
extra |
object |
— | 逃生舱:原样透传给底层 uni.request 的平台私有参数 |
关于
extra:RequestConfig刻意没有声明[key: string]: any索引签名。 带索引签名的接口一旦经过Omit<T, K>(get/post/upload的配置参数正是这么派生的),keyof会退化成string | number,导致所有具名属性被抹平、配置项写错也不报错。 因此平台私有参数统一走extra,换来的是http.get('/a', { timeoutt: 1000 })这类拼写错误能在编译期被抓住。net.http.get('/a', { extra: { someVendorOnlyFlag: true } })
AuthAdapter
| 字段 | 说明 |
|---|---|
getToken() |
读取 token,可异步 |
header / scheme |
默认 Authorization / 'Bearer ' |
shouldRefresh(res, cfg) |
默认 statusCode === 401 |
refresh() |
返回新 token;并发只执行一次(单飞) |
onRefreshFailed(err) |
一般在此跳登录 |
WebSocketClient
| 成员 | 说明 |
|---|---|
connect() / close(code, reason) / reconnect() / destroy() |
生命周期 |
send(data) / sendJSON(data) |
对象自动 JSON 化,未连接时进离线队列 |
ping() |
手动发一次心跳 |
state / isOpen / queuedCount |
状态查询 |
onOpen / onMessage / onError / onClose / onStateChange / onReconnect |
订阅,返回取消函数 |
on / once / off / onAny / waitFor |
通用事件 API |
SocketOptions
| 字段 | 默认 | 说明 |
|---|---|---|
heartbeat.interval |
25000 |
心跳间隔 |
heartbeat.timeout |
10000 |
pong 超时 → 判定链路已死并重连 |
heartbeat.isPong |
收到任意消息即存活 | 自定义 pong 判定 |
reconnect.baseDelay / maxDelay / factor / jitter |
1000 / 30000 / 1.8 / 0.3 |
指数退避 + 抖动 |
reconnect.maxRetries |
-1 |
-1 表示无限重连 |
reconnect.immediateOnNetworkResume |
true |
网络恢复立即重连 |
queueOffline / maxQueueSize |
true / 100 |
离线消息队列 |
codec |
JSON | 自定义编解码(如 protobuf) |
MqttClient
| 方法 | 说明 |
|---|---|
connect() |
建连,返回 Promise |
subscribe(topic \| topics, { qos }) |
订阅,内部记录用于重连恢复 |
unsubscribe(topic) / publish(topic, payload, { qos, retain }) |
取消订阅 / 发布消息 |
onTopic(filter, handler, opts) |
订阅 + 主题级回调,支持 + / # 通配符 |
end(force) / destroy() |
断开 / 销毁 |
isConnected / topics / clientId |
状态 |
小程序注意事项
- 必须使用
wss://(如 EMQX 的8084端口,路径通常为/mqtt)- 在小程序管理后台把 broker 域名加入 socket 合法域名
mqttLib请传mqtt/dist/mqtt.min.js(浏览器构建),Node 版会因缺少net模块报错
SSEClient / createAIStream
| 成员 | 说明 |
|---|---|
start() |
启动流,Promise 在流结束时 resolve |
abort(reason) / close() |
中断 |
on('open' \| 'message' \| 'chunk' \| 'done' \| 'error' \| 'reconnect') |
事件 |
transport |
当前实际使用的传输通道 |
| SSEOptions | 默认 | 说明 |
|---|---|---|
method |
POST |
大模型接口一般是 POST |
transport |
auto |
fetch / chunked / xhr / eventsource |
doneFlag |
'[DONE]' |
结束标记,传 false 关闭 |
parseJSON |
true |
自动把 data 解析到 ev.json |
timeout |
0 |
空闲超时(每收到一块数据就重置) |
reconnect |
false |
断流自动重连,携带 Last-Event-ID |
createAIStream 额外支持 onDelta(delta, full, ev)、onDone(full)、onError、pickDelta。
默认 pickDelta 已兼容:OpenAI / DeepSeek / Kimi / 智谱(choices[0].delta.content)、
Anthropic(content_block_delta)、DashScope 通义(output.text)。私有协议自行传入即可。
错误处理
import { isNetError, NetErrorCode } from 'net-client'
try {
await net.http.get('/x')
} catch (e) {
if (isNetError(e)) {
console.log(e.code) // NetErrorCode.TIMEOUT
console.log(e.statusCode) // HTTP 状态码(非 HTTP 错误为 undefined)
console.log(e.data) // 服务端返回的响应体(如果有)
console.log(e.detail) // { url, statusCode, cause, … } 完整上下文
console.log(e.retryable) // 是否可重试
console.log(e.isAbort, e.isTimeout)
}
}
错误码分组:通用 E_TIMEOUT / E_NETWORK / E_ABORTED / E_PARSE …、
HTTP E_HTTP_STATUS / E_HTTP_UNAUTHORIZED / E_HTTP_TOKEN_REFRESH_FAILED …、
WebSocket E_WS_*、MQTT E_MQTT_*、SSE E_SSE_*、Socket E_SOCKET_*。
支持文案国际化:
import { setErrorMessages, NetErrorCode } from 'net-client'
setErrorMessages({ [NetErrorCode.NETWORK]: 'Network unavailable' })
日志
import { setDebug, setLogLevel, setLogTransport, LogLevel } from 'net-client'
setDebug(true) // 一键打开全部模块调试日志
setLogLevel(LogLevel.WARN) // 精细控制
setLogTransport((level, scope, args) => reportToServer(level, scope, args))
开发与验证
npm install
npm run typecheck # 类型检查(含类型冒烟测试)
npm run test # 运行时冒烟测试(注入假 uni 运行时,56 条断言)
npm run build # 产出 dist/ + .d.ts
npm run verify # 以上三步串起来跑
仓库里有两类测试,职责不同:
| 文件 | 作用 |
|---|---|
tests/type-smoke.ts |
只测类型是否真的安全。用 @ts-expect-error 把"必须报错"的写法钉死(如配置项拼错、upload 漏传 url、事件名写错)。若哪天类型被改松,这些指令会变成"未使用"从而让 tsc 失败,回归立刻暴露。 |
tests/runtime-smoke.ts |
注入 mock 的 uni 运行时,真跑一遍核心链路:拦截器链、指数退避重试、401 单飞刷新、请求取消与去重、WebSocket 离线队列/心跳/断线重连、SSE 分片解析、UTF-8 中文跨分片不乱码、MQTT 通配符匹配。 |
跑测试不需要任何测试框架和额外依赖,只依赖 typescript 一个 devDependency。
FAQ
Q:小程序里 SSE 收到的中文是乱码?
A:本库内置流式 UTF-8 解码器,会把被 chunk 截断的多字节序列暂存到下一块,不会出现乱码。若你自行处理 chunk 事件,请直接用 ev.json / ev.data,不要对 ArrayBuffer 逐块 String.fromCharCode。
Q:WebSocket 明明"连着",但服务端收不到消息?
A:典型的假死连接。把 heartbeat.timeout 调小(如 8000),pong 超时会主动断开并触发重连。
Q:并发请求同时 401,会刷新很多次 Token 吗?
A:不会。AuthAdapter.refresh 有单飞保护,同一时刻只执行一次,其余请求等待同一个 Promise,成功后各自重放。
Q:H5 用 EventSource 不行吗?
A:EventSource 只支持 GET 且不能自定义请求头,无法携带 Authorization,所以默认走 fetch + ReadableStream;确有需要可 transport: 'eventsource'。
Q:包体积会不会很大?
A:核心库无任何运行时依赖,sideEffects: false 支持 Tree-shaking。MQTT 是可选 peerDependency,不用就完全不进包。
发布到 DCloud 插件市场
本仓库通过 scripts/sync-uni-module.js 把 src/ 同步进 uni_modules/net-client/,
该目录就是可直接发布的 uni_modules 插件包(已含 package.json / readme.md / changelog.md)。
插件目录结构:
uni_modules/net-client/
├── package.json # uni_modules 描述文件(id / 平台 / 依赖)
├── readme.md # 即本文档
├── changelog.md # 版本更新日志
├── index.ts # 统一导出入口(默认单例 net + 全部类型)
├── core/ # http / websocket / mqtt / sse / socket 实现
├── adapters/ # ws-polyfill 等平台垫片
├── shared/ # 事件 / 错误 / 日志 / 平台探测等基础能力
└── types/ # 全部对外 Interface 定义
提交方式(二选一):
- HBuilderX:右键
uni_modules/net-client目录 → 「发布到插件市场」。 - 插件市场网页:把
uni_modules/net-client整目录压缩上传(解压后根目录应为uni_modules/net-client/)。
可运行的示例工程在仓库的 demo/ 目录,可在插件市场「示例工程」处一并上传,方便用户一键导入体验。
License
MIT

收藏人数:
购买源码授权版(
试用
使用 HBuilderX 导入示例项目
赞赏(0)
下载 1
赞赏 0
下载 12499002
赞赏 1940
赞赏
京公网安备:11010802035340号