本章导读:想象一座横跨两个世界的桥梁——一边是严谨有序的 C# 世界,强类型、面向对象、编译时检查;另一边是灵活多变的 JavaScript 生态,动态类型、函数式编程、解释执行。WebAssembly 就是这座桥梁的地基,它让 C# 代码能够以接近原生的速度在浏览器中运行。而 Uno Platform 则是在这座地基上建造的豪华大厦,让你用同一套代码同时征服原生应用和 Web 世界。本章将带你深入这座桥梁的内部结构,揭开 C# 与 JavaScript 互操作的神秘面纱。
在正式开始互操作的探讨之前,我们需要先理解一个根本性的问题:为什么 C# 代码能够在浏览器中运行? 要回答这个问题,我们需要回到 2017 年,那是 WebAssembly 正式发布的年份。
WebAssembly(简称 WASM)是一种全新的二进制指令格式,它定义了一种可在现代浏览器中执行的"虚拟机器语言"。与 JavaScript 不同,WASM 不是解释执行的,而是被浏览器直接编译为机器码运行。这使得它的执行速度可以接近原生 C++ 代码——对于计算密集型任务,这简直是革命性的进步。
第一性原理:WebAssembly 的核心设计目标是"可移植、高效、安全"。可移植意味着一次编译,到处运行;高效意味着接近原生的执行速度;安全意味着在沙盒环境中运行,无法直接访问系统资源。这三个特性恰好也是 Uno Platform 追求的目标,难怪 WASM 成为了 Uno Web 版本的技术基础。
当你在浏览器中打开一个 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 界面就会出现在浏览器中。
// 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());
}
}
Uno Platform 的 WebAssembly 版本支持两种执行模式,它们各有优劣,适用于不同的场景。
解释器模式(Interpreter Mode)是默认选项。在这种模式下,.NET 运行时会在浏览器中解释执行你的 DLL 文件中的 IL(中间语言)代码。这种模式的优势在于下载体积小——因为只需要下载运行时和 DLL,不需要额外的编译产物。但代价是执行速度较慢,因为每一行代码都需要在运行时被解释执行。
原生 AOT 模式(Ahead-of-Time Compilation)则完全不同。在这种模式下,你的 C# 代码会在编译时被直接转换为 WASM 二进制指令,而不是 IL。这意味着浏览器不需要解释执行——它只需要直接运行这些预先编译好的机器指令。执行速度可以接近原生 C++ 应用,但代价是下载体积显著增加(可能达到 10-20 MB 或更多)。
<!-- 在 Wasm 项目文件中启用 AOT -->
<PropertyGroup>
<!-- 启用 AOT 编译 -->
<WasmShellEnableAOT>true</WasmShellEnableAOT>
<!-- 启用 AOT 后,可以选择进一步优化 -->
<!-- 这会增加编译时间,但能减小输出体积 -->
<WasmShellILLinker>true</WasmShellILLinker>
<!-- 设置 AOT 优化的级别 -->
<!-- 可选值:none, speed, size, balanced -->
<EmccCompileOptimizationFlag>O2</EmccCompileOptimizationFlag>
</PropertyGroup>
费曼技巧提问:如果你要向一位非技术人员解释"解释执行"和"AOT 编译"的区别,你会怎么说? 想象你在阅读一本外文书。"解释执行"就像是你一边读一边查字典,每个单词都需要翻译,阅读速度很慢;"AOT 编译"就像是你提前请人把整本书翻译成了中文,阅读时一气呵成,但前提是你愿意等待翻译完成,并且愿意接受这本"译本"更大的体积。
JSImport 与 JSExport在理解了 WASM 运行机制之后,我们终于可以进入本章的核心主题:C# 与 JavaScript 的互操作。.NET 7 引入了全新的、基于 Source Generators 的互操作方式,这是目前最高效的跨语言调用方案。
JSImportJSImport 属性让你可以在 C# 中声明一个外部 JavaScript 函数,然后像调用普通 C# 方法一样调用它。编译器会在编译时自动生成高效的桥接代码,避免了传统反射方式的开销。
首先,让我们在 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# 中声明对应的互操作方法:
// 文件位置: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);
}
}
使用这些互操作方法非常简单:
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
}
}
JSExport互操作是双向的——你不仅可以从 C# 调用 JavaScript,还可以将 C# 方法暴露给 JavaScript 调用。这在处理 Web 回调(如支付完成、第三方登录)时特别有用。
// 文件位置: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]属性时,编译器会在编译时自动分析这些声明,并生成高效的互操作代码。这与传统的反射方式相比,不仅消除了运行时开销,还能在编译时发现类型错误。
InvokeJS 快速调用虽然 JSImport 和 JSExport 是现代、高效的互操作方式,但在很多场景下,你只需要执行一行简单的 JavaScript 代码。这时候,Uno Platform 提供的 WebAssemblyRuntime.InvokeJS 方法依然是效率之选。
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
}
}
InvokeJS 只能返回字符串,这对于复杂的数据结构是一个限制。解决方案是使用 JSON 作为数据交换格式:
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需要预先声明和加载模块,对于只需要执行一行代码的场景来说,这种开销是不必要的。选择工具时,要考虑"投入产出比"——简单的任务用简单的工具,复杂的任务用复杂的工具。
Uno 应用并不是孤岛。JavaScript 生态系统中有大量成熟的、经过时间检验的库——图表库(如 Chart.js、D3.js)、地图库(如 Leaflet)、富文本编辑器(如 TinyMCE)等等。通过互操作,你可以在保持 WinUI 架构的同时,充分利用这些专业组件。
让我们通过一个完整的示例,展示如何在 Uno WASM 应用中集成 Chart.js。
第一步:添加 Chart.js 引用
在你的 WASM 项目的 wwwroot 文件夹中找到 index.html,添加 Chart.js 的 CDN 引用:
<!-- 文件位置: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 互操作代码
// 文件位置: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# 互操作服务
// 文件位置: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 中使用
<!-- 文件位置: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>
// 文件位置: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 导航和状态管理),而专家负责他那道拿手菜的烹饪。这种合作模式既保证了整体的一致性,又利用了各方的专长。
WebAssembly 虽然强大,但它在浏览器沙盒内运行,资源是受限的。本节将介绍几种关键的性能优化策略,帮助你的 Uno WASM 应用达到最佳性能。
跨越 C# 和 JavaScript 的边界是有开销的。每次调用都需要进行数据序列化、上下文切换和类型转换。因此,最佳实践是批量传递数据,而非在循环中频繁调用。
// ❌ 不好的做法:在循环中频繁调用互操作
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);
}
在发布 Release 版本时,务必开启 IL Trimming。这会自动移除你应用中未使用的 .NET 库代码,将包体积减少 60% 以上。
<!-- 在 Wasm 项目文件 (.csproj) 中配置 -->
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<!-- 启用 IL 裁剪 -->
<PublishTrimmed>true</PublishTrimmed>
<!-- 设置裁剪粒度 -->
<!-- partial: 只裁剪未被引用的程序集 -->
<!-- full: 裁剪所有程序集(更激进的体积减小) -->
<TrimMode>partial</TrimMode>
<!-- 显示裁剪警告 -->
<!-- 这有助于发现可能被错误裁剪的代码 -->
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
</PropertyGroup>
Uno Platform 支持实验性的 Web Workers 多线程支持。你可以将繁重的后台计算任务移出 UI 线程,保持界面的流畅响应。
// 注意:这是一个实验性功能,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 应用不可忽视的话题。减少互操作次数、开启资源裁剪、利用多线程,这些都是提升用户体验的重要手段。
在下一章中,我们将进入应用架构的核心议题——状态管理与数据持久化策略。无论应用运行在哪个平台上,如何高效地管理应用状态、如何可靠地持久化用户数据,都是决定应用质量的关键因素。
动手实验:
- 浏览器信息探测器:创建一个显示详细浏览器信息的应用,包括 User-Agent、屏幕分辨率、语言设置、网络状态等。使用
InvokeJS快速获取这些信息,并以美观的方式展示。- 本地存储笔记:使用 JavaScript 的
localStorage实现一个简单的笔记应用。用户可以添加、编辑、删除笔记,所有数据都保存在浏览器的本地存储中。即使刷新页面,笔记也不会丢失。- Chart.js 集成:按照本章的指导,将 Chart.js 集成到你的 Uno 应用中。实现至少三种图表类型(柱状图、折线图、饼图)的切换,并支持动态更新数据。尝试从真实的 API 获取数据来填充图表。
还没有人回复