QML 中的 XMLHttpRequest 没有同源限制,并且可以读写本地文件。
代码如下:
function saveText(filename, contentText) { var xhr = new XMLHttpRequest; xhr.onreadystatechange = function() { if (xhr.readyState == XMLHttpRequest.HEADERS_RECEIVED) { console.log(xhr.getAllResponseHeaders()); } else if (xhr.readyState == XMLHttpRequest.DONE) { console.log(xhr.getAllResponseHeaders()); } }; xhr.open("PUT", filename); xhr.send(contentText.toString()); }但是 QML 中只实现了 XMLHttpRequest Level 1 标准。
如下源码来自 Qt 的 qml 模块,此函数的功能就是过滤 setRequestHeader 设置的请求头。
其中 COOKIE,COOKIE2 等不能被设置,更多被过滤的请求头,查看下面的源码。
ReturnedValue QQmlXMLHttpRequestCtor::method_setRequestHeader(CallContext *ctx) { Scope scope(ctx); Scoped<QQmlXMLHttpRequestWrapper> w(scope, ctx->thisObject().as<QQmlXMLHttpRequestWrapper>()); if (!w) V4THROW_REFERENCE("Not an XMLHttpRequest object"); QQmlXMLHttpRequest *r = w->d()->request; if (ctx->argc() != 2) V4THROW_DOM(DOMEXCEPTION_SYNTAX_ERR, "Incorrect argument count"); if (r->readyState() != QQmlXMLHttpRequest::Opened || r->sendFlag()) V4THROW_DOM(DOMEXCEPTION_INVALID_STATE_ERR, "Invalid state"); QString name = ctx->args()[0].toQStringNoThrow(); QString value = ctx->args()[1].toQStringNoThrow(); // ### Check that name and value are well formed QString nameUpper = name.toUpper(); if (nameUpper == QLatin1String("ACCEPT-CHARSET") || nameUpper == QLatin1String("ACCEPT-ENCODING") || nameUpper == QLatin1String("CONNECTION") || nameUpper == QLatin1String("CONTENT-LENGTH") || nameUpper == QLatin1String("COOKIE") || nameUpper == QLatin1String("COOKIE2") || nameUpper == QLatin1String("CONTENT-TRANSFER-ENCODING") || nameUpper == QLatin1String("DATE") || nameUpper == QLatin1String("EXPECT") || nameUpper == QLatin1String("HOST") || nameUpper == QLatin1String("KEEP-ALIVE") || nameUpper == QLatin1String("REFERER") || nameUpper == QLatin1String("TE") || nameUpper == QLatin1String("TRAILER") || nameUpper == QLatin1String("TRANSFER-ENCODING") || nameUpper == QLatin1String("UPGRADE") || nameUpper == QLatin1String("USER-AGENT") || nameUpper == QLatin1String("VIA") || nameUpper.startsWith(QLatin1String("PROXY-")) || nameUpper.startsWith(QLatin1String("SEC-"))) return Encode::undefined(); r->addHeader(name, value); return Encode::undefined(); }如有需求,可参照 XMLHttpRequest 接口可以设计一个支持更多必要功能的 C++ 类。
W3C XMLHttpRequest 标准。
XMLHttpRequest 的 W3C 接口描述如下:
[NoInterfaceObject] interface XMLHttpRequestEventTarget : EventTarget { // event handlers attribute EventHandler onloadstart; attribute EventHandler onprogress; attribute EventHandler onabort; attribute EventHandler onerror; attribute EventHandler onload; attribute EventHandler ontimeout; attribute EventHandler onloadend; }; interface XMLHttpRequestUpload : XMLHttpRequestEventTarget { }; enum XMLHttpRequestResponseType { "", "arraybuffer", "blob", "document", "json", "text" }; [Constructor] interface XMLHttpRequest : XMLHttpRequestEventTarget { // event handler attribute EventHandler onreadystatechange; // states const unsigned short UNSENT = 0; const unsigned short OPENED = 1; const unsigned short HEADERS_RECEIVED = 2; const unsigned short LOADING = 3; const unsigned short DONE = 4; readonly attribute unsigned short readyState; // request void open(ByteString method, [EnsureUTF16] DOMString url); void open(ByteString method, [EnsureUTF16] DOMString url, boolean async, optional [EnsureUTF16] DOMString? username = null, optional [EnsureUTF16] DOMString? password = null); void setRequestHeader(ByteString header, ByteString value); attribute unsigned long timeout; attribute boolean withCredentials; readonly attribute XMLHttpRequestUpload upload; void send(optional (ArrayBufferView or Blob or Document or [EnsureUTF16] DOMString or FormData)? data = null); void abort(); // response readonly attribute unsigned short status; readonly attribute ByteString statusText; ByteString? getResponseHeader(ByteString header); ByteString getAllResponseHeaders(); void overrideMimeType(DOMString mime); attribute XMLHttpRequestResponseType responseType; readonly attribute any response; readonly attribute DOMString responseText; readonly attribute Document? responseXML; };已完成 qyvlik/HttpRequest。
仿制 node-fetch 的接口, Qt 5.13.2
需要使用到 Qt 的一个特性:Promise,从 Qt 5.12 开始支持 New_Features_in_Qt_5.12
// file is qml-fetch.js function createResponse(xhr) { let res = {}; function headersParser() { let headersRaw = {}; let lowerCaseHeaders = {}; let rawHeaderArray = xhr.getAllResponseHeaders().split("\n"); for(let i in rawHeaderArray) { let rawHeader = rawHeaderArray[i]; let headerItem = rawHeader.split(":"); let name = headerItem[0].trim(); let value = headerItem[1].trim(); let lowerName = name.toLowerCase(); headersRaw[name] = value; lowerCaseHeaders [lowerName] = value; } return { "headersRaw": headersRaw, "lowerCaseHeaders": lowerCaseHeaders }; } res.headers = { __alreayParse : false, raw: function() { if (!res.headers.__alreayParse) { let {headersRaw, lowerCaseHeaders} = headersParser(); res.headers.__alreayParse = true; res.headers.__headersRaw = headersRaw; res.headers.__lowerCaseHeaders = lowerCaseHeaders; } return res.headers.__headersRaw; }, get: function(headerName) { if (!res.headers.__alreayParse) { let {headersRaw, lowerCaseHeaders} = headersParser(); res.headers.__alreayParse = true; res.headers.__headersRaw = headersRaw; res.headers.__lowerCaseHeaders = lowerCaseHeaders; } return res.headers.__lowerCaseHeaders[headerName.toLowerCase()]; } }; res.json = function() { if(res.__json) { return res.__json; } return res.__json = JSON.parse(xhr.responseText); } res.text = function() { if (res.__text) { return res.__text; } return res.__text = xhr.responseText; } res.arrayBuffer = function() { if (res.__arrayBuffer) { return res.__arrayBuffer; } return res.__arrayBuffer = new Uint8Array(xhr.response); } res.ok = (xhr.status >= 200 && xhr.status < 300); res.status = xhr.status; res.statusText = xhr.statusText; return res; } function fetch(url, options) { return new Promise(function(resolve, reject) { let requestUrl = ""; let method = ""; let headers = {}; let body; let timeout; if (typeof url === 'string') { requestUrl = url; method = "GET"; if (options) { requestUrl = options['url']; method = options['method']; headers = options['headers']; body = options['body']; timeout = options['timeout']; } } else { let optionsObj = url; requestUrl = optionsObj['url']; method = optionsObj['method']; headers = optionsObj['headers']; body = optionsObj['body']; timeout = optionsObj['timeout']; } let closureData = { 'abort': false }; let xhr = new XMLHttpRequest; // https://github.com/qt/qtdeclarative/blob/v5.13.2/src/qml/qml/qqmlxmlhttprequest.cpp#L1560 // when call xhr.abort, methods will call as follow // 1. in cpp, call destroyNetwork (qml xmlhttprequest abort not call QNetworkReply::abort) // 2. onreadystatechange, the xhr.readyState will be set as DONE // 3. onerror if (timeout && fetch.setTimeout && fetch.clearTimeout) { let timeId = fetch.setTimeout(()=>{ fetch.clearTimeout(timeId); if (xhr.readyState !== XMLHttpRequest.DONE && !closureData.abort) { closureData.abort = true; xhr.abort(); } }, timeout * 1000); } // must set responseType to arraybuffer, then the xhr.response type will be ArrayBuffer // but responseType not effect the responseText // https://github.com/qt/qtdeclarative/blob/v5.13.2/src/qml/qml/qqmlxmlhttprequest.cpp#L2014 // responseType value as follow: `text`, `arraybuffer`, `json`, `document` xhr.responseType = 'arraybuffer'; // https://github.com/qt/qtdeclarative/blob/v5.13.2/src/qml/qml/qqmlxmlhttprequest.cpp#L1582 // callback value as follow: `onreadystatechange`, `onerror`, `onload`, `onloadend` xhr.onreadystatechange = function() { // readyState as follow: UNSENT, OPENED, HEADERS_RECEIVED, LOADING, DONE if(!closureData.abort && xhr.readyState === XMLHttpRequest.DONE) { try { resolve(createResponse(xhr)); } catch(error) { reject(error); } } }; xhr.onerror = function() { if(xhr.readyState === XMLHttpRequest.DONE) { let error = { "message": closureData.abort ? "XMLHttpRequest abort": "XMLHttpRequest statusText:" + xhr.statusText, "res": createResponse(xhr) } reject(error); } } xhr.open(method, requestUrl); // if (nameUpper == QLatin1String("ACCEPT-CHARSET") || // nameUpper == QLatin1String("ACCEPT-ENCODING") || // nameUpper == QLatin1String("CONNECTION") || // nameUpper == QLatin1String("CONTENT-LENGTH") || // nameUpper == QLatin1String("COOKIE") || // nameUpper == QLatin1String("COOKIE2") || // nameUpper == QLatin1String("CONTENT-TRANSFER-ENCODING") || // nameUpper == QLatin1String("DATE") || // nameUpper == QLatin1String("EXPECT") || // nameUpper == QLatin1String("HOST") || // nameUpper == QLatin1String("KEEP-ALIVE") || // nameUpper == QLatin1String("REFERER") || // nameUpper == QLatin1String("TE") || // nameUpper == QLatin1String("TRAILER") || // nameUpper == QLatin1String("TRANSFER-ENCODING") || // nameUpper == QLatin1String("UPGRADE") || // nameUpper == QLatin1String("USER-AGENT") || // nameUpper == QLatin1String("VIA") || // nameUpper.startsWith(QLatin1String("PROXY-")) || // nameUpper.startsWith(QLatin1String("SEC-"))) // RETURN_UNDEFINED(); for(let iter in headers) { xhr.setRequestHeader(iter, headers[iter]); } if("GET" === method || "HEAD" === method) { xhr.send(); } else { xhr.send(body); } }); }qml xmlhttprequest download and handle binary file
使用上诉的 qml-fetch 代码。设置 xhr.responseType 为 arraybuffer, xhr.response 返回的是 arraybuffer。
如下的代码,其中 hexdump 函数是将二进制内容转换成 hex 格式的文本,进行输出,可以配合 linux 的 hexdump 命令进行使用,验证 qml 中的二进制文件内容和实际的文件内容是否相匹配。
import Qt.Quick 2.13 import "qml-fetch.js" as QmlFetch Item { function hexdump(uint8array) { let count = 0; let line = ""; let lineCount = 0; let content = ""; for(let i=0; i<uint8array.byteLength; i++) { let c = uint8array[i]; let hex = c.toString(16).padStart (2, "0"); line += hex + " "; count++; if (count === 16) { let lineCountHex = (lineCount).toString (16).padStart (7, "0") + "0"; content += lineCountHex + " " + line + "\n"; line = ""; count = 0; lineCount++; } } if(line) { let lineCountHex = (lineCount).toString (16).padStart (7, "0") + "0"; content += lineCountHex + " " + line + "\n"; line = ""; // count = 0; lineCount++; } content+= (lineCount).toString (16).padStart (7, "0") + count.toString(16) +"\n"; return content; } Component.onCompleted: { let fetch = QmlFetch.fetch; fetch("https://avatars0.githubusercontent.com/u/6630355") .then((res)=>{ let arrayBuffer = res.arrayBuffer(); let hex = hexdump(arrayBuffer); console.info(hex); }) } }