Categories
程式開發

实现微信小程序编译和运行环境系列(核心篇二)


在上文中我们有点到小程序开发者工具里面的消息是通过websocket协议发送和接受处理的,当然这个不是凭空而说的,是在小程序的逻辑层appservice.js源码里面有代码表明的,至于它的消息格式还有一部分我没有列出来,比如它的数据分析和上报他们自己服务器的一些消息格式可以先先需要关注。

下面还是先给大家展示一下流程找到appservice.js源码文件

实现微信小程序编译和运行环境系列(核心篇二) 1

可以看到它的链接地址,数据发送和接收的部分代码,由于图片尺寸问题我折叠了部分代码,大家可以自己去细看看。

我还是先简述一些webstocket的知识,可能部分同学对这方面不是很熟悉。细节webstocket内容不会在本文描述,后期会写一篇专门的介绍。

websocket是什么

其实这些内容我们通过谷歌搜索可以查阅很多材料,但有没有真正理解可以在自己项目里进行灵活设计运用还是只是简单使用文档 api,还是要靠自己多探索思考一些。

下图为webstockrt协议

实现微信小程序编译和运行环境系列(核心篇二) 2

可以理解为:WebSocket协议允许在运行于受控环境中的不受信任代码的用户代理与已选择从该代码进行通信的远程主机之间进行双向通信。简单点描述就是:客户端和服务器之间存在持久连接,而且双方都可以随时随地相互发送数据

为什么用websocket

一项新规范或者一门新技术的诞生肯定是为了解决或者完善前面方案的不足,这样才能一直进步下去。

在没有websocket之前我们采用http用的很好,但是随着一些应用的要求像聊天 股票 游戏 这种对实时性数据要求高的系统,才用HTTP 协议发送数据的话只能有客户端单方面进行请求,服务端响应获取最新数据,如果服务端的数据变换很快比如股票的信息,因此只能定时去请求,就出出现效率低 浪费资源而且数据还不实时同步的情况,为了解决这些问题通过研究websocket协议就闪亮登场了。

websocket具备的一些优点:

  • 支持双向通信,具有很强的实时性
  • 对二进制的支持比较友好
  • 相比与http协议的控制开销要少很多
  • 用户可以自由的扩展协议,自定义子协议例如(wss)

如何使用websocket

这个点比较广泛一个新方案新技术的产生都会经过由浅入深的过程发展,主要看大家门自己的具体设计和使用了,下面一些链接知识点可以让大家先了解这个概念和基础使用,本章节不在这里衍生更多websocket相关内容。
(大家如果想对websocket深入学习感兴趣 希望可以关注我后面的websocket专栏文章)

实现微信小程序编译和运行环境系列(核心篇二) 3

这个是一个比较简单的可以在线看效果的网页

如果有同学希望自己动手试试的话,我在自己的github仓库写了一个最简化的服务端和客户端的案例,一共10多行代码比较方便,有兴趣的朋友可以看下案例地址

执行index.js后效果如下:

实现微信小程序编译和运行环境系列(核心篇二) 4

下面的内容我会结合在实现这个小程序运行环境里面的对于websocket的一些运用设计和部分代码展示。

我们回到主题先在源码appservice.js的发送和接收的地方添加了一些日志保存,这里一定要彻底退出工具进程在打开不然是不起作用的。

然后我们从新进入开发者工具打开一个小程序项目,我打开的是一个官方的云开发项目列子可以看到:

实现微信小程序编译和运行环境系列(核心篇二) 5

通过这个图我们可以看出一些信息先给大家简单介绍一下:

数据发送部分

send===>{"command":"APPSERVICE_INVOKE","data":{"api":"operateWXData","args":{"data":{"api_name":"qbase_commapi","data":{"qbase_api_name":"tcbapi_init","qbase_req":"{"trace_user":true}","qbase_options":{},"qbase_meta":{"session_id":"1587696384156","sdk_version":"wx-miniprogram-sdk/2.9.5 (1578926697000)"},"cli_req_id":"1587696386661_0.5287857917854695"},"operate_directly":false},"isImportant":false,"requestInQueue":false,"apiName":"qbase_commapi","reqData":{"qbase_api_name":"tcbapi_init","qbase_req":"{"trace_user":true}","qbase_options":{},"qbase_meta":{"session_id":"1587696384156","sdk_version":"wx-miniprogram-sdk/2.9.5 (1578926697000)"},"cli_req_id":"1587696386661_0.5287857917854695"}},"callbackID":20}}

可以观察到一些字段和对象(这个是一个普通云开发项目默认打开的时候的状态,不做任何操作是个例子对象是比较复杂的)

  • command
  • data
    • api
    • args
      • data
        • api_name
        • qbase_api_name
        • qbase_req
    • callbackID

看到这个api operateWXData可能大家不是很熟悉,因为这个api微信没有对外的是内部使用的,这个不是我们现在要讲的重点,我们现在要描述的是webstocket相关的,至于api的实现会在下文如何实现小程序对外api来描述讲解,我们在这里只要知道他的消息传输格式就可以了

  • command 消息类型
  • data 各种数据组合
  • callbackID 标示这个很重要

数据接收部分

<====12receive {"command":"APPSERVICE_INVOKE_CALLBACK","data":{"callbackID":20,"res":{"errMsg":"operateWXData:ok","data":{"data":"{"baseresponse":{"errcode":0,"stat":{"qbase_cost_time":141}},"tcb_api_list":[{"apiname":"tcbapi_db_adddocument","status":1},{"apiname":"tcbapi_callfunction","status":1},{"apiname":"tcbapi_component_gettempfileurl","status":1},{"apiname":"tcbapi_db_countdocument","status":1},{"apiname":"tcbapi_db_deletedocument","status":1},{"apiname":"tcbapi_deletefile","status":1},{"apiname":"tcbapi_downloadfile","status":1},{"apiname":"tcbapi_gettempfileurl","status":1},{"apiname":"tcbapi_db_querydocument","status":1},{"apiname":"tcbapi_db_setdocument","status":1},{"apiname":"tcbapi_slowcallfunction","status":1},{"apiname":"tcbapi_slowcallfunction_v2","status":1},{"apiname":"tcbapi_traceuser","status":1},{"apiname":"tcbapi_uploadfile","status":1},{"apiname":"tcbapi_db_updatedocument","status":1},{"apiname":"tcbapi_init","status":1}],"config":{"db_doc_size_limit":524288,"upload_max_file_size":52428800,"get_temp_file_url_max_requests":50,"call_function_poll_max_retry":10,"call_function_max_req_data_size":5242880,"call_function_client_poll_timeout":15000,"call_function_valid_start_retry_gap":100000}}"}}}}

对比可以看出在上面核心篇里面讲的内容:

send===>   "command":"APPSERVICE_INVOKE" "callbackID":20
receive===>"command":"APPSERVICE_INVOKE_CALLBACK" "callbackID":20

APPSERVICE_INVOKE的消息类型是service层发送给service进行接收处理

代码实现浏览器运行环境websocket服务通信设计

这边采用node方式来启动的服务先创建一个服务端:

const ws = require('ws');
const EventEmitter = require('events');
class SocketServer extends EventEmitter {
  constructor (options) {
    super();
    this.port = options.port;
    this.wss = new ws.Server({ port: this.port });
    this.socketClientMap = new SocketClientMap();
  }

  async start () {
      this.wss.on('connection', ws => {
        this.socketClientMap.addSocketClient(ws);
        ws.on('close', () => {
          this.socketClientMap.removeSocketClient(ws.protocol);
        });

        ws.on('message', async message => {
          await this.handle(message);
        });
      });

      this.on(SEND_MSG_TO_CONTROLLER, (message) => {
        this.sendMessageToController(message);
      });

      this.on(SEND_MSG_TO_SPECIAL_WEBVIEW, ({ webviewId, message }) => {
        this.sendMessageToSpecialWebview(webviewId, message);
      });
      this.running = true;
  }}

创建客户端链接发送和接收:

const WebSocket = require('ws');
class SocketClient {
  constructor (ws) {
    this.ws = ws;
    this.msgQueue = [];
  }

  setWebSocket (ws) {
    this.ws = ws;
    this.msgQueue.forEach(msg => {
      this.ws.send(JSON.stringify(msg));
    });
    this.msgQueue = [];
  }

  removeWebSocket () {
    this.ws = null;
  }

  send (msg) {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      this.msgQueue.push(msg);
    } else {
      this.ws.send(JSON.stringify(msg));
    }
  }
}

上面两个类文件就是比较简单的服务和客户端的创建。这里创建了一个client集合类:

class SocketClientMap {
  constructor () {
    this.socketClients = new Map();
  }

  addSocketClient (ws) {
    let socketClient = this.socketClients.get(ws.protocol);
    if (!socketClient) {
      socketClient = new SocketClient(ws);
    } else {
      socketClient.setWebSocket(ws);
    }
    this.socketClients.set(ws.protocol, socketClient);
  }

  getSocketClient (protocol) {
    let socketClient = this.socketClients.get(protocol);
    if (!socketClient) {
      socketClient = new SocketClient(protocol);
      this.socketClients.set(protocol, socketClient);
    }
    return socketClient;
  }

  removeSocketClient (protocol) {
    this.socketClients.delete(protocol);
  }

  loop (cb) {
    this.socketClients.forEach((value, key) => cb(value, key));
  }
};

新添加的一个addSocketClient方法

表示如果SocketClient不存在,则根据ws创建一个新的SocketClient,否则,将旧的ws替换为新的ws,这样消息队列中的消息就可以被替换后立即发送到新的ws,保证可用性。

getSocketClient方法

调用这个函数总是可以返回一个SocketClient实例,以便用户可以在任何时候发送消息。

上文点主要关注的就是消息的格式内容组成和几个接收方和发送方的顺序,下篇我通过几个大家常用的对外api,用具体代码实现来给大家描述下具体过程。