基於 QWebChannel 的前端通訊方案

本文同步發佈於 個人博客javascript

最近筆者在工做中接觸了一些基於 Qt 桌面端混合應用的開發。本文將結合自身在開發中的一些經歷,將從前端的角度探討 QWebChannelclient 端實例化的本質,以及如何將 QWebChannel 集成到 Vue.js 等相似前端框架中。html

你首先須要可以充分理解 JS 事件循環模型執行上下文執行上下文棧,後文的 QWebChannel 集成將是以代碼執行的實質爲切入點來探討實現 QWebChannel 與前端框架的集成。本文雖以 Vue.js 爲示例,但並不限制你使用什麼前端框架,在理解其中的原理以後,讀者可嘗試使用 ReactAngular 等前端框架來實現 QWebChannel 的集成。前端

Qt 嵌入網頁技術介紹

在當前 v5.x 版本中,存在下文兩種混合應用的實現方式DOCvue

  1. Qt WebView,該模塊已在 v5.5 中被棄用,並被 Qt WebEngine 代替DOCAPI。以前主要應用在移動端,且在不包含完整的 web 瀏覽器棧的狀況下,而使用原生 API (即便用原生端的瀏覽器引擎)實如今一個 QML 應用中展現網頁的方法。筆者在開發 Qt 混合應用時,C++ 同事使用的是 v5.6.2(截至本文發佈日,最新版本爲 v5.13.1),故不對此混合應用實現作討論。java

  2. Qt WebEngine,它自己提供一個 web 引擎,用於在 Qt 應用中嵌入任意的網頁內容。這是一種 不依賴 外部 Web 引擎的混合應用的實現方式,也是最簡單的一種方式。值得注意的是 Qt WebEngine 是基於 Chromium 項目實現的,因此它並不包含一些 Google 另外在 Google Chrome 上實現的功能,讀者可在 Chromium 項目的 上游源碼庫 中找到 ChromiumGoogle Chrome 的區別。node

對於 client 中的 JS 本質上來講,Qt WebEngine 主要是提供了一個 JS 的宿主環境(runtime) —— Chromium 項目下 v8 引擎。另外在 Qt 提供的 web 渲染引擎是 Chromium 項目中的 blinkreact

Qt v5+ 中與 JS 通訊

在瞭解 Qt 爲前端提供的集成環境以後。Qt 引入了 Qt WebChannel(後文簡稱 QWebChannel) 的概念。這是爲了在不能影響各端代碼執行的前提下實現 Qt 端於 client 端的無縫 雙向 通訊。git

QWebChannel 提供了在 ServerC++應用)和 client 端(HTML/JS)之間點對點的通訊能力。經過向 client 端的 QWebChannel 發佈 QObject派生對象,進而實如今 client 端無縫讀取來自 Qt 端的 公共插槽QObject屬性值方法。在整個通訊過程當中,無需任何手動序列化傳入的參數。全部 Qt 端的 屬性 更新,signal 觸發,都會 自動且異步 更新到 client 端。github

  • QObjectQt 中對象模型的核心。該模型的核心特性是被稱爲 signalslot 的對象通訊機制。

客戶端的 QWebChannel

Qt 端實現 QWebChannel 只須要引入對應的 Qt 模塊便可。而要實現 client 端的 QWebChannel,必須引入 Qt 官方提供的 qwebchannel.jsgithubofficialJS#。該庫的目的在於封裝了一系列的 通訊接口 和傳輸信息時的序列化信息的方法。web

對於不一樣端的 Web 站點,而有不一樣的靜態文件引入方式:

  1. QWebEngine 中的本地化站點:經過 qrc:///qtwebchannel/qwebchannel.js 引入。

  2. 遠程 web 站點,則必須將官方提供的 qwebchannel.js 複製到目標 web 服務器上。

在實現通訊以前,必須實例化一個 QWebChannel 對象並傳遞一個用於傳輸功能的對象(稱爲 transport 對象)和一個回調函數。一旦 QWebChannel 完成實例化並 發佈對象 變得可用時,將調用以前實例化時提供的回調函數。在回調函數被調用時,即代表通道創建。

示例代碼以下:

import QWebChannel from './qwebchannel.js'

/** * @description window.qt.webChannelTransport 可用 WebSocket 實例代替。 * 經實踐發現,Qt 向全局注入的 window.qt 僅有屬性 webChannelTransport,而且該對象僅有 * 兩個屬性方法:send 和 onmessage * send 方法用於 js 端向 Qt 端傳輸 `JSON` 信息 * onmessage 用於接受 `Qt` 端發送的 `JSON` 信息 */
new QWebChannel(window.qt.webChannelTransport, initCallback)
複製代碼

示例代碼中 window.qt.webChannelTransport 便是 transport 對象,而 initCallback 是在 QWebChannel 完成實例化且接受到來自 Qt 端的發佈對象後纔會被調用的回調函數。在回調函數被調用時,發佈對象 必定是可用的,並且包含了全部來自 Qt 端的共享信息,如 屬性方法,可被監聽的 cpp signal 等信息。

transport 對象

在通常狀況下,transport 對象指 window.qt.webChannelTransport(由 Qt 端經過 v8 templates 注入到全局環境中) 或 WebSocket 實例

上文闡述的 transport 對象#實現了一個極簡的信息傳輸接口(interface)。它 始終 都應是一個帶有 send 方法的對象,該 send 函數(該函數的功能定位可類比於 WebSocket.send)會傳輸一個字符串化的 JSON 信息,並將它發送到 Qt 端的 QWebChannelAbstractTransport 對象。此外,當 transport 對象接受完成來自 Qt 端的信息時,應該調用 transport 對象的 onmessage 屬性。可選地,你可以使用 WebSocket 來實現該接口(即 transport 對象)。

  1. 根據 官方文檔第二段 描述,onmessage 函數被調用時,是做爲普通 宏任務 被調用,而不是被微任務源的函數包裝後調用(如被 Promise.then 包裹的回調函數)。

  2. #Note that all communication between the HTML client and the QML/C++ server is asynchronous. 全部在 clientQML/C++ 服務之間的通訊都是 異步 的。

    在官方 qwebchannel.js 中可見 56 行65 行75 行,當 transport 對象接受到來自 Qt 端的信息時,將調用 onmessage 方法,因此此方法本質是一個 消息解析器。經過此方法在 JS分發 不一樣類型的 Qt 消息,以後將調用在初始化 QWebChannel 回調中定義的回調函數。這也是 Qt 端和 JS異步通訊的本質。在每個信息發送以後,信息發送函數即退出執行上下文棧,並不會爲了等待消息響應而阻塞當前任務隊列(task queue)。

注意,一旦 transport 對象可用時,JSQWebChannel 對象就應該被實例化。若是是 WebSocket 的實現,這意味着在 socketonopen 回調中就應該建立 QWebChannel 對象。在官方的 QWebChannel 示例中,都是基於 WebSocket 實現的。後文將介紹沒有 WebSocket 如何實現 Qt 端和 client 端異步通訊。

QWebChannel 實例化回調

一旦傳遞給 QWebChannel 構造函數的回調函數被調用時,即代表 channel 完成了實例化,而且全部的來自 Qt 發佈的 發佈對象 均可經過 channel.objects 屬性被 JS 客戶端訪問。注意,全部在 JS 客戶端和 QML/C++ 服務之間的通訊都是 異步 的。屬性能夠被 JS 端緩存。此外,記住只有可被轉換爲 JSONQML/C++ 數據類型纔會被正確地序列化或反序列化,從而被 JS 客戶端訪問。

這裏在後文的源碼分析中,可得出:QWebChannel 的實例化異步回調的意義在於實現相似於 TCP 協議創建階段的 三次握手。以用於確保 Qt 端和 client 端的通訊通道是正常可用的。

interface Channel {
  objects: {
    [contextKey: string]: any
  }
}

new QWebChannel(window.qt.webChannelTransport, (channel: Channel) => {
  // 全部發佈於 Qt 的發佈對象都在 channel.objects 下
  // 值得注意的是,必須提供一個上下文名稱,將共享信息掛載到 channel.objects[上下文]
})
複製代碼

值得注意的是,在向 client 端傳輸一個 Qt 的發佈對象時,必須將與 client 端共享的全部信息掛載到一個或多個 channel.objects 的命名空間下,不能直接掛載到 channel.objects 下。即:

  • Qt 端:webchannel.cpp

    // WebBridge 類包含了一些與 JS 共享信息
    class WebBridge: public QOject
    {
        Q_OBJECT
        public slots:
        void invokedByClient()
        {
            QMessageBox::information(NULL,"invokedByClient","I'm called by client JS!");
        }
    };
    
    WebBridge *webBridge = new WebBridge();
    QWebChannel *channel = new QWebChannel(this);
    channel->registerObject('context', webBridge);
    view->page()->setWebChannel(channel);
    複製代碼
  • client 端:bridge/init.ts

    interface Channel {
      objects: {
        context: {
          [contextKey: string]: any
        }
        [namespaceKey: any]: {
          [key: string]: any
        }
      }
    }
    
    new QWebChannel(window.qt.webChannelTransport, (channel: Channel) => {
      const qtContext = channel.objects.context
      // 此時 qtContext 包含了 Qt 端 context 命名空間下全部與 client 端共享的信息
    })
    複製代碼

探究客戶端 QWebChannel 本質

依據前文闡述,QWebChannel 實例化存在一個 異步回調函數。那麼爲了 研究 在怎樣的一個 時機 來向 Vue.js 等框架中集成 QWebChannel發佈對象容器,而且避免將 QWebChannel 發佈的對象容器 channel.objects(包含全部 published objects ——來自 Qt 端的共享信息) 直接暴露在全局環境中。下文將討論 QWebChannel初始化化路徑(實例化 + 異步回調) 來探究掛載經過 QWebChannel 發佈的來自 Cpp發佈對象

JS 端初始化 QWebChannel 時,有如下邏輯來觸發 QWebChannel 的實例化:

import QWebChannel from './qwebchannel.js'

new QWebChannel(window.qt.webChannelTransport, initCallback)
複製代碼

在以上代碼中,全局環境中的 qt.webChannelTransport 對象便是前文所述的 transport 對象。該對象是由 Qt 端經過 C++ 代碼注入到 client 端的全局環境中的。通過實踐發現,該對象在 Qt v5.6.2 版本中注入時,僅僅包含如下兩個方法:

// TS types
interface MessageFromQt {
  data: {
    type: number
    [dataKey: string]: any
  }
}

declare global {
  interface Window {
    qt: {
      webChannelTransport: {
        send: (data: any) => void
        onmessage: (message: MessageFromQt) => void
      }
    }
  }
}
複製代碼

qt.webChannelTransport 注入

打印上文代碼中的 send 方法,可見函數體並不是原生 JS 語法代碼,而是 v8 templates。經探究發現,在 QtWebEngine開源代碼 中,展現了該 transport 對象 是如何注入到全局環境中的。本文爲了維持文章主題一致性,不對 C++ 代碼進行拓展解讀,若讀者感興趣,可結合 Qt 中引用的 Chromium 頭文件和 v8基本概念 以及 類型文檔 來解讀。

一句話解讀:本質上 QtWebEngine 藉助 v8 的單一實例獲取到 JS 的全局對象,而後在全局 global 對象上實現掛載 qt 對象,及其下屬 webChannelTransport

這裏 讀者能夠找到官方 Chromium 倉庫,並在 Github 上可找到 Chromium 鏡像倉庫。另外前文所述的頭文件,主要集中在 ginthird_party/blink 文件夾。

初始化時 onmessage 函數

在理解了 transport 對象的注入實質以後,transport 對象中第二個方法 onmessage 函數可經過查看 qwebchannel.js 源碼發現,是咱們在實例化 QWebChannel 時才掛載上去的SOURCE

function QWebChannel(transport, initCallback) {
  // some code is here

  var channel = this
  this.transport = transport // qt.webChannelTransport 或 一個 WebSocket 實例

  // 註冊 onmessage 函數以用於接受來自 `Qt` 端的 JSON 消息
  this.transport.onmessage = function(message) {
    var data = message.data
    if (typeof data === 'string') {
      data = JSON.parse(data)
    }
    switch (data.type) {
      case QWebChannelMessageTypes.signal:
        channel.handleSignal(data)
        break
      case QWebChannelMessageTypes.response:
        channel.handleResponse(data)
        break
      case QWebChannelMessageTypes.propertyUpdate:
        channel.handlePropertyUpdate(data)
        break
      default:
        console.error('invalid message received:', message.data)
        break
    }
  }
  // some code is here
}
複製代碼

在每一次實例化 QWebChannel 時,都會將全局環境中的 qt.webChannelTransport 掛載到 QWebChannel 實例的 transport 屬性下SOURCE。而且將實例的 send 方法與 transport 對象send 方法聯繫起來。調用實例的 send 方法 本質 上就是調用 transport 對象send 方法來向 Qt 端發送消息。而調用 transport 對象send 方法本質上是調用了以前 Qt 向全局環境中注入的 v8 template,進而實現向 Qt 發送來自 JS 的消息。

function QWebChannel(transport, initCallback) {
  // some code is here
  var channel = this
  this.transport = transport

  this.send = function(data) {
    if (typeof data !== 'string') {
      data = JSON.stringify(data)
    }
    // 便是調用 qt.webChannelTransport 或 WebSocket 實例的 send 方法
    channel.transport.send(data)
  }
  // some code is here
}
複製代碼

三次握手

qwebchannel.js 中存在如下實例函數 exec 來包裝 transport 對象send 方法,做爲向 Qt 端發送消息的途徑 之一。在消息發送以後,存儲對應的回調函數,這些回調函數都會存儲在實例的 execCallback 屬性中。

this.execCallbacks = {} // 全部的回調函數容器
this.execId = 0
this.exec = // ... 後文將對此作必要分析
複製代碼

若讀者感興趣,可深刻源碼發現,不管是監聽 C++ 的屬性仍是 signal 都須要經過此函數通知 Qt 端。

// Qt signal 處理函數
this.handleSignal = //...

// Qt 消息處理函數,如通訊初始化時的三次握手就是該函數來處理的。
this.handleResponse = // ...

// Qt 屬性更新的處理函數
this.handlePropertyUpdate = // ...
複製代碼

而後在註冊實例的 exec 方法後,後續相繼註冊了實例的 3 個用於處理來自 Qt 消息的回調函數。

QWebChannel 實例化的最後一步是實現 Qt 通訊通道的 初始化SOURCE,相似於 TCP 協議的 三次握手wiki。這一步的目的就在於確保通訊通道的可用性。

// 1. 調用前文所述的 exec 實例方法,通知 Qt 端初始化通訊通道
// 2. 設定一個回調用於接受 Qt 端的初始化通道響應
channel.exec({ type: QWebChannelMessageTypes.init }, function(data) {
  for (var objectName in data) {
    // 建立信息載體 —— client 端的 QObject
    var object = new QObject(objectName, data[objectName], channel)
  }
  // now unwrap properties, which might reference other registered objects
  for (var objectName in channel.objects) {
    channel.objects[objectName].unwrapProperties()
  }
  if (initCallback) {
    // 調用初始化的回調函數
    initCallback(channel)
  }
  // 3. 發送第三次握手信息
  channel.exec({ type: QWebChannelMessageTypes.idle })
})
複製代碼
  1. 第一次握手:在 client 端建立了一個 init 消息,併發送給 Qt 端,用於通知 Qt 端開始初始化通訊通道,並返回發佈對象(若有)。

    1. client 端的 execCallbacks 容器中,若存在響應回調函數,那麼首先註冊響應的回調函數,實現以下:

      this.exec = function(data, callback) {
        if (!callback) {
          // if no callback is given, send directly
          channel.send(data)
          return
        }
        if (channel.execId === Number.MAX_VALUE) {
          // wrap
          channel.execId = Number.MIN_VALUE
        }
        if (data.hasOwnProperty('id')) {
          console.error(
            'Cannot exec message with property id: ' + JSON.stringify(data)
          )
          return
        }
        data.id = channel.execId++
        // 在 execCallbacks 容器中註冊響應回調函數
        channel.execCallbacks[data.id] = callback
        // 根據前文分析,本質調用的是 qt.webChannelTransport.send 方法 來向 Qt 通訊
        channel.send(data)
      }
      複製代碼
    2. 以後發送 init 初始化通訊通道的消息至 Qt 端,實現 第一次握手。消息的 body 爲:

      {
        // QWebChannelMessageTypes 是源碼頂部的配置對象
        type: QWebChannelMessageTypes.init
      }
      複製代碼
  2. 第二次握手Qt 端應響應該 init 消息,若 client 端可正常接受到 Qt 端的響應消息,將執行前文所述的註冊在實例屬性 execCallbacks 容器中對應的回調函數。

    首先觸發 onmessage 函數(據 前文,全部響應均由 onmessage 處理並分發任務),以後將根據響應的類型由對應的 channel.handleResponse 處理函數來處理響應。

    this.handleResponse = function(message) {
      if (!message.hasOwnProperty('id')) {
        console.error(
          'Invalid response message received: ',
          JSON.stringify(message)
        )
        return
      }
      channel.execCallbacks[message.id](message.data)
      delete channel.execCallbacks[message.id]
    }
    複製代碼

    這裏咱們能夠看到以前在 init 消息發送以前,已在 execCallbacks 中註冊了以前的 init 消息響應的回調。在實例方法 handleResponse 中,將剝離響應中的有效載荷並傳入響應回調中完成 第二次握手。並在調用響應回調以後在容器 execCallbacks 中刪除剛剛已經完成調用並退出 執行上下文棧 的回調函數。

  3. 第三次握手:在深刻 第二次握手 的響應回調,可見SOURCE

    function(data) {
      for (const objectName in data) {
        var object = new QObject(objectName, data[objectName], channel)
      }
      // now unwrap properties, which might reference other registered objects
      for (const objectName in channel.objects) {
        channel.objects[objectName].unwrapProperties()
      }
      if (initCallback) {
        // 調用 new QWebChannel 時傳入的回調函數
        initCallback(channel)
      }
      // 第三次握手發送
      channel.exec({type: QWebChannelMessageTypes.idle})
    }
    複製代碼

    該函數執行時,首先接受來自 Qt 端的響應信息,建立 client 端的 QObject 以實現對 Cpp 端的 QObject 的追蹤。在實例化 QObject 時,進行了一系列的 method 映射,signal 監聽,property 監聽的設定。

    在存在 initCallback 時,調用 initCallback 函數。值得注意地是,這裏的 initCallback 函數便是在實例化 QWebChannel 時,傳入的第二個回調函數。此時調用 initCallback 時,Qt 端的 QObject 已經與 client 端經過 第二次握手 實現同步。

    最後,client 端向 Qt 端發出 第三次握手 請求,以用於告知 Qt 端,全部發布對象都已經在 client 端完成同步,並此時的 client 端的通訊通道進入 idle 時期——等待消息推送或消息發送。

QWebChannel 與 Vue.js 集成

以 Vue.js 插件形式集成

這裏藉助 Vue.js插件機制 實現對 QWebChannel 的優雅集成。向模塊外部暴露一個 QWebChannel 實例,並在實例化 QWebChannel 的初始化回調中將 channel.objects 註冊到 Vue 原型上,使其成爲一個 Vue原型屬性。此方法可避免官方示例中將 channel.objects 中全部的發佈自 Qt 端的信息對象泄漏到全局。

  • _utils/index.ts
import Vue from 'vue'

export const isQtClient = (function() {
  return navigator.userAgent.includes('QtWebEngine')
})()

export const bus = new Vue({})

export function assert(condition: any, msg: string) {
  // falsy is not only 'false' value.
  if (!condition)
    throw new Error(msg || `[ASSERT]: ${condition} is a falsy value.`)
}

const __DEV__ = process.env.NODE_ENV !== 'development'
複製代碼

以上代碼是在 _utils.js 中的三個工具函數。

function 描述
isQtClient 用於探測是不是 QtQWebEngine 環境。若在瀏覽器開發環境將模擬一個 qt.webChannelTransport 對象用於防止報錯。
bus 一個 Vue 實例,將用於在 Vue 原型上實現 異步掛載
assert 斷言函數

接下來在 bridge/init.ts 中創建 QWebChannel 的實例化流程:

  • bridge/init.ts
import Vue from 'vue'
import QWebChannel from './qwebchannel' // 另有 qwebchannel.d.ts 聲明文件
import { assert, isQtClient, bus, __DEV__ } from './_utils'
import dispatch from './index'

declare global {
  interface Window {
    qt: {
      webChannelTransport: {
        send: (payload: any) => void
        onmessage: (payload: any) => void
      }
    }
  }
}

export default {
  install(Vue: Vue) {
    if (!__DEV__) {
      assert(
        window && window.qt && window.qt.webChannelTransport,
        "'qt' or 'qt.webChannelTransport' should be initialized(injected) by QtWebEngine"
      )
    }

    // 用於在瀏覽器端開發時,模擬 `Qt` 的注入行爲
    if (__DEV__ && !isQtClient) {
      window.qt = {
        webChannelTransport: {
          send() {
            console.log(` QWebChannel simulator activated ! `)
          }
        }
      }
    }

    new QWebChannel(window.qt.webChannelTransport, function init(channel) {
      const qtContext = channel.objects.context

      // 官方示例直接在此,將整個 channel.objects 對象註冊到全局對象上,這裏並不推薦這樣作。

      /** * @description 這裏筆者採用的方法是註冊到 Vue 的原型對象上,實如今任意子組件中均可訪問 `Qt` 的全部發布在 context 下的發佈對象。 */
      Vue.prototype.$_bridge = qtContext

      /** * @description 此處時調用了 Cpp 的同名方法 onPageLoaded * @destination 用於通知 Qt 端 client 的 Vue.js 應用已經初始化完成 * @analysis 後文將會分析爲何此處回調可表示 Vue.js 應用已經完成初始化 */
      qtContext.onPageLoaded('', function(payload: string) {
        dispatch(payload)
        console.info(` Bridge load ! `)
      })

      // 如有需求,可繼續在此註冊 C++ signal 的監聽回調函數
      // qtContext.onSignalFromCpp.connect(() => {})
      // 以上註冊了一個回調函數用於監聽名爲 onSignalFromCpp 的 signal
    })
  }
}
複製代碼

在以上示例代碼中,主要作的事情就是:

  1. 在當前的 Qt 瀏覽器環境中實例化一個 client 端的 QWebChannel 實例用於與 Qt 端進行 異步 通訊。
  2. QWebChannel 的實例化回調中,未來自於 Qt 端全部的發佈對象註冊到 Vue 實例上,使得可在任意 Vue 實例組件中訪問 Qt 發佈的對象。

Vue.js 項目的入口文件分析

import Vue from 'vue'
import App from './App.vue'
import router from './router'

import '@/plugins/bridge' // 其中包含 bridge 異步掛載

Vue.config.productionTip = process.env.NODE_ENV === 'development'

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
複製代碼

結合事件循環分析

術語 含義
事件循環模型 HTML living standard
宏任務 HTML living standard, ECMA

經過 Vue 源碼(或任意一個 Vue 應用的 火焰圖(flame chart))可見,在初始實例化 Vue 時(不含數據更新 —— Vue.js 的數據更新 異步 更新的),是 同步 實例化。那麼結合 JS 事件循環模型 僅當 src/main.ts 文件(截圖 1 處)徹底執行完畢,並退出 執行上下文棧 時,纔會執行下一個 宏任務HTML living standard, ECMA。此時,onmessage 回調纔有可能成爲下一個待進入 執行上下文棧宏任務

以上通俗點來講,就是基於 Vue 的實例化(不含數據更新)是 同步宏任務 這一本質,QWebChannel 實例化回調函數 initCallback 必定 是在 Vue 實例化以後纔會被執行的。下面火焰圖的 10 處可清晰可見 Vue同步 初始化流程。

那麼由於 ./src/main.ts 入口文件自己是一個 模塊,那麼在執行該模塊是,Webpack 將其包裝爲一個 函數,那麼就會建立一個執行上下文。基於 Execution context 模型ECMA,也就等價於在 ./src/main.ts 中代碼沒有執行徹底,並退出 執行上下文棧 時,後續的 宏任務(task) 始終都只會處於 宏任務隊列 (task queue) 中,而不會被推入執行上下文棧中。以上即解釋了爲何實例化 QWebChannel 時傳入的回調函數 必定 是在 Vue 初始化 以後 被調用。

bridge-flame-chart

在結合以上的全部分析後,不可貴出:

  1. initCallback 始終是在 new Vue 以後被調用。
  1. 基於 JS事件循環模型,在 initCallback 被調用時,routervue 功能據前文闡述必定是可用的。
  1. 結合 1,至少不能早於 Vue 實例化完成,而且 initCallback 被調用前(即三次握手 的第二次握手完成前),觸發 signalQt 通訊。

FAQ

  • 爲何在混合應用中不使用 URL 進行通訊?

    1. 儘量下降 C++ 端與前端的耦合度,避免手動序列化參數,拼接字符串。當出現嵌套的參數對象時,JSON.stringify 的複雜度明顯低於手寫序列化函數的複雜度。

    2. URL 長度有限制,在超出 URL 的長度限制後,後續的傳參將被丟棄。同時這也是爲何不宜在 HTTP GET 請求時攜帶過多參數的緣由。

References

Qt mirrors

相關文章
相關標籤/搜索