您正在查看静态缓存页面 · 查看完整动态版本 · 登录 参与讨论

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

✨步子哥 (steper) 2026年02月17日 05:28 0 次浏览

第十一章:深度集成 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 界面就会出现在浏览器中。

// 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 或更多)。

<!-- 在 Wasm 项目文件中启用 AOT -->
<PropertyGroup>
    <!-- 启用 AOT 编译 -->
    <WasmShellEnableAOT>true</WasmShellEnableAOT>

    <!-- 启用 AOT 后,可以选择进一步优化 -->
    <!-- 这会增加编译时间,但能减小输出体积 -->
    <WasmShellILLinker>true</WasmShellILLinker>

    <!-- 设置 AOT 优化的级别 -->
    <!-- 可选值:none, speed, size, balanced -->
    <EmccCompileOptimizationFlag>O2</EmccCompileOptimizationFlag>
</PropertyGroup>
费曼技巧提问:如果你要向一位非技术人员解释"解释执行"和"AOT 编译"的区别,你会怎么说? 想象你在阅读一本外文书。"解释执行"就像是你一边读一边查字典,每个单词都需要翻译,阅读速度很慢;"AOT 编译"就像是你提前请人把整本书翻译成了中文,阅读时一气呵成,但前提是你愿意等待翻译完成,并且愿意接受这本"译本"更大的体积。

🔄 11.2 现代互操作范式:JSImportJSExport

在理解了 WASM 运行机制之后,我们终于可以进入本章的核心主题:C# 与 JavaScript 的互操作。.NET 7 引入了全新的、基于 Source Generators 的互操作方式,这是目前最高效的跨语言调用方案。

📤 11.2.1 从 C# 调用 JavaScript:JSImport

JSImport 属性让你可以在 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
    }
}

📥 11.2.2 从 JavaScript 调用 C#: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] 属性时,编译器会在编译时自动分析这些声明,并生成高效的互操作代码。这与传统的反射方式相比,不仅消除了运行时开销,还能在编译时发现类型错误。

⚡ 11.3 经典互操作:InvokeJS 快速调用

虽然 JSImportJSExport 是现代、高效的互操作方式,但在很多场景下,你只需要执行一行简单的 JavaScript 代码。这时候,Uno Platform 提供的 WebAssemblyRuntime.InvokeJS 方法依然是效率之选。

🎯 11.3.1 快速执行单行脚本

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 作为数据交换格式:

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
    }
}
第一性原理:为什么 InvokeJSJSImport 更适合简单场景?因为 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 引用:

<!-- 文件位置: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 导航和状态管理),而专家负责他那道拿手菜的烹饪。这种合作模式既保证了整体的一致性,又利用了各方的专长。

🚀 11.5 性能优化策略

WebAssembly 虽然强大,但它在浏览器沙盒内运行,资源是受限的。本节将介绍几种关键的性能优化策略,帮助你的 Uno WASM 应用达到最佳性能。

📦 11.5.1 减少互操作次数

跨越 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);
}

✂️ 11.5.2 资源裁剪(Trimming)

在发布 Release 版本时,务必开启 IL Trimming。这会自动移除你应用中未使用的 .NET 库代码,将包体积减少 60% 以上。

<!-- 在 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 线程,保持界面的流畅响应。

// 注意:这是一个实验性功能,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 模式,都有其适用的场景——前者适合快速开发和迭代,后者适合追求极致性能。

第二,JSImportJSExport 是现代、高效的互操作范式。通过 Source Generators 在编译时生成桥接代码,我们既能享受类型安全的保障,又不需要牺牲运行时性能。

第三,对于简单的脚本执行,InvokeJS 方法仍然是快速便捷的选择。在选择互操作方案时,要根据实际需求的复杂度做出权衡。

第四,集成外部 JavaScript 库让 Uno 应用能够利用整个 JS 生态系统。通过"混合渲染"模式,我们可以在保持 WinUI 架构的同时,使用专业的 Web 组件。

第五,性能优化是 WASM 应用不可忽视的话题。减少互操作次数、开启资源裁剪、利用多线程,这些都是提升用户体验的重要手段。

在下一章中,我们将进入应用架构的核心议题——状态管理与数据持久化策略。无论应用运行在哪个平台上,如何高效地管理应用状态、如何可靠地持久化用户数据,都是决定应用质量的关键因素。


动手实验
  1. 浏览器信息探测器:创建一个显示详细浏览器信息的应用,包括 User-Agent、屏幕分辨率、语言设置、网络状态等。使用 InvokeJS 快速获取这些信息,并以美观的方式展示。
  2. 本地存储笔记:使用 JavaScript 的 localStorage 实现一个简单的笔记应用。用户可以添加、编辑、删除笔记,所有数据都保存在浏览器的本地存储中。即使刷新页面,笔记也不会丢失。
  3. Chart.js 集成:按照本章的指导,将 Chart.js 集成到你的 Uno 应用中。实现至少三种图表类型(柱状图、折线图、饼图)的切换,并支持动态更新数据。尝试从真实的 API 获取数据来填充图表。

讨论回复

0 条回复

还没有人回复