diff --git a/.gitignore b/.gitignore
index 3eadd6c..945b906 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,8 @@
/Avalonia-API/bin
/Avalonia-API/obj
/Avalonia-Services/bin
-/Avalonia-Services/obj
\ No newline at end of file
+/Avalonia-Services/obj
+/Avalonia-Web/.vscode
+/Avalonia-Web/obj
+/Avalonia-Web/node_modules
+/Avalonia-Web/dist
\ No newline at end of file
diff --git a/Avalonia-PC/Avalonia-PC.slnx b/Avalonia-PC/Avalonia-PC.slnx
index 9200293..0509c92 100644
--- a/Avalonia-PC/Avalonia-PC.slnx
+++ b/Avalonia-PC/Avalonia-PC.slnx
@@ -1,5 +1,9 @@
+
+
+
+
diff --git a/Avalonia-PC/Views/MainWindow.axaml.cs b/Avalonia-PC/Views/MainWindow.axaml.cs
index 267b166..2e67699 100644
--- a/Avalonia-PC/Views/MainWindow.axaml.cs
+++ b/Avalonia-PC/Views/MainWindow.axaml.cs
@@ -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;
}
""";
diff --git a/Avalonia-PC/www/index.html b/Avalonia-PC/www/index.html
index 952f347..91245ba 100644
--- a/Avalonia-PC/www/index.html
+++ b/Avalonia-PC/www/index.html
@@ -32,10 +32,21 @@
}
};
- const isWV2 = window.isWebView2 === true;
- setTimeout(() => {
- document.body.insertAdjacentHTML('beforeend', `
当前环境: ${isWV2 ? 'WebView2 (自定义协议)' : '普通浏览器 (HTTP API)'}
`);
- }, 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', `当前环境: ${isWV2 ? 'WebView2 (自定义协议)' : '普通浏览器 (HTTP API)'}
`);
+ };
+
+ renderEnvironment();
+ setTimeout(renderEnvironment, 300);