實現微信小程序編譯和運行環境系列(核心篇二)

在上文中我們有點到小程序開發者工具裏面的消息是通過websocket協議發送和接受處理的,
當然這個不是憑空而說的,是在小程序的邏輯層appservice.js源碼裏面有代碼表明的,至於它的消息格式還有一部分我沒有列出來,比如它的數據分析和上報他們自己服務器的一些消息格式可以先先需要關注。

下面還是先給大家展示一下流程找到appservice.js源碼文件

可以看到它的鏈接地址,數據發送和接收的部分代碼,由於圖片尺寸問題我摺疊了部分代碼,大家可以自己去細看看

我還是先簡述一些webstocket的知識,可能部分同學對這方面不是很熟悉。細節webstocket內容不會在本文描述,後期會寫一篇專門的介紹

websocket是什麼

其實這些內容我們通過谷歌搜索可以查閱很多材料,但有沒有真正理解可以在自己項目裏進行靈活設計運用還是隻是簡單使用文檔api
還是要靠自己多探索思考一些。

我們通過資料webstockrt協議

可以理解爲:WebSocket協議允許在運行於受控環境中的不受信任代碼的用戶代理與已選擇從該代碼進行通信的遠程主機之間進行雙向通信
簡單點描述就是:客戶端和服務器之間存在持久連接,而且雙方都可以隨時隨地相互發送數據

爲什麼用websocket

一項新規範或者一門新技術的誕生肯定是爲了解決或者完善前面方案的不足,這樣才能一直進步下去。
在沒有websocket之前我們採用http用的很好,但是隨着一些應用的要求像聊天 股票 遊戲 這種對實時性數據要求高的系統,
才用HTTP 協議發送數據的話只能有客戶端單方面進行請求,服務端響應獲取最新數據,如果服務端的數據變換很快比如股票的信息,
因此只能定時去請求,就出出現效率低 浪費資源 而且數據還不實時同步的情況,爲了解決這些問題通過研究websocket協議就閃亮登場了

websocket具備的一些優點

  • 支持雙向通信,具有很強的實時性
  • 對二進制的支持比較友好
  • 相比與http協議的控制開銷要少很多
  • 用戶可以自由的擴展協議,自定義子協議例如(wss)

如何使用websocket

這個點比較廣泛一個新方案新技術的產生都會經過由淺入深的過程發展,主要看大家門自己的具體設計和使用了
下面一些鏈接知識點可以讓大家先了解這個概念和基礎使用,本章節不在這裏衍生更多websocket相關內容
(大家如果想對websocket深入學習感興趣 希望可以關注我後面的websocket專欄文章)

webStocket Api

MDN

如果想在線測試的話可以試下這個websocket demo

這個是一個比較簡單的可以在線看效果的網頁

如果有同學希望自己動手試試的話,我在自己的github倉庫寫了一個最簡化的服務端和客戶端的案例
一共10多行代碼比較方便,有興趣的朋友可以看下案例地址

執行index.js後效果如下

下面的內容我會結合在實現這個小程序運行環境裏面的對於websocket的一些運用設計和部分代碼展示

我們回到主題先在源碼appservice.js的發送和接收的地方添加了一些日誌保存,這裏一定要徹底退出工具進程在打開不然是不起作用的。
然後我們從新進入開發者工具打開一個小程序項目,我打開的是一個官方的雲開發項目列子可以看到

通過這個圖我們可以看出一些信息先給大家簡單介紹一下
數據發送部分

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,用具體代碼實現來給大家描述下具體過程

原文地址github感覺有用的話點個star支持一下

歡迎持續關注

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章