新增avalonia-web前端子项目及WebView桥接支持
新增基于Vite+Vue3的avalonia-web前端子项目,包含完整工程结构、配置文件、静态资源和组件。完善.gitignore等忽略规则,并将前端项目集成进解决方案。增强MainWindow.axaml.cs中WebView脚本,支持自定义协议的XMLHttpRequest桥接,提升JS与C#通信能力。补充CHANGELOG及环境检测提示。
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
<Solution>
|
||||
<Project Path="../Avalonia-API/Avalonia-API.csproj" Id="e33aba9a-a56b-4f6b-8eaa-3acbed65ebad" />
|
||||
<Project Path="../Avalonia-Services/Avalonia-Services.csproj" Id="b8757cf9-5422-4c67-acae-3c967c95f866" />
|
||||
<Project Path="../Avalonia-Web/avalonia-web.esproj">
|
||||
<Build />
|
||||
<Deploy />
|
||||
</Project>
|
||||
<Project Path="Avalonia-PC.csproj" />
|
||||
</Solution>
|
||||
|
||||
@@ -236,6 +236,34 @@ if (!window.__appBridgeInstalled) {
|
||||
};
|
||||
|
||||
const nativeFetch = window.fetch ? window.fetch.bind(window) : null;
|
||||
const NativeXMLHttpRequest = window.XMLHttpRequest;
|
||||
|
||||
const sendAppBridgeRequest = ({ requestUrl, method, headers, body, timeoutMs = 30000 }) => {
|
||||
const id = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
|
||||
|
||||
const responsePromise = new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
pending.delete(id);
|
||||
reject(new Error(`Timed out waiting for ${requestUrl}`));
|
||||
}, timeoutMs);
|
||||
|
||||
pending.set(id, {
|
||||
resolve: response => { clearTimeout(timeoutId); resolve(response); },
|
||||
reject: error => { clearTimeout(timeoutId); reject(error); }
|
||||
});
|
||||
});
|
||||
|
||||
window.invokeCSharpAction(JSON.stringify({
|
||||
kind: 'app-request',
|
||||
id,
|
||||
url: requestUrl,
|
||||
method,
|
||||
headers,
|
||||
body
|
||||
}));
|
||||
|
||||
return responsePromise;
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'F12' || (event.ctrlKey && event.shiftKey && (event.key === 'I' || event.key === 'i'))) {
|
||||
@@ -279,30 +307,242 @@ if (!window.__appBridgeInstalled) {
|
||||
body = await new Response(body).text();
|
||||
}
|
||||
|
||||
const id = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
|
||||
const responsePromise = new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
pending.delete(id);
|
||||
reject(new Error(`Timed out waiting for ${requestUrl}`));
|
||||
}, 30000);
|
||||
|
||||
pending.set(id, {
|
||||
resolve: response => { clearTimeout(timeoutId); resolve(response); },
|
||||
reject: error => { clearTimeout(timeoutId); reject(error); }
|
||||
});
|
||||
});
|
||||
|
||||
window.invokeCSharpAction(JSON.stringify({
|
||||
kind: 'app-request',
|
||||
id,
|
||||
url: requestUrl,
|
||||
return sendAppBridgeRequest({
|
||||
requestUrl,
|
||||
method: init?.method ?? request?.method ?? 'GET',
|
||||
headers,
|
||||
body: body ?? null
|
||||
}));
|
||||
|
||||
return responsePromise;
|
||||
body: body ?? null,
|
||||
timeoutMs: 30000
|
||||
});
|
||||
};
|
||||
|
||||
class BridgeXMLHttpRequest {
|
||||
constructor() {
|
||||
this._native = new NativeXMLHttpRequest();
|
||||
this._isAppRequest = false;
|
||||
this._requestUrl = '';
|
||||
this._method = 'GET';
|
||||
this._headers = {};
|
||||
this._responseHeaders = {};
|
||||
this._responseHeadersRaw = '';
|
||||
this._aborted = false;
|
||||
|
||||
this.readyState = 0;
|
||||
this.status = 0;
|
||||
this.statusText = '';
|
||||
this.response = null;
|
||||
this.responseText = '';
|
||||
this.responseType = '';
|
||||
this.responseURL = '';
|
||||
this.timeout = 0;
|
||||
this.withCredentials = false;
|
||||
|
||||
this.onreadystatechange = null;
|
||||
this.onload = null;
|
||||
this.onerror = null;
|
||||
this.ontimeout = null;
|
||||
this.onabort = null;
|
||||
this.onloadend = null;
|
||||
|
||||
this.upload = {
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {}
|
||||
};
|
||||
|
||||
this._native.onreadystatechange = () => {
|
||||
if (this._isAppRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.readyState = this._native.readyState;
|
||||
this.status = this._native.status;
|
||||
this.statusText = this._native.statusText;
|
||||
this.responseURL = this._native.responseURL ?? '';
|
||||
this.response = this._native.response;
|
||||
this.responseText = this._native.responseText ?? '';
|
||||
this._raiseReadyStateChange();
|
||||
};
|
||||
|
||||
this._native.onload = event => {
|
||||
if (!this._isAppRequest && typeof this.onload === 'function') {
|
||||
this.onload(event);
|
||||
}
|
||||
};
|
||||
|
||||
this._native.onerror = event => {
|
||||
if (!this._isAppRequest && typeof this.onerror === 'function') {
|
||||
this.onerror(event);
|
||||
}
|
||||
};
|
||||
|
||||
this._native.ontimeout = event => {
|
||||
if (!this._isAppRequest && typeof this.ontimeout === 'function') {
|
||||
this.ontimeout(event);
|
||||
}
|
||||
};
|
||||
|
||||
this._native.onabort = event => {
|
||||
if (!this._isAppRequest && typeof this.onabort === 'function') {
|
||||
this.onabort(event);
|
||||
}
|
||||
};
|
||||
|
||||
this._native.onloadend = event => {
|
||||
if (!this._isAppRequest && typeof this.onloadend === 'function') {
|
||||
this.onloadend(event);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
open(method, url, async = true, user, password) {
|
||||
const requestUrl = typeof url === 'string' || url instanceof URL
|
||||
? url.toString()
|
||||
: `${url ?? ''}`;
|
||||
|
||||
this._requestUrl = requestUrl;
|
||||
this._method = method ?? 'GET';
|
||||
this._isAppRequest = requestUrl.startsWith('app://');
|
||||
this._headers = {};
|
||||
this._responseHeaders = {};
|
||||
this._responseHeadersRaw = '';
|
||||
this._aborted = false;
|
||||
|
||||
if (!this._isAppRequest) {
|
||||
this._native.open(method, url, async, user, password);
|
||||
return;
|
||||
}
|
||||
|
||||
this.readyState = 1;
|
||||
this._raiseReadyStateChange();
|
||||
}
|
||||
|
||||
setRequestHeader(name, value) {
|
||||
if (!this._isAppRequest) {
|
||||
this._native.setRequestHeader(name, value);
|
||||
return;
|
||||
}
|
||||
|
||||
this._headers[name] = value;
|
||||
}
|
||||
|
||||
getAllResponseHeaders() {
|
||||
if (!this._isAppRequest) {
|
||||
return this._native.getAllResponseHeaders();
|
||||
}
|
||||
|
||||
return this._responseHeadersRaw;
|
||||
}
|
||||
|
||||
getResponseHeader(name) {
|
||||
if (!this._isAppRequest) {
|
||||
return this._native.getResponseHeader(name);
|
||||
}
|
||||
|
||||
return this._responseHeaders[name.toLowerCase()] ?? null;
|
||||
}
|
||||
|
||||
overrideMimeType(mimeType) {
|
||||
if (!this._isAppRequest && typeof this._native.overrideMimeType === 'function') {
|
||||
this._native.overrideMimeType(mimeType);
|
||||
}
|
||||
}
|
||||
|
||||
abort() {
|
||||
if (!this._isAppRequest) {
|
||||
this._native.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
this._aborted = true;
|
||||
if (typeof this.onabort === 'function') {
|
||||
this.onabort();
|
||||
}
|
||||
if (typeof this.onloadend === 'function') {
|
||||
this.onloadend();
|
||||
}
|
||||
}
|
||||
|
||||
async send(body = null) {
|
||||
if (!this._isAppRequest) {
|
||||
this._native.send(body);
|
||||
return;
|
||||
}
|
||||
|
||||
let requestBody = body;
|
||||
if (requestBody && typeof requestBody !== 'string') {
|
||||
requestBody = await new Response(requestBody).text();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await sendAppBridgeRequest({
|
||||
requestUrl: this._requestUrl,
|
||||
method: this._method,
|
||||
headers: this._headers,
|
||||
body: requestBody ?? null,
|
||||
timeoutMs: this.timeout > 0 ? this.timeout : 30000
|
||||
});
|
||||
|
||||
if (this._aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.status = response.status;
|
||||
this.statusText = response.statusText;
|
||||
this.responseURL = this._requestUrl;
|
||||
|
||||
this._responseHeaders = {};
|
||||
this._responseHeadersRaw = '';
|
||||
response.headers.forEach((value, key) => {
|
||||
this._responseHeaders[key.toLowerCase()] = value;
|
||||
this._responseHeadersRaw += `${key}: ${value}\r\n`;
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
this.responseText = text;
|
||||
this.response = this.responseType === 'json'
|
||||
? (text ? JSON.parse(text) : null)
|
||||
: text;
|
||||
|
||||
this.readyState = 4;
|
||||
this._raiseReadyStateChange();
|
||||
|
||||
if (typeof this.onload === 'function') {
|
||||
this.onload();
|
||||
}
|
||||
if (typeof this.onloadend === 'function') {
|
||||
this.onloadend();
|
||||
}
|
||||
} catch (error) {
|
||||
if (this._aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.status = 0;
|
||||
this.statusText = '';
|
||||
this.readyState = 4;
|
||||
this._raiseReadyStateChange();
|
||||
|
||||
const errorMessage = error?.message ?? '';
|
||||
if (errorMessage.includes('Timed out waiting') && typeof this.ontimeout === 'function') {
|
||||
this.ontimeout(error);
|
||||
} else if (typeof this.onerror === 'function') {
|
||||
this.onerror(error);
|
||||
}
|
||||
|
||||
if (typeof this.onloadend === 'function') {
|
||||
this.onloadend();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_raiseReadyStateChange() {
|
||||
if (typeof this.onreadystatechange === 'function') {
|
||||
this.onreadystatechange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.XMLHttpRequest = BridgeXMLHttpRequest;
|
||||
}
|
||||
""";
|
||||
|
||||
|
||||
@@ -32,10 +32,21 @@
|
||||
}
|
||||
};
|
||||
|
||||
const isWV2 = window.isWebView2 === true;
|
||||
setTimeout(() => {
|
||||
document.body.insertAdjacentHTML('beforeend', `<p>当前环境: ${isWV2 ? 'WebView2 (自定义协议)' : '普通浏览器 (HTTP API)'}</p>`);
|
||||
}, 100)
|
||||
const detectIsWebView2 = () => window.isWebView2 === true || typeof window.invokeCSharpAction === 'function';
|
||||
|
||||
const renderEnvironment = () => {
|
||||
const isWV2 = detectIsWebView2();
|
||||
const existing = document.getElementById('envTip');
|
||||
if (existing) {
|
||||
existing.textContent = `当前环境: ${isWV2 ? 'WebView2 (自定义协议)' : '普通浏览器 (HTTP API)'}`;
|
||||
return;
|
||||
}
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', `<p id="envTip">当前环境: ${isWV2 ? 'WebView2 (自定义协议)' : '普通浏览器 (HTTP API)'}</p>`);
|
||||
};
|
||||
|
||||
renderEnvironment();
|
||||
setTimeout(renderEnvironment, 300);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user