Loading...
正在加载...
请稍候

第十一章:深度集成 WebAssembly:与 JavaScript 互操作

✨步子哥 (steper) 2026年02月17日 05:28
# 第十一章:深度集成 WebAssembly:与 JavaScript 互操作 > **本章导读**:想象一座横跨两个世界的桥梁——一边是严谨有序的 C# 世界,强类型、面向对象、编译时检查;另一边是灵活多变的 JavaScript 生态,动态类型、函数式编程、解释执行。WebAssembly 就是这座桥梁的地基,它让 C# 代码能够以接近原生的速度在浏览器中运行。而 Uno Platform 则是在这座地基上建造的豪华大厦,让你用同一套代码同时征服原生应用和 Web 世界。本章将带你深入这座桥梁的内部结构,揭开 C# 与 JavaScript 互操作的神秘面纱。 --- ## 🌐 11.1 .NET 进入浏览器:WASM 运行机制 在正式开始互操作的探讨之前,我们需要先理解一个根本性的问题:**为什么 C# 代码能够在浏览器中运行?** 要回答这个问题,我们需要回到 2017 年,那是 WebAssembly 正式发布的年份。 WebAssembly(简称 WASM)是一种全新的二进制指令格式,它定义了一种可在现代浏览器中执行的"虚拟机器语言"。与 JavaScript 不同,WASM 不是解释执行的,而是被浏览器直接编译为机器码运行。这使得它的执行速度可以接近原生 C++ 代码——对于计算密集型任务,这简直是革命性的进步。 > **第一性原理**:WebAssembly 的核心设计目标是"可移植、高效、安全"。可移植意味着一次编译,到处运行;高效意味着接近原生的执行速度;安全意味着在沙盒环境中运行,无法直接访问系统资源。这三个特性恰好也是 Uno Platform 追求的目标,难怪 WASM 成为了 Uno Web 版本的技术基础。 ### 🔬 11.1.1 Uno WASM 应用的启动流程 当你在浏览器中打开一个 Uno Platform 的 WebAssembly 应用时,实际上发生了一系列精心编排的事件。让我们像慢动作回放一样,逐一审视这个过程的每一个环节。 **第一阶段:加载引导程序**。浏览器首先下载一个极小的 JavaScript 文件(称为 `uno-bootstrap.js`),这个文件负责协调后续的所有加载工作。它就像是交响乐团的指挥,虽然自己不演奏乐器,但整个乐团的节奏都由它掌控。 **第二阶段:下载 .NET 运行时**。接下来,浏览器会下载一个经过精简的 .NET 运行时。这个运行时是基于 Mono 的 WASM 构建版本,大小通常在 2-5 MB 之间(开启 AOT 后会更大)。这个运行时就是 C# 代码在浏览器中生存的"土壤"——没有它,你的 DLL 文件只是一堆毫无意义的二进制数据。 **第三阶段:加载应用程序集**。运行时就位后,浏览器会下载你的应用程序编译出的 DLL 文件,以及所有依赖的 NuGet 包。这些 DLL 在 WASM 运行时中加载,就像在桌面应用中一样。 **第四阶段:初始化渲染引擎**。Uno Platform 支持两种渲染模式:**HTML 模式**将 WinUI 控件映射为 HTML 元素;**Canvas 模式**使用 Skia 图形库在 `<canvas>` 元素上绘制所有 UI。初始化渲染引擎后,你的 XAML 界面就会出现在浏览器中。 ```csharp // Program.cs 中的入口点 public class Program { public static void Main(string[] args) { // 这是 Uno WASM 应用的生命起点 // Microsoft.UI.Xaml.Application.Start 函数会初始化整个应用框架 // 它负责: // 1. 创建 Application 单例 // 2. 初始化 UI 渲染管线 // 3. 加载启动页面 Microsoft.UI.Xaml.Application.Start(p => new App()); } } ``` ### ⚡ 11.1.2 解释器模式 vs 原生 AOT Uno Platform 的 WebAssembly 版本支持两种执行模式,它们各有优劣,适用于不同的场景。 **解释器模式(Interpreter Mode)**是默认选项。在这种模式下,.NET 运行时会在浏览器中解释执行你的 DLL 文件中的 IL(中间语言)代码。这种模式的优势在于**下载体积小**——因为只需要下载运行时和 DLL,不需要额外的编译产物。但代价是**执行速度较慢**,因为每一行代码都需要在运行时被解释执行。 **原生 AOT 模式(Ahead-of-Time Compilation)**则完全不同。在这种模式下,你的 C# 代码会在编译时被直接转换为 WASM 二进制指令,而不是 IL。这意味着浏览器不需要解释执行——它只需要直接运行这些预先编译好的机器指令。执行速度可以接近原生 C++ 应用,但代价是**下载体积显著增加**(可能达到 10-20 MB 或更多)。 ```xml <!-- 在 Wasm 项目文件中启用 AOT --> <PropertyGroup> <!-- 启用 AOT 编译 --> <WasmShellEnableAOT>true</WasmShellEnableAOT> <!-- 启用 AOT 后,可以选择进一步优化 --> <!-- 这会增加编译时间,但能减小输出体积 --> <WasmShellILLinker>true</WasmShellILLinker> <!-- 设置 AOT 优化的级别 --> <!-- 可选值:none, speed, size, balanced --> <EmccCompileOptimizationFlag>O2</EmccCompileOptimizationFlag> </PropertyGroup> ``` > **费曼技巧提问**:如果你要向一位非技术人员解释"解释执行"和"AOT 编译"的区别,你会怎么说? > > 想象你在阅读一本外文书。"解释执行"就像是你一边读一边查字典,每个单词都需要翻译,阅读速度很慢;"AOT 编译"就像是你提前请人把整本书翻译成了中文,阅读时一气呵成,但前提是你愿意等待翻译完成,并且愿意接受这本"译本"更大的体积。 --- ## 🔄 11.2 现代互操作范式:`JSImport` 与 `JSExport` 在理解了 WASM 运行机制之后,我们终于可以进入本章的核心主题:**C# 与 JavaScript 的互操作**。.NET 7 引入了全新的、基于 Source Generators 的互操作方式,这是目前最高效的跨语言调用方案。 ### 📤 11.2.1 从 C# 调用 JavaScript:`JSImport` `JSImport` 属性让你可以在 C# 中声明一个外部 JavaScript 函数,然后像调用普通 C# 方法一样调用它。编译器会在编译时自动生成高效的桥接代码,避免了传统反射方式的开销。 首先,让我们在 JavaScript 文件中定义一个简单的函数: ```javascript // 文件位置:WasmScripts/main.js // 注意:这个文件需要添加到项目配置中 /** * 在控制台打印一条带样式的消息 * @param {string} message - 要打印的消息内容 */ export function logStyledMessage(message) { console.log( `%c Uno WASM: ${message}`, "color: blue; font-weight: bold; font-size: 14px;" ); } /** * 获取浏览器的地理位置 * @returns {Promise<{latitude: number, longitude: number}>} */ export function getCurrentLocation() { return new Promise((resolve, reject) => { if (!navigator.geolocation) { reject(new Error("浏览器不支持地理位置")); return; } navigator.geolocation.getCurrentPosition( (position) => { resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude }); }, (error) => { reject(new Error(error.message)); } ); }); } /** * 在本地存储中保存数据 * @param {string} key - 存储键名 * @param {string} value - 存储值 */ export function saveToLocalStorage(key, value) { localStorage.setItem(key, value); } /** * 从本地存储中读取数据 * @param {string} key - 存储键名 * @returns {string|null} 存储的值,如果不存在则返回 null */ export function loadFromLocalStorage(key) { return localStorage.getItem(key); } ``` 然后,在 C# 中声明对应的互操作方法: ```csharp // 文件位置:Services/BrowserInterop.cs using System.Runtime.InteropServices.JavaScript; namespace MyUnoApp.Services { /// <summary> /// 浏览器互操作服务 /// 提供 C# 与 JavaScript 之间的双向通信能力 /// </summary> public static partial class BrowserInterop { // 确保在使用前初始化 JavaScript 模块 // 这个方法应该在应用启动时调用 public static async Task InitializeAsync() { // 加载 JavaScript 模块 // "main.js" 对应 WasmScripts 文件夹中的文件名 // 如果模块加载失败,后续的所有互操作调用都会抛出异常 await JSHost.ImportAsync("main.js", "./main.js"); } /// <summary> /// 在浏览器控制台打印带样式的消息 /// </summary> /// <param name="message">消息内容</param> [JSImport("logStyledMessage", "main.js")] public static partial void LogStyledMessage(string message); /// <summary> /// 获取当前地理位置(异步) /// </summary> /// <returns>包含纬度和经度的位置信息</returns> [JSImport("getCurrentLocation", "main.js")] [return: JSMarshalAs<Promise<JSObject>>] public static partial Task<JSObject> GetCurrentLocationAsync(); /// <summary> /// 保存数据到本地存储 /// </summary> [JSImport("saveToLocalStorage", "main.js")] public static partial void SaveToLocalStorage(string key, string value); /// <summary> /// 从本地存储读取数据 /// </summary> [JSImport("loadFromLocalStorage", "main.js")] public static partial string LoadFromLocalStorage(string key); } } ``` 使用这些互操作方法非常简单: ```csharp public partial class MainPage : Page { public MainPage() { this.InitializeComponent(); Loaded += OnLoaded; } private async void OnLoaded(object sender, RoutedEventArgs e) { // 初始化互操作模块 // 这一步只需要执行一次 #if __WASM__ await BrowserInterop.InitializeAsync(); // 调用 JavaScript 函数打印消息 BrowserInterop.LogStyledMessage("应用已启动!"); // 获取地理位置 try { var locationObj = await BrowserInterop.GetCurrentLocationAsync(); double latitude = locationObj.GetPropertyAsDouble("latitude"); double longitude = locationObj.GetPropertyAsDouble("longitude"); // 更新 UI LocationTextBlock.Text = $"当前位置:{latitude:F4}, {longitude:F4}"; } catch (Exception ex) { Console.WriteLine($"获取位置失败: {ex.Message}"); } // 使用本地存储 BrowserInterop.SaveToLocalStorage("lastVisit", DateTime.Now.ToString()); string lastVisit = BrowserInterop.LoadFromLocalStorage("lastVisit"); #endif } } ``` ### 📥 11.2.2 从 JavaScript 调用 C#:`JSExport` 互操作是双向的——你不仅可以从 C# 调用 JavaScript,还可以将 C# 方法暴露给 JavaScript 调用。这在处理 Web 回调(如支付完成、第三方登录)时特别有用。 ```csharp // 文件位置:Services/DataProcessor.cs using System.Runtime.InteropServices.JavaScript; namespace MyUnoApp.Services { /// <summary> /// 数据处理器 - 向 JavaScript 暴露 C# 方法 /// </summary> public static partial class DataProcessor { /// <summary> /// 处理文本数据(转换为大写) /// </summary> /// <param name="input">输入文本</param> /// <returns>转换后的文本</returns> [JSExport] public static string ProcessData(string input) { // 这个方法可以从 JavaScript 中调用 // JavaScript 代码:Module.mono_bind_static_method("[MyUnoApp] MyUnoApp.Services.DataProcessor:ProcessData")("hello") return input.ToUpper(); } /// <summary> /// 计算斐波那契数列 /// </summary> /// <param name="n">数列长度</param> /// <returns>斐波那契数列数组</returns> [JSExport] public static int[] CalculateFibonacci(int n) { if (n <= 0) return Array.Empty<int>(); if (n == 1) return new int[] { 0 }; var result = new int[n]; result[0] = 0; result[1] = 1; for (int i = 2; i < n; i++) { result[i] = result[i - 1] + result[i - 2]; } return result; } /// <summary> /// 通知 C# 应用某个 JavaScript 事件发生了 /// </summary> /// <param name="eventName">事件名称</param> /// <param name="eventData">事件数据(JSON 字符串)</param> [JSExport] public static void NotifyEvent(string eventName, string eventData) { // 这个方法可以被外部 JavaScript 库调用 // 例如:支付完成后的回调、第三方登录成功等 System.Diagnostics.Debug.WriteLine($"收到事件: {eventName}, 数据: {eventData}"); // 在实际应用中,这里可能触发某些业务逻辑 // 或者通过消息中心通知其他组件 } } } ``` > **技术术语**:**Source Generator(源生成器)** 是 C# 9.0 引入的编译时代码生成技术。当你使用 `[JSImport]` 或 `[JSExport]` 属性时,编译器会在编译时自动分析这些声明,并生成高效的互操作代码。这与传统的反射方式相比,不仅消除了运行时开销,还能在编译时发现类型错误。 --- ## ⚡ 11.3 经典互操作:`InvokeJS` 快速调用 虽然 `JSImport` 和 `JSExport` 是现代、高效的互操作方式,但在很多场景下,你只需要执行一行简单的 JavaScript 代码。这时候,Uno Platform 提供的 `WebAssemblyRuntime.InvokeJS` 方法依然是效率之选。 ### 🎯 11.3.1 快速执行单行脚本 ```csharp using Uno.Foundation; public class QuickJsInterop { /// <summary> /// 获取浏览器的 User-Agent 字符串 /// </summary> public string GetUserAgent() { #if __WASM__ // InvokeJS 直接执行 JavaScript 代码并返回字符串结果 // 这是最简单的互操作方式,适合一次性、简单的调用 string userAgent = WebAssemblyRuntime.InvokeJS("navigator.userAgent"); return userAgent; #else return "非 WebAssembly 环境"; #endif } /// <summary> /// 获取当前页面的 URL /// </summary> public string GetCurrentUrl() { #if __WASM__ return WebAssemblyRuntime.InvokeJS("window.location.href"); #else return string.Empty; #endif } /// <summary> /// 显示一个原生对话框 /// </summary> public void ShowAlert(string message) { #if __WASM__ // 注意:alert 会阻塞浏览器,应谨慎使用 WebAssemblyRuntime.InvokeJS($"alert('{message.Replace("'", "\\'")}')"); #endif } /// <summary> /// 导航到指定的 URL /// </summary> public void NavigateToUrl(string url) { #if __WASM__ WebAssemblyRuntime.InvokeJS($"window.location.href = '{url}'"); #endif } /// <summary> /// 执行更复杂的 JavaScript 表达式 /// </summary> public int GetScreenHeight() { #if __WASM__ string result = WebAssemblyRuntime.InvokeJS("window.screen.height"); if (int.TryParse(result, out int height)) { return height; } return 0; #else return 0; #endif } } ``` ### 🔧 11.3.2 处理复杂返回值 `InvokeJS` 只能返回字符串,这对于复杂的数据结构是一个限制。解决方案是使用 JSON 作为数据交换格式: ```csharp using System.Text.Json; using Uno.Foundation; public class JsonInterop { public class BrowserInfo { public string UserAgent { get; set; } public int ScreenWidth { get; set; } public int ScreenHeight { get; set; } public string Language { get; set; } public bool IsOnline { get; set; } } public BrowserInfo GetBrowserInfo() { #if __WASM__ // 在 JavaScript 中构建 JSON 字符串 string jsonResult = WebAssemblyRuntime.InvokeJS(@" JSON.stringify({ userAgent: navigator.userAgent, screenWidth: window.screen.width, screenHeight: window.screen.height, language: navigator.language, isOnline: navigator.onLine }) "); // 在 C# 中反序列化 return JsonSerializer.Deserialize<BrowserInfo>(jsonResult); #else return new BrowserInfo(); #endif } } ``` > **第一性原理**:为什么 `InvokeJS` 比 `JSImport` 更适合简单场景?因为 `JSImport` 需要预先声明和加载模块,对于只需要执行一行代码的场景来说,这种开销是不必要的。选择工具时,要考虑"投入产出比"——简单的任务用简单的工具,复杂的任务用复杂的工具。 --- ## 📊 11.4 集成外部 JavaScript 库 Uno 应用并不是孤岛。JavaScript 生态系统中有大量成熟的、经过时间检验的库——图表库(如 Chart.js、D3.js)、地图库(如 Leaflet)、富文本编辑器(如 TinyMCE)等等。通过互操作,你可以在保持 WinUI 架构的同时,充分利用这些专业组件。 ### 📈 11.4.1 实战:集成 Chart.js 图表库 让我们通过一个完整的示例,展示如何在 Uno WASM 应用中集成 Chart.js。 **第一步:添加 Chart.js 引用** 在你的 WASM 项目的 `wwwroot` 文件夹中找到 `index.html`,添加 Chart.js 的 CDN 引用: ```html <!-- 文件位置:Wasm/wwwroot/index.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>My Uno App</title> <!-- 添加 Chart.js CDN 引用 --> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> </head> <body> <div id="uno-body"></div> </body> </html> ``` **第二步:创建 JavaScript 互操作代码** ```javascript // 文件位置:WasmScripts/chartInterop.js let currentChart = null; /** * 创建或更新图表 * @param {string} canvasId - Canvas 元素的 ID * @param {string} chartType - 图表类型 (bar, line, pie, doughnut 等) * @param {string} labelsJson - 标签数组的 JSON 字符串 * @param {string} dataJson - 数据数组的 JSON 字符串 */ export function createChart(canvasId, chartType, labelsJson, dataJson) { const canvas = document.getElementById(canvasId); if (!canvas) { console.error(`Canvas element with id '${canvasId}' not found`); return false; } // 如果已存在图表,先销毁 if (currentChart) { currentChart.destroy(); } const labels = JSON.parse(labelsJson); const data = JSON.parse(dataJson); currentChart = new Chart(canvas, { type: chartType, data: { labels: labels, datasets: [{ label: '数据系列', data: data, backgroundColor: [ 'rgba(255, 99, 132, 0.7)', 'rgba(54, 162, 235, 0.7)', 'rgba(255, 206, 86, 0.7)', 'rgba(75, 192, 192, 0.7)', 'rgba(153, 102, 255, 0.7)' ], borderColor: [ 'rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)' ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'top' } } } }); return true; } /** * 更新图表数据 * @param {string} labelsJson - 新的标签数组 JSON * @param {string} dataJson - 新的数据数组 JSON */ export function updateChartData(labelsJson, dataJson) { if (!currentChart) { console.error("No chart exists to update"); return false; } currentChart.data.labels = JSON.parse(labelsJson); currentChart.data.datasets[0].data = JSON.parse(dataJson); currentChart.update(); return true; } ``` **第三步:创建 C# 互操作服务** ```csharp // 文件位置:Services/ChartInterop.cs using System.Runtime.InteropServices.JavaScript; using System.Text.Json; namespace MyUnoApp.Services { public static partial class ChartInterop { public static async Task InitializeAsync() { #if __WASM__ await JSHost.ImportAsync("chartInterop.js", "./chartInterop.js"); #endif } /// <summary> /// 创建图表 /// </summary> /// <param name="canvasId">Canvas 元素的 ID</param> /// <param name="chartType">图表类型</param> /// <param name="labels">标签数组</param> /// <param name="data">数据数组</param> /// <returns>是否创建成功</returns> [JSImport("createChart", "chartInterop.js")] public static partial bool CreateChart( string canvasId, string chartType, string labelsJson, string dataJson); [JSImport("updateChartData", "chartInterop.js")] public static partial bool UpdateChartData(string labelsJson, string dataJson); // 便捷方法(自动处理 JSON 序列化) public static bool CreateChart<T>(string canvasId, string chartType, string[] labels, T[] data) { string labelsJson = JsonSerializer.Serialize(labels); string dataJson = JsonSerializer.Serialize(data); return CreateChart(canvasId, chartType, labelsJson, dataJson); } } } ``` **第四步:在 XAML 中使用** ```xml <!-- 文件位置:Pages/ChartPage.xaml --> <Page x:Class="MyUnoApp.Pages.ChartPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- 图表类型选择 --> <StackPanel Grid.Row="0" Orientation="Horizontal" Padding="16"> <Button Content="柱状图" Click="OnBarChartClick" Margin="0,0,8,0"/> <Button Content="折线图" Click="OnLineChartClick" Margin="0,0,8,0"/> <Button Content="饼图" Click="OnPieChartClick" Margin="0,0,8,0"/> <Button Content="刷新数据" Click="OnRefreshDataClick"/> </StackPanel> <!-- 图表容器 --> <!-- 使用 HtmlElement 在 XAML 中嵌入 HTML 元素 --> <Border Grid.Row="1" Margin="16" Background="White" CornerRadius="8" xmlns:utu="using:Uno.Toolkit.UI"> <!-- HtmlElement 是 Uno Platform 提供的桥接控件 --> <!-- 它允许你在 XAML 布局中嵌入原生 HTML 元素 --> <ContentControl x:Name="ChartContainer" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/> </Border> </Grid> </Page> ``` ```csharp // 文件位置:Pages/ChartPage.xaml.cs using System.Collections.Generic; using MyUnoApp.Services; namespace MyUnoApp.Pages { public sealed partial class ChartPage : Page { private readonly string _canvasId = "myChart"; public ChartPage() { this.InitializeComponent(); Loaded += OnLoaded; } private async void OnLoaded(object sender, RoutedEventArgs e) { #if __WASM__ // 初始化图表互操作 await ChartInterop.InitializeAsync(); // 创建 HTML Canvas 元素 // 注意:这里使用了 WebAssembly 特有的 HTML 宿主功能 CreateCanvasElement(); // 创建默认图表 ShowBarChart(); #endif } private void CreateCanvasElement() { // 在实际项目中,可以使用 Uno.Toolkit 的 HtmlElement 控件 // 或者通过 JavaScript 动态创建 Canvas string createCanvasScript = $@" (function() {{ var container = document.getElementById('chartContainer'); if (!container) {{ container = document.createElement('div'); container.id = 'chartContainer'; container.style.width = '100%'; container.style.height = '100%'; document.body.appendChild(container); }} var existingCanvas = document.getElementById('{_canvasId}'); if (!existingCanvas) {{ var canvas = document.createElement('canvas'); canvas.id = '{_canvasId}'; canvas.style.width = '100%'; canvas.style.height = '100%'; container.appendChild(canvas); }} }})(); "; Uno.Foundation.WebAssemblyRuntime.InvokeJS(createCanvasScript); } private void ShowBarChart() { var labels = new[] { "一月", "二月", "三月", "四月", "五月" }; var data = new[] { 12, 19, 3, 5, 2 }; ChartInterop.CreateChart(_canvasId, "bar", labels, data); } private void OnBarChartClick(object sender, RoutedEventArgs e) => ShowBarChart(); private void OnLineChartClick(object sender, RoutedEventArgs e) { var labels = new[] { "一月", "二月", "三月", "四月", "五月" }; var data = new[] { 12, 19, 3, 5, 2 }; ChartInterop.CreateChart(_canvasId, "line", labels, data); } private void OnPieChartClick(object sender, RoutedEventArgs e) { var labels = new[] { "红色", "蓝色", "黄色", "绿色", "紫色" }; var data = new[] { 12, 19, 3, 5, 2 }; ChartInterop.CreateChart(_canvasId, "pie", labels, data); } private void OnRefreshDataClick(object sender, RoutedEventArgs e) { // 生成随机数据 var random = new Random(); var labels = new[] { "一月", "二月", "三月", "四月", "五月" }; var data = new[] { random.Next(1, 30), random.Next(1, 30), random.Next(1, 30), random.Next(1, 30), random.Next(1, 30) }; // 注意:这里使用了 UpdateChartData,需要先将数据序列化 string labelsJson = System.Text.Json.JsonSerializer.Serialize(labels); string dataJson = System.Text.Json.JsonSerializer.Serialize(data); ChartInterop.UpdateChartData(labelsJson, dataJson); } } } ``` > **费曼技巧提问**:为什么我们需要这种"混合渲染"模式?为什么不让 C# 完全控制一切? > > 想象你是一家餐厅的厨师。你有自己拿手的菜品(C# 业务逻辑),但有时候客人想要一道你不擅长的异国料理(复杂的图表、地图)。与其从零学习这道菜的做法,不如请一位擅长这道菜的专家(JavaScript 库)来帮忙。你负责整体菜单的规划和上菜顺序(WinUI 导航和状态管理),而专家负责他那道拿手菜的烹饪。这种合作模式既保证了整体的一致性,又利用了各方的专长。 --- ## 🚀 11.5 性能优化策略 WebAssembly 虽然强大,但它在浏览器沙盒内运行,资源是受限的。本节将介绍几种关键的性能优化策略,帮助你的 Uno WASM 应用达到最佳性能。 ### 📦 11.5.1 减少互操作次数 跨越 C# 和 JavaScript 的边界是有开销的。每次调用都需要进行数据序列化、上下文切换和类型转换。因此,最佳实践是**批量传递数据,而非在循环中频繁调用**。 ```csharp // ❌ 不好的做法:在循环中频繁调用互操作 public void UpdateChartBad(double[] data) { for (int i = 0; i < data.Length; i++) { // 每次循环都跨越一次 C#/JS 边界 UpdateSingleDataPoint(i, data[i]); } } // ✅ 好的做法:批量传递数据 public void UpdateChartGood(double[] data) { // 一次性传递所有数据,只跨越一次边界 string dataJson = JsonSerializer.Serialize(data); UpdateAllDataPoints(dataJson); } ``` ### ✂️ 11.5.2 资源裁剪(Trimming) 在发布 Release 版本时,务必开启 IL Trimming。这会自动移除你应用中未使用的 .NET 库代码,将包体积减少 60% 以上。 ```xml <!-- 在 Wasm 项目文件 (.csproj) 中配置 --> <PropertyGroup Condition="'$(Configuration)'=='Release'"> <!-- 启用 IL 裁剪 --> <PublishTrimmed>true</PublishTrimmed> <!-- 设置裁剪粒度 --> <!-- partial: 只裁剪未被引用的程序集 --> <!-- full: 裁剪所有程序集(更激进的体积减小) --> <TrimMode>partial</TrimMode> <!-- 显示裁剪警告 --> <!-- 这有助于发现可能被错误裁剪的代码 --> <SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings> </PropertyGroup> ``` ### 🔀 11.5.3 多线程与 Web Workers Uno Platform 支持实验性的 Web Workers 多线程支持。你可以将繁重的后台计算任务移出 UI 线程,保持界面的流畅响应。 ```csharp // 注意:这是一个实验性功能,API 可能会变化 // 在使用前请检查最新的 Uno Platform 文档 public class BackgroundCalculation { public async Task<double[]> PerformHeavyCalculationAsync(int size) { #if __WASM__ // 在实际应用中,这里可能使用 Web Worker // 或者使用 Task.Run 将计算调度到后台线程 return await Task.Run(() => { var result = new double[size]; for (int i = 0; i < size; i++) { // 模拟繁重的计算 result[i] = CalculatePi(i); } return result; }); #else return await Task.Run(() => PerformCalculation(size)); #endif } private double CalculatePi(int iterations) { // 使用莱布尼茨公式计算 π double pi = 0; for (int i = 0; i < iterations; i++) { pi += Math.Pow(-1, i) / (2 * i + 1); } return pi * 4; } } ``` > **技术术语**:**IL Trimming(中间语言裁剪)** 是一种在编译时移除未使用代码的技术。.NET 应用程序会引用很多基础类库(如 System.dll),但通常只使用其中一小部分。裁剪器会分析整个应用程序的调用图,找出那些从未被调用的方法和类型,然后将它们从最终输出中删除。这就像是在搬家前清理不需要的物品——只带走真正有用的东西。 --- ## 📝 本章小结 深度集成 WebAssembly 是 Uno Platform 最具魅力的特性之一。通过本章的学习,你已经掌握了让 C# 代码在浏览器中飞翔的关键技能。 让我们回顾本章的核心要点: 第一,WebAssembly 的运行机制让我们能够在浏览器中以接近原生的速度执行 C# 代码。无论是解释器模式还是 AOT 模式,都有其适用的场景——前者适合快速开发和迭代,后者适合追求极致性能。 第二,`JSImport` 和 `JSExport` 是现代、高效的互操作范式。通过 Source Generators 在编译时生成桥接代码,我们既能享受类型安全的保障,又不需要牺牲运行时性能。 第三,对于简单的脚本执行,`InvokeJS` 方法仍然是快速便捷的选择。在选择互操作方案时,要根据实际需求的复杂度做出权衡。 第四,集成外部 JavaScript 库让 Uno 应用能够利用整个 JS 生态系统。通过"混合渲染"模式,我们可以在保持 WinUI 架构的同时,使用专业的 Web 组件。 第五,性能优化是 WASM 应用不可忽视的话题。减少互操作次数、开启资源裁剪、利用多线程,这些都是提升用户体验的重要手段。 在下一章中,我们将进入应用架构的核心议题——**状态管理与数据持久化策略**。无论应用运行在哪个平台上,如何高效地管理应用状态、如何可靠地持久化用户数据,都是决定应用质量的关键因素。 --- > **动手实验**: > 1. **浏览器信息探测器**:创建一个显示详细浏览器信息的应用,包括 User-Agent、屏幕分辨率、语言设置、网络状态等。使用 `InvokeJS` 快速获取这些信息,并以美观的方式展示。 > 2. **本地存储笔记**:使用 JavaScript 的 `localStorage` 实现一个简单的笔记应用。用户可以添加、编辑、删除笔记,所有数据都保存在浏览器的本地存储中。即使刷新页面,笔记也不会丢失。 > 3. **Chart.js 集成**:按照本章的指导,将 Chart.js 集成到你的 Uno 应用中。实现至少三种图表类型(柱状图、折线图、饼图)的切换,并支持动态更新数据。尝试从真实的 API 获取数据来填充图表。

讨论回复

0 条回复

还没有人回复,快来发表你的看法吧!