更新记录

1.0.0(2026-08-11)

  • 1.0.0:首版,包含 HTTP / WebSocket / MQTT / SSE / TCP / UDP 六协议与拦截器、重连、Token 刷新、流式输出等能力。
  • 更新时修改 uni_modules/net-client/package.jsonversion,并在 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 流式打字机。

[platform]() [typescript]() [license]()


为什么需要它

在 uni-app 项目里,网络层通常是这样长起来的:utils/request.js 里塞一个 uni.request 封装, 再来个 socket.js 写一堆 setInterval 心跳,MQTT 在小程序上跑不起来只好换协议, 接入大模型时又发现流式输出在小程序端一片乱码……

本库把这些坑一次性填平:

能力 说明
六协议统一 HTTP / WebSocket / MQTT / SSE / TCP / UDP,同一套实例管理与事件模型
拦截器体系 请求 / 响应 / 错误三段式,支持条件执行 runWheneject 卸载
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.vuewebsocket-demo.vueai-stream-demo.vuemqtt-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 的平台私有参数

关于 extraRequestConfig 刻意没有声明 [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 状态

小程序注意事项

  1. 必须使用 wss://(如 EMQX 的 8084 端口,路径通常为 /mqtt
  2. 在小程序管理后台把 broker 域名加入 socket 合法域名
  3. 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)onErrorpickDelta。 默认 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.jssrc/ 同步进 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 定义

提交方式(二选一):

  1. HBuilderX:右键 uni_modules/net-client 目录 → 「发布到插件市场」。
  2. 插件市场网页:把 uni_modules/net-client 整目录压缩上传(解压后根目录应为 uni_modules/net-client/)。

可运行的示例工程在仓库的 demo/ 目录,可在插件市场「示例工程」处一并上传,方便用户一键导入体验。


License

MIT

隐私、权限声明

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

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

插件不采集任何用户数据,所有网络请求均由使用方自行发起

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

暂无用户评论。