QML 中的 XMLHttpRequest 对象
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++
类。
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。
qml-fetch
仿制 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 下载处理二进制文件
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);
})
}
}