UNPKG

hera-cli

Version:
534 lines (455 loc) 14.3 kB
var requestLib = require('./request') var wxTunnel = require('./wxTunnel') /** * 当前打开的信道,同一时间只能有一个信道打开 */ var currentTunnel = null // 信道状态枚举 var STATUS_CLOSED = (Tunnel.STATUS_CLOSED = 'CLOSED') var STATUS_CONNECTING = (Tunnel.STATUS_CONNECTING = 'CONNECTING') var STATUS_ACTIVE = (Tunnel.STATUS_ACTIVE = 'ACTIVE') var STATUS_RECONNECTING = (Tunnel.STATUS_RECONNECTING = 'RECONNECTING') // 错误类型枚举 var ERR_CONNECT_SERVICE = (Tunnel.ERR_CONNECT_SERVICE = 1001) var ERR_CONNECT_SOCKET = (Tunnel.ERR_CONNECT_SOCKET = 1002) var ERR_RECONNECT = (Tunnel.ERR_RECONNECT = 2001) var ERR_SOCKET_ERROR = (Tunnel.ERR_SOCKET_ERROR = 3001) // 包类型枚举 var PACKET_TYPE_MESSAGE = 'message' var PACKET_TYPE_PING = 'ping' var PACKET_TYPE_PONG = 'pong' var PACKET_TYPE_TIMEOUT = 'timeout' var PACKET_TYPE_CLOSE = 'close' // 断线重连最多尝试 5 次 var DEFAULT_MAX_RECONNECT_TRY_TIMES = 5 // 每次重连前,等待时间的增量值 var DEFAULT_RECONNECT_TIME_INCREASE = 1000 function Tunnel (serviceUrl) { if (currentTunnel && currentTunnel.status !== STATUS_CLOSED) { throw new Error('当前有未关闭的信道,请先关闭之前的信道,再打开新信道') } currentTunnel = this // 等确认微信小程序全面支持 ES6 就不用那么麻烦了 var me = this //= ======================================================================== // 暴露实例状态以及方法 //= ======================================================================== this.serviceUrl = serviceUrl this.socketUrl = null this.status = null this.open = openConnect this.on = registerEventHandler this.emit = emitMessagePacket this.close = close this.isClosed = isClosed this.isConnecting = isConnecting this.isActive = isActive this.isReconnecting = isReconnecting //= ======================================================================== // 信道状态处理,状态说明: // closed - 已关闭 // connecting - 首次连接 // active - 当前信道已经在工作 // reconnecting - 断线重连中 //= ======================================================================== function isClosed () { return me.status === STATUS_CLOSED } function isConnecting () { return me.status === STATUS_CONNECTING } function isActive () { return me.status === STATUS_ACTIVE } function isReconnecting () { return me.status === STATUS_RECONNECTING } function setStatus (status) { var lastStatus = me.status if (lastStatus !== status) { me.status = status } } // 初始为关闭状态 setStatus(STATUS_CLOSED) //= ======================================================================== // 信道事件处理机制 // 信道事件包括: // connect - 连接已建立 // close - 连接被关闭(包括主动关闭和被动关闭) // reconnecting - 开始重连 // reconnect - 重连成功 // error - 发生错误,其中包括连接失败、重连失败、解包失败等等 // [message] - 信道服务器发送过来的其它事件类型,如果事件类型和上面内置的事件类型冲突,将在事件类型前面添加前缀 `@` //= ======================================================================== var preservedEventTypes = 'connect,close,reconnecting,reconnect,error'.split( ',' ) var eventHandlers = [] /** * 注册消息处理函数 * @param {string} messageType 支持内置消息类型("connect"|"close"|"reconnecting"|"reconnect"|"error")以及业务消息类型 */ function registerEventHandler (eventType, eventHandler) { if (typeof eventHandler === 'function') { eventHandlers.push([eventType, eventHandler]) } } /** * 派发事件,通知所有处理函数进行处理 */ function dispatchEvent (eventType, eventPayload) { eventHandlers.forEach(function (handler) { var handleType = handler[0] var handleFn = handler[1] if (handleType === '*') { handleFn(eventType, eventPayload) } else if (handleType === eventType) { handleFn(eventPayload) } }) } /** * 派发事件,事件类型和系统保留冲突的,事件名会自动加上 '@' 前缀 */ function dispatchEscapedEvent (eventType, eventPayload) { if (preservedEventTypes.indexOf(eventType) > -1) { eventType = '@' + eventType } dispatchEvent(eventType, eventPayload) } //= ======================================================================== // 信道连接控制 //= ======================================================================== var isFirstConnection = true var isOpening = false /** * 连接信道服务器,获取 WebSocket 连接地址,获取地址成功后,开始进行 WebSocket 连接 */ function openConnect () { if (isOpening) return isOpening = true // 只有关闭状态才会重新进入准备中 setStatus(isFirstConnection ? STATUS_CONNECTING : STATUS_RECONNECTING) requestLib.request({ url: serviceUrl, method: 'GET', success: function (response) { if ( +response.statusCode === 200 && response.data && response.data.url ) { openSocket((me.socketUrl = response.data.url)) } else { dispatchConnectServiceError(response) } }, fail: dispatchConnectServiceError, complete: () => (isOpening = false) }) function dispatchConnectServiceError (detail) { if (isFirstConnection) { setStatus(STATUS_CLOSED) dispatchEvent('error', { code: ERR_CONNECT_SERVICE, message: '连接信道服务失败,网络错误或者信道服务没有正确响应', detail: detail || null }) } else { startReconnect(detail) } } } /** * 打开 WebSocket 连接,打开后,注册微信的 Socket 处理方法 */ function openSocket (url) { wxTunnel.listen({ onOpen: handleSocketOpen, onMessage: handleSocketMessage, onClose: handleSocketClose, onError: handleSocketError }) wx.connectSocket({ url: url }) isFirstConnection = false } //= ======================================================================== // 处理消息通讯 // // packet - 数据包,序列化形式为 `${type}` 或者 `${type}:${content}` // packet.type - 包类型,包括 message, ping, pong, close // packet.content? - 当包类型为 message 的时候,会附带 message 数据 // // message - 消息体,会使用 JSON 序列化后作为 packet.content // message.type - 消息类型,表示业务消息类型 // message.content? - 消息实体,可以为任意类型,表示消息的附带数据,也可以为空 // // 数据包示例: // - 'ping' 表示 Ping 数据包 // - 'message:{"type":"speak","content":"hello"}' 表示一个打招呼的数据包 //= ======================================================================== // 连接还没成功建立的时候,需要发送的包会先存放到队列里 var queuedPackets = [] /** * WebSocket 打开之后,更新状态,同时发送所有遗留的数据包 */ function handleSocketOpen () { /* istanbul ignore else */ if (isConnecting()) { dispatchEvent('connect') } else if (isReconnecting()) { dispatchEvent('reconnect') resetReconnectionContext() } setStatus(STATUS_ACTIVE) emitQueuedPackets() nextPing() } /** * 收到 WebSocket 数据包,交给处理函数 */ function handleSocketMessage (message) { resolvePacket(message.data) } /** * 发送数据包,如果信道没有激活,将先存放队列 */ function emitPacket (packet) { if (isActive()) { sendPacket(packet) } else { queuedPackets.push(packet) } } /** * 数据包推送到信道 */ function sendPacket (packet) { var encodedPacket = [packet.type] if (packet.content) { encodedPacket.push(JSON.stringify(packet.content)) } wx.sendSocketMessage({ data: encodedPacket.join(':'), fail: handleSocketError }) } function emitQueuedPackets () { queuedPackets.forEach(emitPacket) // empty queued packets queuedPackets.length = 0 } /** * 发送消息包 */ function emitMessagePacket (messageType, messageContent) { var packet = { type: PACKET_TYPE_MESSAGE, content: { type: messageType, content: messageContent } } emitPacket(packet) } /** * 发送 Ping 包 */ function emitPingPacket () { emitPacket({ type: PACKET_TYPE_PING }) } /** * 发送关闭包 */ function emitClosePacket () { emitPacket({ type: PACKET_TYPE_CLOSE }) } /** * 解析并处理从信道接收到的包 */ function resolvePacket (raw) { var packetParts = raw.split(':') var packetType = packetParts.shift() var packetContent = packetParts.join(':') || null var packet = { type: packetType } if (packetContent) { try { packet.content = JSON.parse(packetContent) } catch (e) {} } switch (packet.type) { case PACKET_TYPE_MESSAGE: handleMessagePacket(packet) break case PACKET_TYPE_PONG: handlePongPacket(packet) break case PACKET_TYPE_TIMEOUT: handleTimeoutPacket(packet) break case PACKET_TYPE_CLOSE: handleClosePacket(packet) break default: handleUnknownPacket(packet) break } } /** * 收到消息包,直接 dispatch 给处理函数 */ function handleMessagePacket (packet) { var message = packet.content dispatchEscapedEvent(message.type, message.content) } //= ======================================================================== // 心跳、断开与重连处理 //= ======================================================================== /** * Ping-Pong 心跳检测超时控制,这个值有两个作用: * 1. 表示收到服务器的 Pong 相应之后,过多久再发下一次 Ping * 2. 如果 Ping 发送之后,超过这个时间还没收到 Pong,断开与服务器的连接 * 该值将在与信道服务器建立连接后被更新 */ let pingPongTimeout = 15000 let pingTimer = 0 let pongTimer = 0 /** * 信道服务器返回 Ping-Pong 控制超时时间 */ function handleTimeoutPacket (packet) { var timeout = packet.content * 1000 /* istanbul ignore else */ if (!isNaN(timeout)) { pingPongTimeout = timeout ping() } } /** * 收到服务器 Pong 响应,定时发送下一个 Ping */ function handlePongPacket (packet) { nextPing() } /** * 发送下一个 Ping 包 */ function nextPing () { clearTimeout(pingTimer) clearTimeout(pongTimer) pingTimer = setTimeout(ping, pingPongTimeout) } /** * 发送 Ping,等待 Pong */ function ping () { /* istanbul ignore else */ if (isActive()) { emitPingPacket() // 超时没有响应,关闭信道 pongTimer = setTimeout(handlePongTimeout, pingPongTimeout) } } /** * Pong 超时没有响应,信道可能已经不可用,需要断开重连 */ function handlePongTimeout () { startReconnect('服务器已失去响应') } // 已经重连失败的次数 var reconnectTryTimes = 0 // 最多允许失败次数 var maxReconnectTryTimes = Tunnel.MAX_RECONNECT_TRY_TIMES || DEFAULT_MAX_RECONNECT_TRY_TIMES // 重连前等待的时间 var waitBeforeReconnect = 0 // 重连前等待时间增量 var reconnectTimeIncrease = Tunnel.RECONNECT_TIME_INCREASE || DEFAULT_RECONNECT_TIME_INCREASE var reconnectTimer = 0 function startReconnect (lastError) { if (reconnectTryTimes >= maxReconnectTryTimes) { close() dispatchEvent('error', { code: ERR_RECONNECT, message: '重连失败', detail: lastError }) } else { wx.closeSocket() waitBeforeReconnect += reconnectTimeIncrease setStatus(STATUS_RECONNECTING) reconnectTimer = setTimeout(doReconnect, waitBeforeReconnect) } if (reconnectTryTimes === 0) { dispatchEvent('reconnecting') } reconnectTryTimes += 1 } function doReconnect () { openConnect() } function resetReconnectionContext () { reconnectTryTimes = 0 waitBeforeReconnect = 0 } /** * 收到服务器的关闭请求 */ function handleClosePacket (packet) { close() } function handleUnknownPacket (packet) { // throw away } var isClosing = false /** * 收到 WebSocket 断开的消息,处理断开逻辑 */ function handleSocketClose () { /* istanbul ignore if */ if (isClosing) return /* istanbul ignore else */ if (isActive()) { // 意外断开的情况,进行重连 startReconnect('链接已断开') } } function close () { isClosing = true closeSocket() setStatus(STATUS_CLOSED) resetReconnectionContext() isFirstConnection = false clearTimeout(pingTimer) clearTimeout(pongTimer) clearTimeout(reconnectTimer) dispatchEvent('close') isClosing = false } function closeSocket (emitClose) { if (isActive() && emitClose !== false) { emitClosePacket() } wx.closeSocket() } //= ======================================================================== // 错误处理 //= ======================================================================== /** * 错误处理 */ function handleSocketError (detail) { switch (me.status) { case Tunnel.STATUS_CONNECTING: dispatchEvent('error', { code: ERR_SOCKET_ERROR, message: '连接信道失败,网络错误或者信道服务不可用', detail: detail }) break } } } module.exports = Tunnel