QML Book 第十一章 網(wǎng)絡(luò) 1

11.網(wǎng)絡(luò)(Networking

本章的作者:jryannel

注意:
最新的構(gòu)建時(shí)間:2016/03/21
這章的源代碼能夠在assetts folder找到。

Qt 5 在其 C++ 部分提供了豐富的網(wǎng)絡(luò)接口。例如 http 協(xié)議層上的高級類,例如提供了 QNetworkRequest、QNetworkReply 和 QNetworkAccessManager 等請求回復(fù)方式的上層便利類。但也在 TCP/IP 或 UDP協(xié)議層(如QTcpSocket,QTcpServer和QUdpSocket)上提供了較低級別的類。另外,也存在用于管理代理,網(wǎng)絡(luò)緩存以及系統(tǒng)網(wǎng)絡(luò)配置的其他類。

本章不會講解關(guān)于 C++ 部分的網(wǎng)絡(luò)知識,本章是關(guān)于 Qt Quick 和網(wǎng)絡(luò)的。那么如何將 QML/JS 用戶界面直接連接到網(wǎng)絡(luò)服務(wù),或者如何通過網(wǎng)絡(luò)服務(wù)來為我的用戶界面提供服務(wù)。有很好的書籍和參考資料講解 Qt/C++ 的網(wǎng)絡(luò)部分。那么這只是一個閱讀有關(guān) C++ 集成的章節(jié),以提供一個集成層來將我們的數(shù)據(jù)提供給 Qt Quick 部分。

11.1 通過 HTTP 為用戶界面提供服務(wù)

要通過 HTTP 加載一個簡單的用戶界面,我們需要一個 web 服務(wù)器,它為 UI 文檔提供服務(wù)。我們開始使用我們自己的簡單的 web 服務(wù)器,使用一個 python 單線程。但首先我們需要有我們的演示用戶界面。為此,我們在項(xiàng)目文件夾中創(chuàng)建一個小的 main.qml 文件,并在其中創(chuàng)建一個紅色矩形。

// main.qml
import QtQuick 2.5

Rectangle {
    width: 320
    height: 320
    color: '#ff0000'
}

為了提供這個文件,我們推出了一個小的 python 腳本:

$ cd <PROJECT>
# python -m SimpleHTTPServer 8080

現(xiàn)在我們的文件應(yīng)該通過 http://localhost:8080/main.qml 可以訪問。我們可以通過以下方式測試:

curl http://localhost:8080/main.qml

或者將瀏覽器指向位置。我們的瀏覽器不了解 QML,無法通過文檔進(jìn)行呈現(xiàn)。我們需要為 QML 文檔創(chuàng)建一個能夠解析 QML 的瀏覽器。為了呈現(xiàn)文檔,我們需要指出我們的 qmlscene 的位置。不幸的是,qmlscene 僅限于解析本地文件。我們可以通過編寫我們自己的 qmlscene 替換原有的 qmlscene 來克服這個限制,或者使用 QML 動態(tài)加載它。我們選擇動態(tài)加載,因?yàn)樗ぷ髡?。為此,我們使用一個加載器元素為我們檢索遠(yuǎn)程文檔。

// remote.qml
import QtQuick 2.5

Loader {
    id: root
    source: 'http://localhost:8080/main2.qml'
    onLoaded: {
        root.width = item.width
        root.height = item.height
    }
}

現(xiàn)在我們可以要求 qmlscene 加載本地的 remote.qml 從而實(shí)現(xiàn)加載遠(yuǎn)程文件。還有一個問題 —— 加載程序?qū)⒄{(diào)整到加載項(xiàng)目的大小。而我們的 qmlscene 也需要適應(yīng)這種尺寸。這可以使用 qmlscene 的 --resize-to-root 選項(xiàng)來實(shí)現(xiàn):

$ qmlscene --resize-to-root remote.qml

調(diào)整到根的大小告訴 qml 場景將其窗口的大小調(diào)整為根元素的大小。遠(yuǎn)程目前正在從本地服務(wù)器加載 main.qml,并將其自身調(diào)整為加載的用戶界面。這很優(yōu)雅和簡單。

注意:
如果我們不想運(yùn)行本地服務(wù)器,還可以使用 GitHub 的 gist 服務(wù)。Gist 是像 PasteBin 和其他的在線服務(wù)的剪貼板。它可以在 https://gist.github.com 下找到。 我(原作者)為這個例子創(chuàng)建了 https://gist.github.com/jryannel/7983492 下的一個小小的要點(diǎn)。這將顯示一個綠色矩形。由于主要網(wǎng)址將網(wǎng)站提供為 HTML 代碼,我們需要將 /raw 附加到網(wǎng)址以檢索原始文件而不是 HTML 代碼。

// remote.qml
import QtQuick 2.5

Loader {
    id: root
    source: 'https://gist.github.com/jryannel/7983492/raw'
    onLoaded: {
        root.width = item.width
        root.height = item.height
    }
}

要通過網(wǎng)絡(luò)加載另一個文件,我們只需要引用組件名稱。例如,Button.qml 可以正常訪問,只要它在同一個遠(yuǎn)程文件夾中。

11.1.1 網(wǎng)絡(luò)組件

讓我們創(chuàng)建一個小實(shí)驗(yàn)。我們添加到我們的遠(yuǎn)程端一個小按鈕作為可重復(fù)使用的組件。

- src/main.qml
- src/Button.qml

我們修改我們的 main.qml 來使用該按鈕并保存為 main2.qml:

import QtQuick 2.5

Rectangle {
    width: 320
    height: 320
    color: '#ff0000'

    Button {
        anchors.centerIn: parent
        text: 'Click Me'
        onClicked: Qt.quit()
    }
}

再次啟動我們的網(wǎng)絡(luò)服務(wù)器:

$ cd src
# python -m SimpleHTTPServer 8080

我們的遠(yuǎn)程加載程序通過 http 重新加載主要的 QML:

$ qmlscene --resize-to-root remote.qml

我們看到的是一個錯誤:

http://localhost:8080/main2.qml:11:5: Button is not a type

所以 QML 在遠(yuǎn)程加載時(shí)無法解析按鈕組件。如果代碼將在本地 qmlscene src/main.qml 這將是沒有問題的。本地 Qt 可以解析目錄并檢測哪些組件可用,但遠(yuǎn)程地,http 沒有 “l(fā)ist-dir” 功能。我們可以強(qiáng)制 QML 使用 main.qml 中的 import 語句加載元素:

import "http://localhost:8080" as Remote

...

Remote.Button { ... }

當(dāng) qmlscene 再次運(yùn)行時(shí),這將可以正常工作:

$ qmlscene --resize-to-root remote.qml

這里完整的代碼:

// main2.qml
import QtQuick 2.5
import "http://localhost:8080" 1.0 as Remote

Rectangle {
    width: 320
    height: 320
    color: '#ff0000'

    Remote.Button {
        anchors.centerIn: parent
        text: 'Click Me'
        onClicked: Qt.quit()
    }
}

更好的選擇是使用服務(wù)器端的 qmldir 文件來控制導(dǎo)出。

// qmldir
Button 1.0 Button.qml

然后更新 main.qml:

import "http://localhost:8080" 1.0 as Remote

...

Remote.Button { ... }

注意:
當(dāng)使用本地文件系統(tǒng)中的組件時(shí),將立即創(chuàng)建它們,而不會有延遲。當(dāng)通過網(wǎng)絡(luò)加載組件時(shí),它們將異步創(chuàng)建。這具有這樣的問題:創(chuàng)建的時(shí)間是未知的,并且當(dāng)其他元素已經(jīng)加載完成時(shí)有些元素可能尚未被完全加載。在使用通過網(wǎng)絡(luò)加載的組件時(shí)需要考慮到這一點(diǎn)。

11.2 模板

當(dāng)使用 HTML 項(xiàng)目時(shí),通常需要使用模板驅(qū)動開發(fā)。服務(wù)器使用模板機(jī)制生成代碼在服務(wù)器端對一個 HTML 根進(jìn)行擴(kuò)展。例如一個照片列表的列表頭將使用 HTML 編碼,動態(tài)圖片鏈表將會使用模板機(jī)制動態(tài)生成。一般來說,這也可以使用 QML 來完成,但有一些問題。

首先它是沒有必要的。HTML 開發(fā)人員這樣做的原因是克服對 HTML 后端的限制。在 HTML 中沒有組件模型,因此動態(tài)方面必須使用這些機(jī)制來替代,或者在客戶端使用程序化的 JavaScript。許多 JS 框架(jQuery、dojo、backbone、angular、...)都用來解決這個問題,并將更多的邏輯放在客戶端瀏覽器中以與網(wǎng)絡(luò)服務(wù)連接。然后,客戶端將僅使用 Web 服務(wù) API(例如,提供 JSON 或 XML 數(shù)據(jù))來與服務(wù)器進(jìn)行通信。這似乎也是 QML 更好的方法。

第二個問題是 QML 的組件緩存。當(dāng) QML 訪問組件時(shí),它將緩存渲染樹,并加載緩存版本進(jìn)行渲染。在重新啟動客戶端之前,將無法檢測到磁盤或遠(yuǎn)程的修改版本。為了克服這個問題,我們可以使用一個技巧。我們可以使用 URL 片段來加載網(wǎng)址(例如 http://localhost:8080/main.qml#1234),其中 '#1234' 是片段。HTTP 服務(wù)器始終保持相同的文檔,但 QML 將使用完整的 URL(包括片段)存儲此文檔。每次我們訪問此 URL 時(shí),片段都需要更改,并且 QML 緩存不會得到這個信息。片段可以是例如當(dāng)前時(shí)間(毫秒)或隨機(jī)數(shù)。

Loader {
    source: 'http://localhost:8080/main.qml#' + new Date().getTime()
}

總而言之,模板是可能的,但不是很推薦的,并沒有發(fā)揮 QML 的優(yōu)勢。更好的方法是使用提供 JSON 或 XML 數(shù)據(jù)的 Web 服務(wù)器。

11.3 HTTP 請求

Qt 中的 http 請求通常使用 QNetworkRequest 和 QNetworkReply 從 C++ 代碼中完成,然后響應(yīng)將使用 Qt/C++ 集成推送數(shù)據(jù)到 QML 代碼中。所以我們試圖把這個信封放在這里,使用 Qt Quick 提供的當(dāng)前工具讓我們與一個網(wǎng)絡(luò)端點(diǎn)進(jìn)行通信。為此,我們使用一個幫助對象來發(fā)出 http 請求,響應(yīng)周期。它以 java 腳本 XMLHttpRequest 對象的形式出現(xiàn)。

XMLHttpRequest 對象允許用戶注冊一個響應(yīng)句柄函數(shù)和一個 url??梢允褂?http 動詞之一(get,post,put,delete,...)發(fā)送請求。當(dāng)響應(yīng)到達(dá)時(shí),調(diào)用 handle 函數(shù)。句柄函數(shù)被調(diào)用多次。每次請求狀態(tài)已更改(例如標(biāo)題已到達(dá)或請求完成)。

這里有一個簡短的例子:

function request() {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
            print('HEADERS_RECEIVED');
        } else if(xhr.readyState === XMLHttpRequest.DONE) {
            print('DONE');
        }
    }
    xhr.open("GET", "http://example.com");
    xhr.send();
}

對于響應(yīng),我們可以獲取 XML 格式或只是原始文本??梢詫Y(jié)果 XML 進(jìn)行迭代,但更常用的是 JSON 格式響應(yīng)的原始文本。JSON 文檔將用于使用 JSON.parse(text) 將文本轉(zhuǎn)換為 JS 對象。

...
} else if(xhr.readyState === XMLHttpRequest.DONE) {
    var object = JSON.parse(xhr.responseText.toString());
    print(JSON.stringify(object, null, 2));
}

在響應(yīng)處理程序中,我們訪問原始響應(yīng)文本并將其轉(zhuǎn)換為 JavaScript 對象。這個 JSON 對象現(xiàn)在是一個有效的 JS 對象(在javascript中,對象可以是對象或數(shù)組)。

注意:
似乎優(yōu)先使用 toString() 轉(zhuǎn)換使代碼更加穩(wěn)定。沒有進(jìn)行明確的轉(zhuǎn)換,我有幾次解析器錯誤。不知道是什么原因。

11.3.1 Flickr 調(diào)用

讓我們來看看一個更真實(shí)的世界的例子。一個典型的例子是使用 Flickr 服務(wù)來檢索新上傳圖片的公共 Feed。為此,我們可以使用 http://api.flickr.com/services/feeds/photos_public.gne 網(wǎng)址。不幸的是,它默認(rèn)返回一個 XML 流,這可以很容易地被 qml 中的 XmlListModel 解析。為了實(shí)例,我們想集中注意力在 JSON 數(shù)據(jù)上。為了獲得一個干凈的 JSON 響應(yīng),我們需要為請求附加一些參數(shù):http://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1。這將返回沒有 JSON 回調(diào)的 JSON 響應(yīng)。

注意:
JSON 回調(diào)將 JSON 響應(yīng)包裝到函數(shù)調(diào)用中。這是用于 HTML 編程的快捷方式,其中使用腳本標(biāo)記來生成 JSON 請求。響應(yīng)將觸發(fā)由回調(diào)定義的本地函數(shù)。在 QML 中沒有使用 JSON 回調(diào)的機(jī)制。

讓我們先來看看使用 curl 的回應(yīng):

curl "http://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=munich"

響應(yīng)將是類似下面這樣的:

{
    "title": "Recent Uploads tagged munich",
    ...
    "items": [
        {
        "title": "Candle lit dinner in Munich",
        "media": {"m":"http://farm8.staticflickr.com/7313/11444882743_2f5f87169f_m.jpg"},
        ...
        },{
        "title": "Munich after sunset: a train full of \"must haves\" =",
        "media": {"m":"http://farm8.staticflickr.com/7394/11443414206_a462c80e83_m.jpg"},
        ...
        }
    ]
    ...
}

返回的 JSON 文檔具有定義好的結(jié)構(gòu)。具有標(biāo)題和項(xiàng)目屬性的對象。標(biāo)題是字符串,而項(xiàng)目是一組對象。將此文本轉(zhuǎn)換為 JSON 文檔時(shí),我們可以訪問各個條目,因?yàn)樗怯行У?JS 對象/數(shù)組結(jié)構(gòu)。

// JS code
obj = JSON.parse(response);
print(obj.title) // => "Recent Uploads tagged munich"
for(var i=0; i<obj.items.length; i++) {
    // iterate of the items array entries
    print(obj.items[i].title) // title of picture
    print(obj.items[i].media.m) // url of thumbnail
}

作為有效的 JS 數(shù)組,我們可以使用 obj.items 數(shù)組作為列表視圖的模型。我們將盡力實(shí)現(xiàn)這一點(diǎn)。首先,我們需要檢索響應(yīng)并將其轉(zhuǎn)換為有效的 JS 對象。 然后我們可以將 response.items 屬性設(shè)置為列表視圖的模型。

function request() {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if(...) {
            ...
        } else if(xhr.readyState === XMLHttpRequest.DONE) {
            var response = JSON.parse(xhr.responseText.toString());
            // set JS object as model for listview
            view.model = response.items;
        }
    }
    xhr.open("GET", "http://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=munich");
    xhr.send();
}

這是完整的源代碼,我們創(chuàng)建請求時(shí),加載組件。然后,請求響應(yīng)用作我們的簡單列表視圖的模型。

import QtQuick 2.5

Rectangle {
    width: 320
    height: 480
    ListView {
        id: view
        anchors.fill: parent
        delegate: Thumbnail {
            width: view.width
            text: modelData.title
            iconSource: modelData.media.m
        }
    }

    function request() {
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
                print('HEADERS_RECEIVED')
            } else if(xhr.readyState === XMLHttpRequest.DONE) {
                print('DONE')
                var json = JSON.parse(xhr.responseText.toString())
                view.model = json.items
            }
        }
        xhr.open("GET", "http://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=munich");
        xhr.send();
    }

    Component.onCompleted: {
        request()
    }
}

當(dāng)文檔完全加載(Component.onCompleted)時(shí),我們從 Flickr 請求最新的 Feed 內(nèi)容。在到達(dá)時(shí),我們解析 JSON 響應(yīng),并將 items 數(shù)組設(shè)置為我們視圖的模型。列表視圖具有一個代理,它在一行中顯示縮略圖圖標(biāo)和標(biāo)題文本。

另一個選擇是擁有占位符 ListModel 并將每個項(xiàng)目附加到列表模型上。為了支持更大的模型,需要支持分頁(例如第1頁,共10頁)和懶惰內(nèi)容檢索(lazy content retrieval)。

11.4 本地文件

也可以使用 XMLHttpRequest 加載本地(XML / JSON)文件。例如,可以使用以下命令加載名為 “colors.json” 的本地文件:

xhr.open("GET", "colors.json");

我們使用它來讀取顏色表并將其顯示為網(wǎng)格。不能從 Qt Quick 側(cè)修改文件。要將數(shù)據(jù)存儲回源,我們需要一個基于 REST 的小型 HTTP 服務(wù)器或本地 Qt Quick 擴(kuò)展來進(jìn)行文件訪問。

import QtQuick 2.5

Rectangle {
    width: 360
    height: 360
    color: '#000'

    GridView {
        id: view
        anchors.fill: parent
        cellWidth: width/4
        cellHeight: cellWidth
        delegate: Rectangle {
            width: view.cellWidth
            height: view.cellHeight
            color: modelData.value
        }
    }

    function request() {
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
                print('HEADERS_RECEIVED')
            } else if(xhr.readyState === XMLHttpRequest.DONE) {
                print('DONE');
                var obj = JSON.parse(xhr.responseText.toString());
                view.model = obj.colors
            }
        }
        xhr.open("GET", "colors.json");
        xhr.send();
    }

    Component.onCompleted: {
        request()
    }
}

不使用 XMLHttpRequest 也可以使用 XmlListModel 來訪問本地文件的。

import QtQuick.XmlListModel 2.0

XmlListModel {
    source: "http://localhost:8080/colors.xml"
    query: "/colors"
    XmlRole { name: 'color'; query: 'name/string()' }
    XmlRole { name: 'value'; query: 'value/string()' }
}

使用 XmlListModel,只能讀取 XML 文件而不是 JSON 文件。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,538評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,939評論 25 709
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,186評論 4 61
  • 竹子,用了五年的時(shí)間潛伏在地底,第六年借著春風(fēng)和雨水開始瘋長,直至長成參天的模樣才開始罷休。 而銀杏的壽命極長,到...
    魚子仙人閱讀 1,565評論 2 2
  • 待定
    曉冰閱讀 143評論 0 1

友情鏈接更多精彩內(nèi)容