初始化 Vue3+Vite 前端模板,适配 WebView2 桥接
新增项目基础结构与配置,集成 Vue3、Vite、TypeScript、ESLint 等开发环境。实现主页面、样式、图标组件,封装 http 请求,支持 WebView2 与普通浏览器统一 API 调用,便于与 C# 后端通信。完善类型声明与开发文档。
This commit is contained in:
+4
@@ -0,0 +1,4 @@
|
||||
# Copilot Instructions
|
||||
|
||||
## 项目指南
|
||||
- 用户偏好:仅修改明确要求的内容,不要做额外改动(如未请求的 ViewModel DI 注册)。
|
||||
@@ -3,6 +3,7 @@ using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia_PC.ViewModels;
|
||||
using Avalonia_PC.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Avalonia_PC
|
||||
{
|
||||
@@ -17,13 +18,11 @@ namespace Avalonia_PC
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow
|
||||
{
|
||||
DataContext = new MainWindowViewModel(),
|
||||
};
|
||||
desktop.MainWindow = Program.Services.GetRequiredService<MainWindow>();
|
||||
desktop.MainWindow.DataContext = new MainWindowViewModel();
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include=".github\copilot-instructions.md" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="12.0.1" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="12.0.1" />
|
||||
@@ -24,7 +28,12 @@
|
||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
|
||||
<PackageReference Include="Avalonia.Controls.WebView" Version="12.0.0" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Avalonia-Services\Avalonia-Services.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<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">
|
||||
<Project Path="../avalonia-web-react/avalonia-web-react.esproj">
|
||||
<Build />
|
||||
<Deploy />
|
||||
</Project>
|
||||
<Project Path="../Avalonia-Web/avalonia-web-vue.esproj">
|
||||
<Build />
|
||||
<Deploy />
|
||||
</Project>
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
using Avalonia;
|
||||
using Avalonia_PC.Views;
|
||||
using Avalonia_Services.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
|
||||
namespace Avalonia_PC
|
||||
{
|
||||
internal sealed class Program
|
||||
{
|
||||
public static IServiceProvider Services { get; private set; } = null!;
|
||||
|
||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||
// yet and stuff might break.
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
ConfigureServices();
|
||||
|
||||
#if DEBUG
|
||||
// 开启 WebView2 远程调试,启动后在 Edge 中访问 edge://inspect 调试网页
|
||||
Environment.SetEnvironmentVariable(
|
||||
@@ -20,7 +27,14 @@ namespace Avalonia_PC
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||
}
|
||||
|
||||
private static void ConfigureServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<WeatherForecastService>();
|
||||
services.AddTransient<MainWindow>(sp => new MainWindow(sp));
|
||||
|
||||
Services = services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
namespace Avalonia_PC.Views
|
||||
{
|
||||
public partial class MainWindow
|
||||
{
|
||||
private const string BridgeScript = """
|
||||
if (!window.__appBridgeInstalled) {
|
||||
window.__appBridgeInstalled = true;
|
||||
window.isWebView2 = true;
|
||||
const pending = new Map();
|
||||
|
||||
const tryOpenDevTools = () => {
|
||||
window.invokeCSharpAction(JSON.stringify({ kind: 'app-open-devtools' }));
|
||||
};
|
||||
|
||||
window.__dispatchAppResponse = function(jsonStr) {
|
||||
const payload = JSON.parse(jsonStr);
|
||||
const responseId = payload.id ?? payload.Id;
|
||||
const entry = pending.get(responseId);
|
||||
if (!entry) return;
|
||||
pending.delete(responseId);
|
||||
entry.resolve(new Response(payload.body ?? payload.Body ?? '', {
|
||||
status: payload.statusCode ?? payload.StatusCode ?? 200,
|
||||
statusText: payload.statusMessage ?? payload.StatusMessage ?? 'OK',
|
||||
headers: payload.headers ?? payload.Headers ?? { 'Content-Type': 'application/json' }
|
||||
}));
|
||||
};
|
||||
|
||||
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'))) {
|
||||
event.preventDefault();
|
||||
tryOpenDevTools();
|
||||
}
|
||||
}, true);
|
||||
|
||||
document.addEventListener('contextmenu', event => {
|
||||
if (event.shiftKey) {
|
||||
event.preventDefault();
|
||||
tryOpenDevTools();
|
||||
}
|
||||
}, true);
|
||||
|
||||
window.fetch = async (input, init) => {
|
||||
const request = input instanceof Request ? input : null;
|
||||
const requestUrl = typeof input === 'string' || input instanceof URL
|
||||
? input.toString()
|
||||
: request?.url;
|
||||
|
||||
if (!requestUrl || !requestUrl.startsWith('app://')) {
|
||||
if (!nativeFetch) throw new Error('window.fetch is not available.');
|
||||
return nativeFetch(input, init);
|
||||
}
|
||||
|
||||
const combinedHeaders = new Headers(request?.headers);
|
||||
if (init?.headers) {
|
||||
new Headers(init.headers).forEach((value, key) => combinedHeaders.set(key, value));
|
||||
}
|
||||
|
||||
const headers = {};
|
||||
combinedHeaders.forEach((value, key) => headers[key] = value);
|
||||
|
||||
let body = init?.body;
|
||||
if (body === undefined && request) {
|
||||
body = await request.clone().text();
|
||||
}
|
||||
|
||||
if (body && typeof body !== 'string') {
|
||||
body = await new Response(body).text();
|
||||
}
|
||||
|
||||
return sendAppBridgeRequest({
|
||||
requestUrl,
|
||||
method: init?.method ?? request?.method ?? 'GET',
|
||||
headers,
|
||||
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;
|
||||
}
|
||||
""";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Avalonia_Services.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Avalonia_PC.Views
|
||||
{
|
||||
public partial class MainWindow
|
||||
{
|
||||
// 路由表:key = 接口路径(忽略大小写),value = 处理方法
|
||||
// 新增接口:在此方法中添加一行 _routes["api/xxx"] = ctx => ...
|
||||
private Dictionary<string, Func<RouteRequestContext, Task<object?>>> _routes = [];
|
||||
|
||||
// 服务容器,通过构造函数注入,路由注册时按需解析服务
|
||||
private IServiceProvider _services = null!;
|
||||
|
||||
private void RegisterRoutes()
|
||||
{
|
||||
var weather = _services.GetRequiredService<WeatherForecastService>();
|
||||
// 新增服务示例:var myService = _services.GetRequiredService<MyService>();
|
||||
|
||||
_routes = new Dictionary<string, Func<RouteRequestContext, Task<object?>>>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["api/getUser"] = _ => GetUserFromDatabaseAsync(),
|
||||
["api/processData"] = ctx => ProcessDataAsync(ExtractInput(ctx)),
|
||||
["api/wData"] = _ => Task.FromResult<object?>(weather.GetWeatherForecasts()),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 示例:模拟读取用户数据。
|
||||
/// </summary>
|
||||
private static async Task<object?> GetUserFromDatabaseAsync()
|
||||
{
|
||||
await Task.Delay(100);
|
||||
return new { id = 1, name = "张三", email = "zhangsan@example.com" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 示例:模拟处理输入数据。
|
||||
/// </summary>
|
||||
private static async Task<object?> ProcessDataAsync(string? input)
|
||||
{
|
||||
await Task.Delay(200);
|
||||
return $"Processed: {input?.ToUpperInvariant()}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@ namespace Avalonia_PC.Views
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private const string AppScheme = "app";
|
||||
//private const string? OnlineStartupUrl = "https://re.laitool.cn";
|
||||
private const string? OnlineStartupUrl = null;
|
||||
private const string? OnlineStartupUrl = "http://localhost:51240";
|
||||
//private const string? OnlineStartupUrl = null;
|
||||
private const string? LocalStartupPath = null;
|
||||
private static readonly JsonSerializerOptions BridgeJsonSerializerOptions = new()
|
||||
{
|
||||
@@ -36,12 +36,14 @@ namespace Avalonia_PC.Views
|
||||
/// <summary>
|
||||
/// 初始化窗口并注册生命周期事件。
|
||||
/// </summary>
|
||||
public MainWindow()
|
||||
public MainWindow(IServiceProvider services)
|
||||
{
|
||||
_services = services;
|
||||
InitializeComponent();
|
||||
|
||||
Opened += OnOpened;
|
||||
Closed += OnClosed;
|
||||
|
||||
RegisterRoutes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -212,341 +214,7 @@ namespace Avalonia_PC.Views
|
||||
return;
|
||||
}
|
||||
|
||||
const string script = """
|
||||
if (!window.__appBridgeInstalled) {
|
||||
window.__appBridgeInstalled = true;
|
||||
window.isWebView2 = true;
|
||||
const pending = new Map();
|
||||
|
||||
const tryOpenDevTools = () => {
|
||||
window.invokeCSharpAction(JSON.stringify({ kind: 'app-open-devtools' }));
|
||||
};
|
||||
|
||||
window.__dispatchAppResponse = function(jsonStr) {
|
||||
const payload = JSON.parse(jsonStr);
|
||||
const responseId = payload.id ?? payload.Id;
|
||||
const entry = pending.get(responseId);
|
||||
if (!entry) return;
|
||||
pending.delete(responseId);
|
||||
entry.resolve(new Response(payload.body ?? payload.Body ?? '', {
|
||||
status: payload.statusCode ?? payload.StatusCode ?? 200,
|
||||
statusText: payload.statusMessage ?? payload.StatusMessage ?? 'OK',
|
||||
headers: payload.headers ?? payload.Headers ?? { 'Content-Type': 'application/json' }
|
||||
}));
|
||||
};
|
||||
|
||||
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'))) {
|
||||
event.preventDefault();
|
||||
tryOpenDevTools();
|
||||
}
|
||||
}, true);
|
||||
|
||||
document.addEventListener('contextmenu', event => {
|
||||
if (event.shiftKey) {
|
||||
event.preventDefault();
|
||||
tryOpenDevTools();
|
||||
}
|
||||
}, true);
|
||||
|
||||
window.fetch = async (input, init) => {
|
||||
const request = input instanceof Request ? input : null;
|
||||
const requestUrl = typeof input === 'string' || input instanceof URL
|
||||
? input.toString()
|
||||
: request?.url;
|
||||
|
||||
if (!requestUrl || !requestUrl.startsWith('app://')) {
|
||||
if (!nativeFetch) throw new Error('window.fetch is not available.');
|
||||
return nativeFetch(input, init);
|
||||
}
|
||||
|
||||
const combinedHeaders = new Headers(request?.headers);
|
||||
if (init?.headers) {
|
||||
new Headers(init.headers).forEach((value, key) => combinedHeaders.set(key, value));
|
||||
}
|
||||
|
||||
const headers = {};
|
||||
combinedHeaders.forEach((value, key) => headers[key] = value);
|
||||
|
||||
let body = init?.body;
|
||||
if (body === undefined && request) {
|
||||
body = await request.clone().text();
|
||||
}
|
||||
|
||||
if (body && typeof body !== 'string') {
|
||||
body = await new Response(body).text();
|
||||
}
|
||||
|
||||
return sendAppBridgeRequest({
|
||||
requestUrl,
|
||||
method: init?.method ?? request?.method ?? 'GET',
|
||||
headers,
|
||||
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;
|
||||
}
|
||||
""";
|
||||
|
||||
await _webView.InvokeScript(script);
|
||||
await _webView.InvokeScript(BridgeScript);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -590,8 +258,6 @@ if (!window.__appBridgeInstalled) {
|
||||
{
|
||||
var uri = new Uri(rawUrl ?? throw new InvalidOperationException("请求地址不能为空。"));
|
||||
var requestContext = CreateRouteRequestContext(uri, body);
|
||||
var authorization = GetAuthorizationHeader(headers);
|
||||
_ = authorization;
|
||||
|
||||
if (string.Equals(method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -625,37 +291,14 @@ if (!window.__appBridgeInstalled) {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按请求前缀分发处理器(例如 api、sys、admin 等)。
|
||||
/// 按路由表匹配并调用对应处理器。
|
||||
/// </summary>
|
||||
private async Task<RouteDispatchResult> DispatchByPrefixAsync(RouteRequestContext requestContext)
|
||||
{
|
||||
if (requestContext.PathSegments.Length > 0 &&
|
||||
string.Equals(requestContext.PathSegments[0], "api", StringComparison.OrdinalIgnoreCase))
|
||||
if (_routes.TryGetValue(requestContext.NormalizedPath, out var handler))
|
||||
{
|
||||
return await HandleApiPrefixAsync(requestContext);
|
||||
}
|
||||
|
||||
return RouteDispatchResult.NotMatched();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 api 前缀下的具体业务路由。
|
||||
/// </summary>
|
||||
private static async Task<RouteDispatchResult> HandleApiPrefixAsync(RouteRequestContext requestContext)
|
||||
{
|
||||
if (string.Equals(requestContext.NormalizedPath, "api/getUser", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var user = await GetUserFromDatabaseAsync();
|
||||
return RouteDispatchResult.Success(user);
|
||||
}
|
||||
|
||||
if (string.Equals(requestContext.NormalizedPath, "api/processData", StringComparison.OrdinalIgnoreCase) ||
|
||||
(requestContext.PathSegments.Length > 1 &&
|
||||
string.Equals(requestContext.PathSegments[1], "processData", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var input = ExtractInput(requestContext);
|
||||
var result = await ProcessDataAsync(input);
|
||||
return RouteDispatchResult.Success(result);
|
||||
var data = await handler(requestContext);
|
||||
return RouteDispatchResult.Success(data);
|
||||
}
|
||||
|
||||
return RouteDispatchResult.NotMatched();
|
||||
@@ -1000,28 +643,6 @@ if (!window.__appBridgeInstalled) {
|
||||
|
||||
#endregion
|
||||
|
||||
#region 业务示例方法
|
||||
|
||||
/// <summary>
|
||||
/// 示例:模拟读取用户数据。
|
||||
/// </summary>
|
||||
private static async Task<object> GetUserFromDatabaseAsync()
|
||||
{
|
||||
await Task.Delay(100);
|
||||
return new { id = 1, name = "张三", email = "zhangsan@example.com" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 示例:模拟处理输入数据。
|
||||
/// </summary>
|
||||
private static async Task<string> ProcessDataAsync(string? input)
|
||||
{
|
||||
await Task.Delay(200);
|
||||
return $"Processed: {input?.ToUpperInvariant()}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DTO / 路由上下文模型
|
||||
|
||||
private sealed class AppResponse
|
||||
|
||||
@@ -46,5 +46,6 @@ async function callApi(endpoint, options = {}) {
|
||||
|
||||
window.api = {
|
||||
getUser: () => callApi("getUser?t=1"),
|
||||
processData: (input) => callApi("processData", { method: "POST", body: { input } })
|
||||
processData: (input) => callApi("processData", { method: "POST", body: { input } }),
|
||||
wData: (input) => callApi("wData", { method: "POST", body: { input } }),
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<h1>WebView2 自定义协议演示</h1>
|
||||
<button id="getUserBtn">获取用户信息</button>
|
||||
<button id="processBtn">处理数据</button>
|
||||
<button id="wBtn">天气数据</button>
|
||||
<pre id="output"></pre>
|
||||
|
||||
<script src="./api.js"></script>
|
||||
@@ -32,6 +33,15 @@
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('wBtn').onclick = async () => {
|
||||
try {
|
||||
const result = await window.api.wData('hello world');
|
||||
output.textContent = JSON.stringify(result, null, 2);
|
||||
} catch (err) {
|
||||
output.textContent = `错误: ${err.message}`;
|
||||
}
|
||||
};
|
||||
|
||||
const detectIsWebView2 = () => window.isWebView2 === true || typeof window.invokeCSharpAction === 'function';
|
||||
|
||||
const renderEnvironment = () => {
|
||||
|
||||
Reference in New Issue
Block a user