diff --git a/packages/uni-api/src/index.ts b/packages/uni-api/src/index.ts index 443d6c9b865f9163b984fd2e93207470e8bc21c0..a6c999eb33435a916b77de6118ecfa3ea3fae7e2 100644 --- a/packages/uni-api/src/index.ts +++ b/packages/uni-api/src/index.ts @@ -27,6 +27,7 @@ export * from './protocols/media/getImageInfo' export * from './protocols/network/request' export * from './protocols/network/downloadFile' export * from './protocols/network/uploadFile' +export * from './protocols/network/socket' export * from './protocols/route/route' diff --git a/packages/uni-api/src/protocols/network/socket.ts b/packages/uni-api/src/protocols/network/socket.ts index 4eabdc708efb059ace55575cad790b423d62dea1..39e0cd911350d71cc257b7836b5ce9bd92462352 100644 --- a/packages/uni-api/src/protocols/network/socket.ts +++ b/packages/uni-api/src/protocols/network/socket.ts @@ -35,13 +35,13 @@ export const ConnectSocketProtocol: ApiProtocol = { export const API_SEND_SOCKET_MESSAGE = 'sendSocketMessage' export type API_TYPE_SEND_SOCKET_MESSAGE = typeof uni.sendSocketMessage -export const sendSocketMessage: ApiProtocol = { +export const SendSocketMessageProtocol: ApiProtocol = { data: [String, ArrayBuffer], } export const API_CLOSE_SOCKET = 'closeSocket' export type API_TYPE_CLOSE_SOCKET = typeof uni.closeSocket -export const closeSocket: ApiProtocol = { +export const CloseSocketProtocol: ApiProtocol = { code: Number, reason: String, } diff --git a/packages/uni-h5/dist/uni-h5.esm.js b/packages/uni-h5/dist/uni-h5.esm.js index 67e55bcd8cac9e78244a02c8804a82d64bd6746f..3f89a0d2b593d4c96faddba091848002fdd48cde 100644 --- a/packages/uni-h5/dist/uni-h5.esm.js +++ b/packages/uni-h5/dist/uni-h5.esm.js @@ -4739,6 +4739,42 @@ const UploadFileProtocol = { formData: Object, timeout: Number }; +const API_CONNECT_SOCKET = "connectSocket"; +const ConnectSocketOptions = { + formatArgs: { + header(value, params) { + params.header = value || {}; + }, + method(value, params) { + params.method = elemInArray((value || "").toUpperCase(), HTTP_METHODS); + }, + protocols(protocols, params) { + if (typeof protocols === "string") { + params.protocols = [protocols]; + } + } + } +}; +const ConnectSocketProtocol = { + url: { + type: String, + required: true + }, + header: { + type: Object + }, + method: String, + protocols: [Array, String] +}; +const API_SEND_SOCKET_MESSAGE = "sendSocketMessage"; +const SendSocketMessageProtocol = { + data: [String, ArrayBuffer] +}; +const API_CLOSE_SOCKET = "closeSocket"; +const CloseSocketProtocol = { + code: Number, + reason: String +}; function encodeQueryString(url) { if (typeof url !== "string") { return url; @@ -11070,6 +11106,183 @@ const uploadFile = /* @__PURE__ */ defineTaskApi(API_UPLOAD_FILE, ({ }); return uploadTask; }, UploadFileProtocol, UploadFileOptions); +const socketTasks = []; +const globalEvent = { + open: "", + close: "", + error: "", + message: "" +}; +class SocketTask { + constructor(url, protocols, callback) { + this._callbacks = { + open: [], + close: [], + error: [], + message: [] + }; + let error; + try { + const webSocket = this._webSocket = new WebSocket(url, protocols); + webSocket.binaryType = "arraybuffer"; + const eventNames = ["open", "close", "error", "message"]; + eventNames.forEach((name) => { + this._callbacks[name] = []; + webSocket.addEventListener(name, (event2) => { + const res = name === "message" ? { + data: event2.data + } : {}; + this._callbacks[name].forEach((callback2) => { + try { + callback2(res); + } catch (e2) { + console.error(`thirdScriptError +${e2};at socketTask.on${capitalize(name)} callback function +`, e2); + } + }); + if (this === socketTasks[0] && globalEvent[name]) { + UniServiceJSBridge.invokeOnCallback(globalEvent[name], res); + } + if (name === "error" || name === "close") { + const index2 = socketTasks.indexOf(this); + if (index2 >= 0) { + socketTasks.splice(index2, 1); + } + } + }); + }); + const propertys = [ + "CLOSED", + "CLOSING", + "CONNECTING", + "OPEN", + "readyState" + ]; + propertys.forEach((property) => { + Object.defineProperty(this, property, { + get() { + return webSocket[property]; + } + }); + }); + } catch (e2) { + error = e2; + } + callback && callback(error, this); + } + send(options) { + const data = (options || {}).data; + const ws = this._webSocket; + try { + if (ws.readyState !== ws.OPEN) { + throw new Error("SocketTask.readyState is not OPEN"); + } + ws.send(data); + this._callback(options, "sendSocketMessage:ok"); + } catch (error) { + this._callback(options, `sendSocketMessage:fail ${error}`); + } + } + close(options = {}) { + const ws = this._webSocket; + try { + const code = options.code || 1e3; + const reason = options.reason; + if (typeof reason === "string") { + ws.close(code, reason); + } else { + ws.close(code); + } + this._callback(options, "closeSocket:ok"); + } catch (error) { + this._callback(options, `closeSocket:fail ${error}`); + } + } + _callback({ + success, + fail, + complete + } = {}, errMsg) { + const data = { + errMsg + }; + if (/:ok$/.test(errMsg)) { + if (typeof success === "function") { + success(data); + } + } else { + if (typeof fail === "function") { + fail(data); + } + } + if (typeof complete === "function") { + complete(data); + } + } + onOpen(callback) { + this._callbacks.open.push(callback); + } + onMessage(callback) { + this._callbacks.message.push(callback); + } + onError(callback) { + this._callbacks.error.push(callback); + } + onClose(callback) { + this._callbacks.close.push(callback); + } +} +const connectSocket = /* @__PURE__ */ defineTaskApi(API_CONNECT_SOCKET, ({url, protocols}, {resolve, reject}) => { + return new SocketTask(url, protocols, (error, socketTask) => { + if (error) { + reject(error.toString()); + return; + } + socketTasks.push(socketTask); + resolve(); + }); +}, ConnectSocketProtocol, ConnectSocketOptions); +function callSocketTask(socketTask, method, option, resolve, reject) { + const fn = socketTask[method]; + if (typeof fn === "function") { + fn.call(socketTask, Object.assign({}, option, { + success() { + resolve(); + }, + fail({errMsg}) { + reject(errMsg.replace("sendSocketMessage:fail ", "")); + }, + complete: void 0 + })); + } +} +const sendSocketMessage = /* @__PURE__ */ defineAsyncApi(API_SEND_SOCKET_MESSAGE, (options, {resolve, reject}) => { + const socketTask = socketTasks[0]; + if (socketTask && socketTask.readyState === socketTask.OPEN) { + callSocketTask(socketTask, "send", options, resolve, reject); + } else { + reject("WebSocket is not connected"); + } +}, SendSocketMessageProtocol); +const closeSocket = /* @__PURE__ */ defineAsyncApi(API_CLOSE_SOCKET, (options, {resolve, reject}) => { + const socketTask = socketTasks[0]; + if (socketTask) { + callSocketTask(socketTask, "send", options, resolve, reject); + } else { + reject("WebSocket is not connected"); + } +}, CloseSocketProtocol); +function on(event2) { + const api2 = `onSocket${capitalize(event2)}`; + return /* @__PURE__ */ defineOnApi(api2, () => { + globalEvent[event2] = api2; + }); +} +const onSocketOpen = on("open"); +const onSocketError = on("error"); +const onSocketMessage = on("message"); +const onSocketClose = on("close"); const navigateBack = /* @__PURE__ */ defineAsyncApi(API_NAVIGATE_BACK, ({delta}, {resolve, reject}) => { let canBack = true; if (invokeHook("onBackPress") === true) { @@ -11288,6 +11501,13 @@ var api = /* @__PURE__ */ Object.freeze({ request, downloadFile, uploadFile, + connectSocket, + sendSocketMessage, + closeSocket, + onSocketOpen, + onSocketError, + onSocketMessage, + onSocketClose, navigateBack, navigateTo, redirectTo, @@ -12352,4 +12572,4 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { ]); } _sfc_main.render = _sfc_render; -export {_sfc_main$1 as AsyncErrorComponent, _sfc_main as AsyncLoadingComponent, _sfc_main$n as Audio, index$4 as Button, _sfc_main$m as Canvas, _sfc_main$l as Checkbox, _sfc_main$k as CheckboxGroup, _sfc_main$j as Editor, index$5 as Form, index$3 as Icon, _sfc_main$h as Image, _sfc_main$g as Input, _sfc_main$f as Label, LayoutComponent, _sfc_main$e as MovableView, _sfc_main$d as Navigator, index as PageComponent, _sfc_main$c as Progress, _sfc_main$b as Radio, _sfc_main$a as RadioGroup, _sfc_main$i as ResizeSensor, _sfc_main$9 as RichText, _sfc_main$8 as ScrollView, _sfc_main$7 as Slider, _sfc_main$6 as SwiperItem, _sfc_main$5 as Switch, index$2 as Text, _sfc_main$4 as Textarea, UniServiceJSBridge$1 as UniServiceJSBridge, UniViewJSBridge$1 as UniViewJSBridge, _sfc_main$3 as Video, index$1 as View, addInterceptor, arrayBufferToBase64, base64ToArrayBuffer, canIUse, createIntersectionObserver, createSelectorQuery, createVideoContext, cssBackdropFilter, cssConstant, cssEnv, cssVar, downloadFile, getApp$1 as getApp, getCurrentPages$1 as getCurrentPages, getImageInfo, getNetworkType, getSystemInfo, getSystemInfoSync, hideLoading, hideNavigationBarLoading, hideTabBar, hideTabBarRedDot, hideToast, makePhoneCall, navigateBack, navigateTo, offNetworkStatusChange, onNetworkStatusChange, onTabBarMidButtonTap, openDocument, index$6 as plugin, promiseInterceptor, reLaunch, redirectTo, removeInterceptor, removeTabBarBadge, request, setNavigationBarColor, setNavigationBarTitle, setTabBarBadge, setTabBarItem, setTabBarStyle, setupApp, setupPage, showActionSheet, showLoading, showModal, showNavigationBarLoading, showTabBar, showTabBarRedDot, showToast, switchTab, uni$1 as uni, uploadFile, upx2px, useSubscribe}; +export {_sfc_main$1 as AsyncErrorComponent, _sfc_main as AsyncLoadingComponent, _sfc_main$n as Audio, index$4 as Button, _sfc_main$m as Canvas, _sfc_main$l as Checkbox, _sfc_main$k as CheckboxGroup, _sfc_main$j as Editor, index$5 as Form, index$3 as Icon, _sfc_main$h as Image, _sfc_main$g as Input, _sfc_main$f as Label, LayoutComponent, _sfc_main$e as MovableView, _sfc_main$d as Navigator, index as PageComponent, _sfc_main$c as Progress, _sfc_main$b as Radio, _sfc_main$a as RadioGroup, _sfc_main$i as ResizeSensor, _sfc_main$9 as RichText, _sfc_main$8 as ScrollView, _sfc_main$7 as Slider, _sfc_main$6 as SwiperItem, _sfc_main$5 as Switch, index$2 as Text, _sfc_main$4 as Textarea, UniServiceJSBridge$1 as UniServiceJSBridge, UniViewJSBridge$1 as UniViewJSBridge, _sfc_main$3 as Video, index$1 as View, addInterceptor, arrayBufferToBase64, base64ToArrayBuffer, canIUse, closeSocket, connectSocket, createIntersectionObserver, createSelectorQuery, createVideoContext, cssBackdropFilter, cssConstant, cssEnv, cssVar, downloadFile, getApp$1 as getApp, getCurrentPages$1 as getCurrentPages, getImageInfo, getNetworkType, getSystemInfo, getSystemInfoSync, hideLoading, hideNavigationBarLoading, hideTabBar, hideTabBarRedDot, hideToast, makePhoneCall, navigateBack, navigateTo, offNetworkStatusChange, onNetworkStatusChange, onSocketClose, onSocketError, onSocketMessage, onSocketOpen, onTabBarMidButtonTap, openDocument, index$6 as plugin, promiseInterceptor, reLaunch, redirectTo, removeInterceptor, removeTabBarBadge, request, sendSocketMessage, setNavigationBarColor, setNavigationBarTitle, setTabBarBadge, setTabBarItem, setTabBarStyle, setupApp, setupPage, showActionSheet, showLoading, showModal, showNavigationBarLoading, showTabBar, showTabBarRedDot, showToast, switchTab, uni$1 as uni, uploadFile, upx2px, useSubscribe}; diff --git a/packages/uni-h5/src/service/api/index.ts b/packages/uni-h5/src/service/api/index.ts index fcf3f9f0c9ae7f7c7a7ccea1b382833ea35686b6..f3bfabb374cbb7498a32bad9b1df2064bc9ce021 100644 --- a/packages/uni-h5/src/service/api/index.ts +++ b/packages/uni-h5/src/service/api/index.ts @@ -12,6 +12,7 @@ export * from './media/getImageInfo' export * from './network/request' export * from './network/downloadFile' export * from './network/uploadFile' +export * from './network/socket' export * from './route/navigateBack' export * from './route/navigateTo' diff --git a/packages/uni-h5/src/service/api/network/socket.ts b/packages/uni-h5/src/service/api/network/socket.ts new file mode 100644 index 0000000000000000000000000000000000000000..85697c80c8db3e90fa41855d832857f2c90010aa --- /dev/null +++ b/packages/uni-h5/src/service/api/network/socket.ts @@ -0,0 +1,271 @@ +import { capitalize } from '@vue/shared' +import { + defineTaskApi, + defineAsyncApi, + defineOnApi, + API_CONNECT_SOCKET, + API_TYPE_CONNECT_SOCKET, + ConnectSocketProtocol, + ConnectSocketOptions, + API_SEND_SOCKET_MESSAGE, + API_TYPE_SEND_SOCKET_MESSAGE, + SendSocketMessageProtocol, + API_CLOSE_SOCKET, + API_TYPE_CLOSE_SOCKET, + CloseSocketProtocol, +} from '@dcloudio/uni-api' + +type eventName = keyof WebSocketEventMap + +const socketTasks: SocketTask[] = [] +const globalEvent: Record = { + open: '', + close: '', + error: '', + message: '', +} + +class SocketTask implements UniApp.SocketTask { + /** + * WebSocket实例 + */ + _webSocket?: WebSocket + _callbacks: Record = { + open: [], + close: [], + error: [], + message: [], + } + readonly CLOSED?: number + readonly CLOSING?: number + readonly CONNECTING?: number + readonly OPEN?: number + readonly readyState?: number + /** + * 构造函数 + * @param {string} url + * @param {Array} protocols + */ + constructor(url: string, protocols?: string[], callback?: Function) { + let error + try { + const webSocket = (this._webSocket = new WebSocket(url, protocols)) + webSocket.binaryType = 'arraybuffer' + const eventNames: eventName[] = ['open', 'close', 'error', 'message'] + eventNames.forEach((name: eventName) => { + this._callbacks[name] = [] + webSocket.addEventListener(name, (event) => { + const res = + name === 'message' + ? { + data: (event).data, + } + : {} + this._callbacks[name].forEach((callback) => { + try { + callback(res) + } catch (e) { + console.error( + `thirdScriptError\n${e};at socketTask.on${capitalize( + name + )} callback function\n`, + e + ) + } + }) + if (this === socketTasks[0] && globalEvent[name]) { + UniServiceJSBridge.invokeOnCallback(globalEvent[name], res) + } + if (name === 'error' || name === 'close') { + const index = socketTasks.indexOf(this) + if (index >= 0) { + socketTasks.splice(index, 1) + } + } + }) + }) + const propertys: (keyof WebSocket)[] = [ + 'CLOSED', + 'CLOSING', + 'CONNECTING', + 'OPEN', + 'readyState', + ] + propertys.forEach((property) => { + Object.defineProperty(this, property, { + get() { + return webSocket[property] + }, + }) + }) + } catch (e) { + error = e + } + callback && callback(error, this) + } + + /** + * 发送 + * @param {any} data + */ + send(options: UniApp.SendSocketMessageOptions) { + const data = (options || {}).data + const ws = this._webSocket + try { + if (ws.readyState !== ws.OPEN) { + throw new Error('SocketTask.readyState is not OPEN') + } + ws.send(data) + this._callback(options, 'sendSocketMessage:ok') + } catch (error) { + this._callback(options, `sendSocketMessage:fail ${error}`) + } + } + + /** + * 关闭 + * @param {number} code + * @param {string} reason + */ + close(options: UniApp.CloseSocketOptions = {}) { + const ws = this._webSocket + try { + const code = options.code || 1000 + const reason = options.reason + if (typeof reason === 'string') { + ws.close(code, reason) + } else { + ws.close(code) + } + this._callback(options, 'closeSocket:ok') + } catch (error) { + this._callback(options, `closeSocket:fail ${error}`) + } + } + + /** + * 通用回调处理 + */ + _callback( + { + success, + fail, + complete, + }: UniApp.SendSocketMessageOptions | UniApp.CloseSocketOptions = {}, + errMsg: string + ) { + const data = { + errMsg, + } + if (/:ok$/.test(errMsg)) { + if (typeof success === 'function') { + success(data) + } + } else { + if (typeof fail === 'function') { + fail(data) + } + } + if (typeof complete === 'function') { + complete(data) + } + } + + onOpen(callback: (result: any) => void) { + this._callbacks.open.push(callback) + } + onMessage(callback: (result: any) => void) { + this._callbacks.message.push(callback) + } + onError(callback: (result: any) => void) { + this._callbacks.error.push(callback) + } + onClose(callback: (result: any) => void) { + this._callbacks.close.push(callback) + } +} + +export const connectSocket = defineTaskApi( + API_CONNECT_SOCKET, + ({ url, protocols }, { resolve, reject }) => { + return new SocketTask( + url, + protocols, + (error: Error, socketTask: SocketTask) => { + if (error) { + reject(error.toString()) + return + } + socketTasks.push(socketTask) + resolve() + } + ) + }, + ConnectSocketProtocol, + ConnectSocketOptions +) + +function callSocketTask( + socketTask: SocketTask, + method: 'send' | 'close', + option: any, + resolve: Function, + reject: Function +) { + const fn = socketTask[method] + if (typeof fn === 'function') { + fn.call( + socketTask, + Object.assign({}, option, { + success() { + resolve() + }, + fail({ errMsg }: any) { + reject(errMsg.replace('sendSocketMessage:fail ', '')) + }, + complete: undefined, + }) + ) + } +} + +export const sendSocketMessage = defineAsyncApi( + API_SEND_SOCKET_MESSAGE, + (options, { resolve, reject }) => { + const socketTask = socketTasks[0] + if (socketTask && socketTask.readyState === socketTask.OPEN) { + callSocketTask(socketTask, 'send', options, resolve, reject) + } else { + reject('WebSocket is not connected') + } + }, + SendSocketMessageProtocol +) + +export const closeSocket = defineAsyncApi( + API_CLOSE_SOCKET, + (options, { resolve, reject }) => { + const socketTask = socketTasks[0] + if (socketTask) { + callSocketTask(socketTask, 'send', options, resolve, reject) + } else { + reject('WebSocket is not connected') + } + }, + CloseSocketProtocol +) + +function on(event: eventName) { + const api = `onSocket${capitalize(event)}` + return defineOnApi(api, () => { + globalEvent[event] = api + }) +} + +export const onSocketOpen = on('open') + +export const onSocketError = on('error') + +export const onSocketMessage = on('message') + +export const onSocketClose = on('close')