WebAssembly 学习与应用

文章目录

  • 概述
  • [实操案例:Vue + Blazor WebAssembly 文件校验器](#实操案例:Vue + Blazor WebAssembly 文件校验器)
    • 项目概述
    • [第一步:创建 Blazor WebAssembly 项目](#第一步:创建 Blazor WebAssembly 项目)
    • [第二步:创建并配置 Vue 项目](#第二步:创建并配置 Vue 项目)
    • [第三步:集成 Blazor 到 Vue](#第三步:集成 Blazor 到 Vue)
    • 第四步:启动并测试
    • 数据流向图
  • 总结

概述

WebAssembly(简称 Wasm)是一种紧凑的二进制指令格式,旨在让代码以接近原生速度在浏览器中运行。它最初是为 Web 浏览器设计的高性能计算方案,现在已发展成为一个可在多种环境中运行的安全、可移植的通用技术平台。

核心特性 说明
二进制格式 体积小、加载快,比 JavaScript 解析更快
接近原生性能 利用通用硬件能力,执行效率高
沙箱安全 在隔离的内存环境中运行,保证浏览器安全
多语言支持 C/C++、Rust、C#、Go 等均可编译为 Wasm
跨平台 浏览器 + 服务器 + 边缘计算 + 物联网

WebAssembly 与 JavaScript 的关系

WebAssembly 不是要替代 JavaScript,而是与之协同工作:

  • JavaScript:负责 UI 交互、DOM 操作、事件处理等"灵活"的部分
  • WebAssembly:负责计算密集型任务,如文件校验、图像处理、音视频编解码

#mermaid-svg-Z8Na4c4Dr6nBOdgr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Z8Na4c4Dr6nBOdgr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .error-icon{fill:#552222;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .marker.cross{stroke:#333333;}#mermaid-svg-Z8Na4c4Dr6nBOdgr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Z8Na4c4Dr6nBOdgr p{margin:0;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .cluster-label text{fill:#333;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .cluster-label span{color:#333;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .cluster-label span p{background-color:transparent;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .label text,#mermaid-svg-Z8Na4c4Dr6nBOdgr span{fill:#333;color:#333;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .node rect,#mermaid-svg-Z8Na4c4Dr6nBOdgr .node circle,#mermaid-svg-Z8Na4c4Dr6nBOdgr .node ellipse,#mermaid-svg-Z8Na4c4Dr6nBOdgr .node polygon,#mermaid-svg-Z8Na4c4Dr6nBOdgr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .rough-node .label text,#mermaid-svg-Z8Na4c4Dr6nBOdgr .node .label text,#mermaid-svg-Z8Na4c4Dr6nBOdgr .image-shape .label,#mermaid-svg-Z8Na4c4Dr6nBOdgr .icon-shape .label{text-anchor:middle;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .rough-node .label,#mermaid-svg-Z8Na4c4Dr6nBOdgr .node .label,#mermaid-svg-Z8Na4c4Dr6nBOdgr .image-shape .label,#mermaid-svg-Z8Na4c4Dr6nBOdgr .icon-shape .label{text-align:center;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .node.clickable{cursor:pointer;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .arrowheadPath{fill:#333333;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Z8Na4c4Dr6nBOdgr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z8Na4c4Dr6nBOdgr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Z8Na4c4Dr6nBOdgr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .cluster text{fill:#333;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .cluster span{color:#333;}#mermaid-svg-Z8Na4c4Dr6nBOdgr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Z8Na4c4Dr6nBOdgr rect.text{fill:none;stroke-width:0;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .icon-shape,#mermaid-svg-Z8Na4c4Dr6nBOdgr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .icon-shape p,#mermaid-svg-Z8Na4c4Dr6nBOdgr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .icon-shape .label rect,#mermaid-svg-Z8Na4c4Dr6nBOdgr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z8Na4c4Dr6nBOdgr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Z8Na4c4Dr6nBOdgr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Z8Na4c4Dr6nBOdgr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 浏览器
调用/回调
JavaScript

(UI 交互、DOM 操作)
WebAssembly

(高性能计算)

关键技术概念

概念 说明
WASI WebAssembly System Interface,让 Wasm 可以访问系统资源(文件、网络等)
自定义元素 将 Wasm 组件封装为原生 HTML 标签,可与任何前端框架集成
JS 互操作 通过 JSRuntime 和 JSInvokable 实现双向通信
AOT 编译 Ahead-Of-Time 编译,将代码预编译为机器码,提升运行性能

适用场景

场景 说明
文件处理 本地文件校验、压缩、加密解密
数据计算 复杂数据转换、分析、图表生成
图像/视频处理 Canvas 操作、滤镜、编解码
业务规则引擎 复杂规则计算、工作流引擎
遗留系统迁移 将现有 C#/C++ 代码移植到 Web

实操案例:Vue + Blazor WebAssembly 文件校验器

项目概述

本案例搭建了一个 Vue 前端 + Blazor WebAssembly 后端计算 的混合应用,实现一个文件校验器。核心功能:

  • Vue 负责 UI 交互和结果展示
  • Blazor(C#)负责文件哈希计算和校验逻辑
  • 两者通过桥梁脚本实现双向通信

最终效果 :用户在 Vue 页面中选择文件,Blazor 组件计算 SHA256 哈希并执行校验,结果回传 Vue 展示。

整体架构如下:
#mermaid-svg-0TDUTjmPj1edRGli{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0TDUTjmPj1edRGli .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0TDUTjmPj1edRGli .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0TDUTjmPj1edRGli .error-icon{fill:#552222;}#mermaid-svg-0TDUTjmPj1edRGli .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0TDUTjmPj1edRGli .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0TDUTjmPj1edRGli .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0TDUTjmPj1edRGli .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0TDUTjmPj1edRGli .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0TDUTjmPj1edRGli .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0TDUTjmPj1edRGli .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0TDUTjmPj1edRGli .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0TDUTjmPj1edRGli .marker.cross{stroke:#333333;}#mermaid-svg-0TDUTjmPj1edRGli svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0TDUTjmPj1edRGli p{margin:0;}#mermaid-svg-0TDUTjmPj1edRGli .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0TDUTjmPj1edRGli .cluster-label text{fill:#333;}#mermaid-svg-0TDUTjmPj1edRGli .cluster-label span{color:#333;}#mermaid-svg-0TDUTjmPj1edRGli .cluster-label span p{background-color:transparent;}#mermaid-svg-0TDUTjmPj1edRGli .label text,#mermaid-svg-0TDUTjmPj1edRGli span{fill:#333;color:#333;}#mermaid-svg-0TDUTjmPj1edRGli .node rect,#mermaid-svg-0TDUTjmPj1edRGli .node circle,#mermaid-svg-0TDUTjmPj1edRGli .node ellipse,#mermaid-svg-0TDUTjmPj1edRGli .node polygon,#mermaid-svg-0TDUTjmPj1edRGli .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0TDUTjmPj1edRGli .rough-node .label text,#mermaid-svg-0TDUTjmPj1edRGli .node .label text,#mermaid-svg-0TDUTjmPj1edRGli .image-shape .label,#mermaid-svg-0TDUTjmPj1edRGli .icon-shape .label{text-anchor:middle;}#mermaid-svg-0TDUTjmPj1edRGli .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0TDUTjmPj1edRGli .rough-node .label,#mermaid-svg-0TDUTjmPj1edRGli .node .label,#mermaid-svg-0TDUTjmPj1edRGli .image-shape .label,#mermaid-svg-0TDUTjmPj1edRGli .icon-shape .label{text-align:center;}#mermaid-svg-0TDUTjmPj1edRGli .node.clickable{cursor:pointer;}#mermaid-svg-0TDUTjmPj1edRGli .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0TDUTjmPj1edRGli .arrowheadPath{fill:#333333;}#mermaid-svg-0TDUTjmPj1edRGli .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0TDUTjmPj1edRGli .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0TDUTjmPj1edRGli .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0TDUTjmPj1edRGli .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0TDUTjmPj1edRGli .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0TDUTjmPj1edRGli .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0TDUTjmPj1edRGli .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0TDUTjmPj1edRGli .cluster text{fill:#333;}#mermaid-svg-0TDUTjmPj1edRGli .cluster span{color:#333;}#mermaid-svg-0TDUTjmPj1edRGli div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0TDUTjmPj1edRGli .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0TDUTjmPj1edRGli rect.text{fill:none;stroke-width:0;}#mermaid-svg-0TDUTjmPj1edRGli .icon-shape,#mermaid-svg-0TDUTjmPj1edRGli .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0TDUTjmPj1edRGli .icon-shape p,#mermaid-svg-0TDUTjmPj1edRGli .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0TDUTjmPj1edRGli .icon-shape .label rect,#mermaid-svg-0TDUTjmPj1edRGli .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0TDUTjmPj1edRGli .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0TDUTjmPj1edRGli .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0TDUTjmPj1edRGli :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 🌐 浏览器
invokeBlazorMethod()

调用方法
receiveValidationResult

回传结果
计算层
Blazor WebAssembly
• 文件选择 (InputFile)

• SHA256 哈希计算

• 自定义校验规则
用户交互层
Vue 前端
• 按钮点击

• 参数传递

• 结果展示
桥接层
桥梁脚本 (index.html)
• 注册 Blazor 组件实例

• 转发调用和结果

第一步:创建 Blazor WebAssembly 项目

  1. 在Visual Studio中选择 Blazor WebAssembly 模板创建一个 .NET 10的 BlazorFileValidator 项目。

  2. 编写文件校验组件:在 Pages/ 文件夹下,右键 → 添加 → Razor 组件,命名为 FileValidator.razor,完整代码如下:

    csharp 复制代码
    @inject IJSRuntime JSRuntime
    @using System.Text
    
    <div class="file-validator">
        <h3>📁 文件校验器</h3>
        <!-- 显示当前状态 -->
        <div class="status-bar">
            <span>当前校验模式:</span>
            <span class="badge">@CurrentMode</span>
        </div>
        <InputFile OnChange="@OnFileSelected" accept=".pdf,.jpg,.png,.xlsx" />
        @if (!string.IsNullOrEmpty(StatusMessage))
        {
            <div class="alert @(IsValid ? "alert-success" : "alert-danger")">
                @StatusMessage
            </div>
        }
        <!-- 显示参数信息 -->
        @if (!string.IsNullOrEmpty(ReceivedParameter))
        {
            <div class="alert alert-info">
                📌 接收到 Vue 参数:@ReceivedParameter
            </div>
        }
    </div>
    
    @code {
        private DotNetObjectReference<FileValidator>? objRef;
    
        // 注册组件实例,供桥梁脚本调用
        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                objRef = DotNetObjectReference.Create(this);
                await JSRuntime.InvokeVoidAsync("window.registerBlazorComponent", objRef);
            }
        }
    
        private string StatusMessage = string.Empty;
        private bool IsValid = false;
        public string CurrentMode { get; private set; } = "默认模式";
        public string ReceivedParameter { get; private set; } = string.Empty;
    
        private byte[]? LastFileBytes;
        private string? LastFileName;
    
        // ========== Vue 可调用的方法 ==========
    
        /// <summary>
        /// Vue 调用 - 无参数触发校验
        /// </summary>
        [JSInvokable]
        public async Task<string> TriggerValidation()
        {
            Console.WriteLine("📥 Vue 调用了 TriggerValidation 方法(无参数)");
    
            ReceivedParameter = "模式=无参数";
            StateHasChanged();
    
            if (LastFileBytes == null || string.IsNullOrEmpty(LastFileName))
            {
                var result = "❌ 请先选择文件!";
                await SendResultToVue(new FileValidationResult
                {
                    FileName = "无文件",
                    IsValid = false,
                    Message = result,
                    Error = "未选择文件"
                });
                return result;
            }
    
            var hash = ComputeHash(LastFileBytes);
            IsValid = ValidateFile(LastFileName, LastFileBytes.Length, hash);
            StatusMessage = IsValid
                ? $"✅ 文件 '{LastFileName}' 校验通过!"
                : $"❌ 文件 '{LastFileName}' 校验失败!";
    
            var resultObj = new FileValidationResult
            {
                FileName = LastFileName,
                FileSize = LastFileBytes.Length,
                IsValid = IsValid,
                Hash = hash,
                Message = StatusMessage
            };
    
            await SendResultToVue(resultObj);
            StateHasChanged();
    
            return StatusMessage;
        }
    
        /// <summary>
        /// Vue 调用 - 带参数触发校验
        /// </summary>
        [JSInvokable]
        public async Task<string> TriggerValidationWithParams(string mode, int threshold, string? customRule = null)
        {
            Console.WriteLine($"📥 Vue 调用了 TriggerValidationWithParams 方法");
            Console.WriteLine($"   - mode: {mode}");
            Console.WriteLine($"   - threshold: {threshold}");
            Console.WriteLine($"   - customRule: {customRule ?? "无"}");
    
            CurrentMode = mode;
            ReceivedParameter = $"模式={mode}, 阈值={threshold}, 规则={customRule ?? "默认"}";
            StateHasChanged();
    
            if (LastFileBytes == null || string.IsNullOrEmpty(LastFileName))
            {
                var result = "❌ 请先选择文件!";
                await SendResultToVue(new FileValidationResult
                {
                    FileName = "无文件",
                    IsValid = false,
                    Message = result,
                    Error = "未选择文件"
                });
                return result;
            }
    
            var hash = ComputeHash(LastFileBytes);
            IsValid = ValidateFileWithParams(LastFileName, LastFileBytes.Length, hash, mode, threshold);
    
            StatusMessage = IsValid
                ? $"✅ 文件 '{LastFileName}' 校验通过!(模式: {mode})"
                : $"❌ 文件 '{LastFileName}' 校验失败!(模式: {mode})";
    
            var resultObj = new FileValidationResult
            {
                FileName = LastFileName,
                FileSize = LastFileBytes.Length,
                IsValid = IsValid,
                Hash = hash,
                Message = StatusMessage
            };
    
            await SendResultToVue(resultObj);
            StateHasChanged();
    
            return StatusMessage;
        }
    
        /// <summary>
        /// Vue 调用 - 通过 Base64 传入文件数据
        /// </summary>
        [JSInvokable]
        public async Task<string> ValidateFileDataFromBase64(string base64Data, string fileName, string? rule = null)
        {
            Console.WriteLine($"📥 Vue 传入文件数据(Base64):{fileName}");
    
            ReceivedParameter = $"Vue 传入文件数据(Base64)";
            StateHasChanged();
    
            byte[] fileData;
            try
            {
                var base64 = base64Data;
                if (base64Data.Contains(","))
                {
                    base64 = base64Data.Substring(base64Data.IndexOf(",") + 1);
                }
                fileData = Convert.FromBase64String(base64);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"❌ Base64 解码失败: {ex.Message}");
                return $"❌ 数据解码失败: {ex.Message}";
            }
    
            Console.WriteLine($"   大小:{fileData.Length} 字节");
    
            LastFileBytes = fileData;
            LastFileName = fileName;
    
            var hash = ComputeHash(fileData);
            IsValid = ValidateFile(fileName, fileData.Length, hash);
            StatusMessage = IsValid
                ? $"✅ 文件 '{fileName}' 校验通过!{(rule != null ? $" 规则: {rule}" : "")}"
                : $"❌ 文件 '{fileName}' 校验失败!{(rule != null ? $" 规则: {rule}" : "")}";
    
            var resultObj = new FileValidationResult
            {
                FileName = fileName,
                FileSize = fileData.Length,
                IsValid = IsValid,
                Hash = hash,
                Message = StatusMessage
            };
    
            await SendResultToVue(resultObj);
            StateHasChanged();
    
            return StatusMessage;
        }
    
        // ========== 文件选择事件 ==========
    
        private async Task OnFileSelected(InputFileChangeEventArgs e)
        {
            try
            {
                var file = e.File;
                if (file == null) return;
    
                LastFileName = file.Name;
                ReceivedParameter = "用户选择了文件";
                StateHasChanged();
    
                using var memoryStream = new MemoryStream();
                await file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024)
                           .CopyToAsync(memoryStream);
    
                LastFileBytes = memoryStream.ToArray();
                var fileContentHash = ComputeHash(LastFileBytes);
    
                IsValid = ValidateFile(LastFileName, LastFileBytes.Length, fileContentHash);
                StatusMessage = IsValid
                    ? $"✅ 文件 '{LastFileName}' 校验通过!"
                    : $"❌ 文件 '{LastFileName}' 校验失败!";
    
                var result = new FileValidationResult
                {
                    FileName = LastFileName,
                    FileSize = LastFileBytes.Length,
                    IsValid = IsValid,
                    Hash = fileContentHash,
                    Message = StatusMessage
                };
    
                await SendResultToVue(result);
                StateHasChanged();
            }
            catch (Exception ex)
            {
                StatusMessage = $"❌ 处理文件时出错: {ex.Message}";
                IsValid = false;
    
                var result = new FileValidationResult
                {
                    FileName = e.File?.Name ?? "未知文件",
                    IsValid = false,
                    Message = StatusMessage,
                    Error = ex.Message
                };
    
                await SendResultToVue(result);
                StateHasChanged();
            }
        }
    
        // ========== 核心校验逻辑 ==========
    
        private bool ValidateFile(string fileName, long fileSize, string hash)
        {
            var allowedExtensions = new[] { ".pdf", ".jpg", ".png", ".xlsx" };
            var isValidExtension = allowedExtensions.Contains(Path.GetExtension(fileName).ToLower());
            var isValidSize = fileSize <= 5 * 1024 * 1024;
            var isValidContent = hash.StartsWith("a1b2c3");
            return isValidExtension && isValidSize && isValidContent;
        }
    
        private bool ValidateFileWithParams(string fileName, long fileSize, string hash, string mode, int threshold)
        {
            var baseResult = ValidateFile(fileName, fileSize, hash);
    
            if (mode == "strict")
            {
                return baseResult && fileSize <= 1 * 1024 * 1024;
            }
            else if (mode == "relaxed")
            {
                return baseResult && fileSize <= threshold * 1024 * 1024;
            }
    
            return baseResult;
        }
    
        private string ComputeHash(byte[] fileBytes)
        {
            using var sha256 = System.Security.Cryptography.SHA256.Create();
            var hashBytes = sha256.ComputeHash(fileBytes);
            return Convert.ToHexString(hashBytes).ToLower();
        }
    
        // ========== 数据回传 ==========
    
        private async Task SendResultToVue(FileValidationResult result)
        {
            try
            {
                await JSRuntime.InvokeVoidAsync("window.receiveValidationResult", result);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"发送结果到 Vue 失败: {ex.Message}");
            }
        }
    
        // ========== 数据定义与资源释放 ==========
    
        public class FileValidationResult
        {
            public string FileName { get; set; } = string.Empty;
            public long FileSize { get; set; }
            public bool IsValid { get; set; }
            public string Hash { get; set; } = string.Empty;
            public string Message { get; set; } = string.Empty;
            public string? Error { get; set; }
        }
    
        public async ValueTask DisposeAsync()
        {
            if (objRef is not null)
            {
                try
                {
                    // 1️⃣ 通知桥梁脚本:组件实例即将销毁
                    await JSRuntime.InvokeVoidAsync("window.registerBlazorComponent", null);
                }
                catch { }
                // 2️⃣ 释放 .NET 对象引用,防止内存泄漏
                objRef.Dispose();
            }
            await Task.CompletedTask;
        }
    }
  3. 注册为自定义元素

    先在项目文件 (.csproj) 中添加对Microsoft.AspNetCore.Components.CustomElements 包的引用。接着修改 Program.cs

    csharp 复制代码
    using Microsoft.AspNetCore.Components.Web;
    using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
    
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    
    // 👇 将 FileValidator 注册为自定义元素 <csharp-file-validator>
    builder.RootComponents.RegisterCustomElement<BlazorFileValidator.Pages.FileValidator>("csharp-file-validator");
    
    // 👇 注释掉默认的 App 根组件(避免渲染整个 Blazor 应用)
    // builder.RootComponents.Add<App>("#app");
    // builder.RootComponents.Add<HeadOutlet>("head::after");  // 可选保留
    
    builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
    
    await builder.Build().RunAsync();

    说明RegisterCustomElement 将 C# 组件转换为标准的 HTML 自定义标签 <csharp-file-validator>,Vue 可以像使用普通 HTML 标签一样使用它

  4. 构建并发布项目 :在 Visual Studio 中,右键点击项目 → 发布 → 选择 文件夹 → 选择发布路径(如 D:\demo\publish)→ 点击 发布

    发布完成后,D:\demo\publish\wwwroot\ 目录结构如下:

    text 复制代码
    wwwroot/
    ├── _framework/          ← Blazor 运行时核心文件
    │   ├── blazor.webassembly.xxx.js  ← xxx 是随机 hash
    │   ├── dotnet.xxx.js
    │   ├── dotnet.native.xxx.wasm
    │   └── BlazorFileValidator.xxx.wasm
    ├── css/                 ← 应用样式
    └── index.html           ← Blazor 入口(参考用)

第二步:创建并配置 Vue 项目

  1. 创建Vue项目

    打开 VS Code 终端,执行以下命令:

    bash 复制代码
    # 进入工作目录
    cd D:\demo
    
    # 使用 Vite 创建 Vue 项目
    npm create vue@latest vue-frontend
    
    # 进入项目目录
    cd vue-frontend
    
    # 安装依赖
    npm install
    
    # 安装 WebAssembly 支持插件
    npm install -D vite-plugin-wasm
    npm install bootstrap

    创建时选择配置建议:

    • TypeScript:Yes
    • 其他选项:No
  2. 配置 Vite

    修改 vite.config.ts

    typescript 复制代码
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import wasm from 'vite-plugin-wasm'
    
    export default defineConfig({
      plugins: [
        vue({
          template: {
            compilerOptions: {
              // 👇 告诉 Vue 忽略 Blazor 自定义元素
              isCustomElement: (tag) => tag.startsWith('csharp-'),
            },
          },
        }),
        wasm(),
      ],
      server: {
        headers: {
          // 👇 解决跨域问题,允许加载 Wasm
          'Cross-Origin-Opener-Policy': 'same-origin',
          'Cross-Origin-Embedder-Policy': 'require-corp',
        },
      },
    })

第三步:集成 Blazor 到 Vue

  1. 复制 Blazor 发布文件中的 _framework文件夹 (包含 Blazor 的核心运行时)、css文件夹 (包含 Bootstrap 等样式)和 _content文件夹(包含样式等资源)到 Vue 项目。

  2. 在 Vue 的 main.ts 中引入 Bootstrap 样式

    typescript 复制代码
    import './assets/main.css'
    
    import { createApp } from 'vue'
    import App from './App.vue'
    // 👇 添加 Bootstrap 样式
    import 'bootstrap/dist/css/bootstrap.min.css'
    
    createApp(App).mount('#app')
  3. 修改 Vue 的 index.html

    参考Blazor 的 index.html,将 Blazor 需要的样式引用和脚本引用,添加到 Vue 的 index.html 中。

    打开 Blazor 发布的 index.html,查看它的内容,通常会像这样:

    html 复制代码
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>BlazorFileValidator</title>
        <base href="/" />
        <link href="_framework/dotnet.6ssezgr9mq.js" rel="preload" as="script" fetchpriority="high" crossorigin="anonymous" integrity="sha256-Nrxa4ea1+Tt9EYAQmY/6wipDrUr8GgZgSR1ezFlJTY0=" />
        <link rel="stylesheet" href="css/app.css" />
        <!-- If you add any scoped CSS files, uncomment the following to load them
        <link href="BlazorFileValidator.styles.css" rel="stylesheet" /> -->
        <script type="importmap">{
      "imports": {
        "./_framework/blazor.webassembly.js": "./_framework/blazor.webassembly.958z1vx7fr.js",
        "./_framework/dotnet.js": "./_framework/dotnet.6ssezgr9mq.js",
        "./_framework/dotnet.native.js": "./_framework/dotnet.native.ikrs475e5v.js",
        "./_framework/dotnet.runtime.js": "./_framework/dotnet.runtime.a6jcqbs390.js"
      },
      "scopes": {},
      "integrity": {
        "./_framework/blazor.webassembly.958z1vx7fr.js": "sha256-A59Tr9HqEhHMu8kU7kruLI9ayw5MRS6Lvc6z7jCbghk=",
        "./_framework/blazor.webassembly.js": "sha256-A59Tr9HqEhHMu8kU7kruLI9ayw5MRS6Lvc6z7jCbghk=",
        "./_framework/dotnet.6ssezgr9mq.js": "sha256-Nrxa4ea1+Tt9EYAQmY/6wipDrUr8GgZgSR1ezFlJTY0=",
        "./_framework/dotnet.js": "sha256-Nrxa4ea1+Tt9EYAQmY/6wipDrUr8GgZgSR1ezFlJTY0=",
        "./_framework/dotnet.native.ikrs475e5v.js": "sha256-kkp5wX0htwkBcZt5WmEiKmhBqjqdCJtGc+koldfyoDQ=",
        "./_framework/dotnet.native.js": "sha256-kkp5wX0htwkBcZt5WmEiKmhBqjqdCJtGc+koldfyoDQ=",
        "./_framework/dotnet.runtime.a6jcqbs390.js": "sha256-7i3usfTrnzC/9qWO4si5Bw4w7D9fUSnSBdhQ47blX2M=",
        "./_framework/dotnet.runtime.js": "sha256-7i3usfTrnzC/9qWO4si5Bw4w7D9fUSnSBdhQ47blX2M="
      }
    }</script>
    </head>
    
    <body>
        <div id="app">
            <svg class="loading-progress">
                <circle r="40%" cx="50%" cy="50%" />
                <circle r="40%" cx="50%" cy="50%" />
            </svg>
            <div class="loading-progress-text"></div>
        </div>
    
        <div id="blazor-error-ui">
            An unhandled error has occurred.
            <a href="." class="reload">Reload</a>
            <span class="dismiss">ðŸ---™</span>
        </div>
        <script src="_framework/blazor.webassembly.958z1vx7fr.js"></script>
    </body>
    
    </html>

    Vue 的 index.html 修改后:

    html 复制代码
    <!DOCTYPE html>
    <html lang="">
      <head>
        <meta charset="UTF-8">
        <link rel="icon" href="/favicon.ico">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Vue + Blazor 应用</title>
        
        <!-- ======================================== -->
        <!-- 👇 预加载 .NET 运行时                    -->
        <!-- ======================================== -->
        <!-- 👇 从 Blazor 的 index.html 复制这些预加载 -->
        <link href="/_framework/dotnet.6ssezgr9mq.js" rel="preload" as="script" fetchpriority="high" crossorigin="anonymous" integrity="sha256-Nrxa4ea1+Tt9EYAQmY/6wipDrUr8GgZgSR1ezFlJTY0=" />
        <!-- 👇 从 Blazor 的 index.html 复制这些样式引用 -->
        <link rel="stylesheet" href="/css/app.css" />
    
        <!-- ======================================== -->
        <!-- 👇 Import Map:模块映射(必须!)        -->
        <!-- ======================================== -->
        <!-- 👇 从 Blazor 的 index.html 复制 Import Map -->
        <script type="importmap">{
        "imports": {
          "./_framework/blazor.webassembly.js": "./_framework/blazor.webassembly.958z1vx7fr.js",
          "./_framework/dotnet.js": "./_framework/dotnet.6ssezgr9mq.js",
          "./_framework/dotnet.native.js": "./_framework/dotnet.native.ikrs475e5v.js",
          "./_framework/dotnet.runtime.js": "./_framework/dotnet.runtime.a6jcqbs390.js"
        },
        "scopes": {},
        "integrity": {
          "./_framework/blazor.webassembly.958z1vx7fr.js": "sha256-A59Tr9HqEhHMu8kU7kruLI9ayw5MRS6Lvc6z7jCbghk=",
          "./_framework/blazor.webassembly.js": "sha256-A59Tr9HqEhHMu8kU7kruLI9ayw5MRS6Lvc6z7jCbghk=",
          "./_framework/dotnet.6ssezgr9mq.js": "sha256-Nrxa4ea1+Tt9EYAQmY/6wipDrUr8GgZgSR1ezFlJTY0=",
          "./_framework/dotnet.js": "sha256-Nrxa4ea1+Tt9EYAQmY/6wipDrUr8GgZgSR1ezFlJTY0=",
          "./_framework/dotnet.native.ikrs475e5v.js": "sha256-kkp5wX0htwkBcZt5WmEiKmhBqjqdCJtGc+koldfyoDQ=",
          "./_framework/dotnet.native.js": "sha256-kkp5wX0htwkBcZt5WmEiKmhBqjqdCJtGc+koldfyoDQ=",
          "./_framework/dotnet.runtime.a6jcqbs390.js": "sha256-7i3usfTrnzC/9qWO4si5Bw4w7D9fUSnSBdhQ47blX2M=",
          "./_framework/dotnet.runtime.js": "sha256-7i3usfTrnzC/9qWO4si5Bw4w7D9fUSnSBdhQ47blX2M="
        }
      }
        </script>
      </head>
      <body>
        <div id="app"></div>
    
      <!-- Blazor 错误 UI(必须保留) -->
        <div id="blazor-error-ui" style="display:none;">
          An unhandled error has occurred.
          <a href="." class="reload">Reload</a>
          <span class="dismiss">🗙</span>
        </div>
    
        <!-- ======================================== -->
        <!-- 👇 加载 Blazor WebAssembly               -->
        <!-- ======================================== -->
        <script src="_framework/blazor.webassembly.958z1vx7fr.js"></script>
    
        <!-- ======================================== -->
        <!-- 👇 桥梁脚本:连接 Vue 和 Blazor          -->
        <!-- ======================================== -->
        <script>
          // 存储 Blazor 组件实例
          let blazorComponentInstance = null;
          
          // Blazor 调用此函数注册实例
          window.registerBlazorComponent = function(instance) {
            blazorComponentInstance = instance;
            console.log('✅ Blazor 组件已注册到桥梁');
          };
          
          // Vue 通过此函数调用 Blazor 方法
          window.invokeBlazorMethod = async function(methodName, ...args) {
            console.log(`📤 Vue 调用 Blazor 方法: ${methodName}`, args);
            
            if (!blazorComponentInstance) {
              console.error('❌ Blazor 组件未注册');
              throw new Error('Blazor 组件未注册,请等待 Blazor 加载完成');
            }
            
            try {
              const result = await blazorComponentInstance.invokeMethodAsync(methodName, ...args);
              console.log(`📥 Blazor 方法返回:`, result);
              return result;
            } catch (error) {
              console.error(`❌ 调用 Blazor 方法失败:`, error);
              throw error;
            }
          };
          
          console.log('✅ 桥梁脚本已加载');
        </script>
    
        <!-- ======================================== -->
        <!-- 👇 Vue 入口脚本                         -->
        <!-- ======================================== -->
        <script type="module" src="/src/main.ts"></script>
      </body>
    </html>

    注意Blazor 的 index.html 中路径是 相对路径(如 css/app.css),但在 Vue 项目中需要改成 绝对路径(以 / 开头),因为 Vue 开发服务器以 public/ 为根目录

  4. 修改 App.vue

    src/App.vue 完整替换为:

    typescript 复制代码
    <script setup lang="ts">
    import { ref, onMounted, onUnmounted } from 'vue'
    
    interface FileValidationResult {
      fileName: string
      fileSize: number
      isValid: boolean
      hash: string
      message: string
      error?: string
    }
    
    const validationResult = ref<FileValidationResult | null>(null)
    const isCalling = ref(false)
    const blazorReady = ref(false)
    
    // 处理 Blazor 发来的校验结果
    const handleValidationResult = (event: CustomEvent<FileValidationResult>) => {
      console.log('📥 收到 Blazor 校验结果:', event.detail)
      validationResult.value = event.detail
      isCalling.value = false
    }
    
    // ========== Vue 调用 Blazor 方法 ==========
    
    // 无参数调用
    const callBlazorTrigger = async () => {
      if (!blazorReady.value) {
        console.error('❌ Blazor 组件未就绪')
        return
      }
      
      isCalling.value = true
      try {
        const result = await (window as any).invokeBlazorMethod('TriggerValidation')
        console.log('✅ 调用结果:', result)
      } catch (error) {
        console.error('❌ 调用失败:', error)
        isCalling.value = false
      }
    }
    
    // 带参数调用
    const callBlazorWithParams = async () => {
      if (!blazorReady.value) {
        console.error('❌ Blazor 组件未就绪')
        return
      }
      
      isCalling.value = true
      try {
        const result = await (window as any).invokeBlazorMethod(
          'TriggerValidationWithParams',
          'strict',           // mode
          2,                  // threshold
          'required-content'  // customRule
        )
        console.log('✅ 带参数调用结果:', result)
      } catch (error) {
        console.error('❌ 带参数调用失败:', error)
        isCalling.value = false
      }
    }
    
    // 传入文件数据(Base64 编码)
    const callBlazorWithFileData = async () => {
      if (!blazorReady.value) {
        console.error('❌ Blazor 组件未就绪')
        return
      }
      
      const mockFileData = new TextEncoder().encode('这是 Vue 传入的测试数据')
      const fileName = 'vue-generated-data.txt'
      const rule = 'test-rule'
      
      const base64Data = btoa(String.fromCharCode(...mockFileData))
      
      isCalling.value = true
      try {
        const result = await (window as any).invokeBlazorMethod(
          'ValidateFileDataFromBase64',
          base64Data,
          fileName,
          rule
        )
        console.log('✅ 传入文件数据调用结果:', result)
      } catch (error) {
        console.error('❌ 传入文件数据调用失败:', error)
        isCalling.value = false
      }
    }
    
    const formatFileSize = (bytes: number) => {
      if (bytes < 1024) return bytes + ' B'
      if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'
      return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
    }
    
    // ========== 生命周期钩子 ==========
    
    // 检查 Blazor 是否就绪
    const checkBlazorReady = async () => {
      let attempts = 0
      const maxAttempts = 30
      
      return new Promise((resolve) => {
        const interval = setInterval(() => {
          attempts++
          const hasInvokeMethod = typeof (window as any).invokeBlazorMethod === 'function'
          const hasComponent = (window as any).blazorComponentInstance !== null
          
          if (hasInvokeMethod && hasComponent) {
            clearInterval(interval)
            blazorReady.value = true
            console.log('✅ Blazor 已就绪')
            resolve(true)
          } else if (attempts >= maxAttempts) {
            clearInterval(interval)
            console.warn('⚠️ Blazor 未就绪,超时')
            blazorReady.value = true
            resolve(false)
          }
        }, 300)
      })
    }
    
    onMounted(async () => {
      // 注册全局函数(接收 Blazor 结果)
      ;(window as any).receiveValidationResult = function(result: FileValidationResult) {
        console.log('📥 Blazor 调用 receiveValidationResult:', result)
        const event = new CustomEvent('validation-complete', { detail: result })
        window.dispatchEvent(event)
      }
      
      window.addEventListener('validation-complete', handleValidationResult as EventListener)
      console.log('✅ Vue 已注册 validation-complete 事件监听')
      
      await checkBlazorReady()
    })
    
    onUnmounted(() => {
      window.removeEventListener('validation-complete', handleValidationResult as EventListener)
    })
    </script>
    
    <template>
      <main>
        <!-- 加载提示 -->
        <div v-if="!blazorReady" class="loading">
          ⏳ 加载 Blazor 组件中...
        </div>
        
        <!-- Blazor 自定义元素 -->
        <csharp-file-validator />
        
        <!-- Vue 控制按钮区 -->
        <div class="control-panel" v-if="blazorReady">
          <h3>🎮 Vue 控制 Blazor</h3>
          
          <div class="button-group">
            <button 
              @click="callBlazorTrigger" 
              :disabled="isCalling"
              class="btn btn-primary"
            >
              🔄 调用 Blazor 无参方法
            </button>
            
            <button 
              @click="callBlazorWithParams" 
              :disabled="isCalling"
              class="btn btn-warning"
            >
              📤 调用 Blazor 带参方法
            </button>
            
            <button 
              @click="callBlazorWithFileData" 
              :disabled="isCalling"
              class="btn btn-info"
            >
              📂 传入文件数据到 Blazor
            </button>
          </div>
        </div>
        
        <!-- 显示校验结果 -->
        <div v-if="validationResult" class="result-container">
          <h3>📊 Vue 接收到的校验结果</h3>
          <div class="result-card" :class="{ success: validationResult.isValid, fail: !validationResult.isValid }">
            <p><strong>文件名:</strong>{{ validationResult.fileName }}</p>
            <p><strong>文件大小:</strong>{{ formatFileSize(validationResult.fileSize) }}</p>
            <p><strong>校验状态:</strong>
              <span :class="validationResult.isValid ? 'text-success' : 'text-danger'">
                {{ validationResult.isValid ? '✅ 通过' : '❌ 失败' }}
              </span>
            </p>
            <p><strong>文件哈希:</strong><code>{{ validationResult.hash || '未计算' }}</code></p>
            <p><strong>消息:</strong>{{ validationResult.message }}</p>
            <p v-if="validationResult.error" class="text-danger">
              <strong>错误:</strong>{{ validationResult.error }}
            </p>
          </div>
        </div>
      </main>
    </template>
    
    <style scoped>
    .loading {
      padding: 20px;
      text-align: center;
      background: #f0f0f0;
      border-radius: 8px;
      margin: 10px 0;
    }
    
    .control-panel {
      margin: 20px 0;
      padding: 20px;
      border: 2px solid #007bff;
      border-radius: 8px;
      background-color: #f0f8ff;
    }
    
    .button-group {
      display: flex;
      gap: 10px;
      flex-wrap: wrap;
      margin-top: 10px;
    }
    
    .btn {
      padding: 10px 20px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
      color: white;
      transition: all 0.3s;
    }
    
    .btn:disabled {
      opacity: 0.6;
      cursor: not-allowed;
    }
    
    .btn-primary { background-color: #007bff; }
    .btn-warning { background-color: #ffc107; color: #212529; }
    .btn-info { background-color: #17a2b8; }
    
    .result-container {
      margin-top: 20px;
      padding: 20px;
      border: 2px solid #e0e0e0;
      border-radius: 8px;
      background-color: #f9f9f9;
    }
    
    .result-card {
      padding: 15px;
      border-radius: 6px;
      margin-bottom: 15px;
    }
    
    .result-card.success {
      background-color: #d4edda;
      border: 1px solid #c3e6cb;
    }
    
    .result-card.fail {
      background-color: #f8d7da;
      border: 1px solid #f5c6cb;
    }
    
    .text-success { color: #28a745; }
    .text-danger { color: #dc3545; }
    code {
      background: #eee;
      padding: 2px 6px;
      border-radius: 3px;
      font-size: 12px;
    }
    </style>

第四步:启动并测试

  1. 启动 Vue 开发服务器

    bash 复制代码
    cd D:\demo\vue-frontend
    npm run dev
  2. 打开浏览器

    访问 http://localhost:5173,应该能看到:

    此时可以点击按钮测试:

数据流向图

Blazor UI Blazor 桥梁脚本 Vue Blazor UI Blazor 桥梁脚本 Vue #mermaid-svg-4bgSGBTMWlCnELeR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4bgSGBTMWlCnELeR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4bgSGBTMWlCnELeR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4bgSGBTMWlCnELeR .error-icon{fill:#552222;}#mermaid-svg-4bgSGBTMWlCnELeR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4bgSGBTMWlCnELeR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4bgSGBTMWlCnELeR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4bgSGBTMWlCnELeR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4bgSGBTMWlCnELeR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4bgSGBTMWlCnELeR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4bgSGBTMWlCnELeR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4bgSGBTMWlCnELeR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4bgSGBTMWlCnELeR .marker.cross{stroke:#333333;}#mermaid-svg-4bgSGBTMWlCnELeR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4bgSGBTMWlCnELeR p{margin:0;}#mermaid-svg-4bgSGBTMWlCnELeR .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4bgSGBTMWlCnELeR text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-4bgSGBTMWlCnELeR .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4bgSGBTMWlCnELeR .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-4bgSGBTMWlCnELeR .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-4bgSGBTMWlCnELeR .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-4bgSGBTMWlCnELeR #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-4bgSGBTMWlCnELeR .sequenceNumber{fill:white;}#mermaid-svg-4bgSGBTMWlCnELeR #sequencenumber{fill:#333;}#mermaid-svg-4bgSGBTMWlCnELeR #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-4bgSGBTMWlCnELeR .messageText{fill:#333;stroke:none;}#mermaid-svg-4bgSGBTMWlCnELeR .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4bgSGBTMWlCnELeR .labelText,#mermaid-svg-4bgSGBTMWlCnELeR .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-4bgSGBTMWlCnELeR .loopText,#mermaid-svg-4bgSGBTMWlCnELeR .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-4bgSGBTMWlCnELeR .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4bgSGBTMWlCnELeR .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-4bgSGBTMWlCnELeR .noteText,#mermaid-svg-4bgSGBTMWlCnELeR .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-4bgSGBTMWlCnELeR .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4bgSGBTMWlCnELeR .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4bgSGBTMWlCnELeR .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4bgSGBTMWlCnELeR .actorPopupMenu{position:absolute;}#mermaid-svg-4bgSGBTMWlCnELeR .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-4bgSGBTMWlCnELeR .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4bgSGBTMWlCnELeR .actor-man circle,#mermaid-svg-4bgSGBTMWlCnELeR line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-4bgSGBTMWlCnELeR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户点击按钮 invokeBlazorMethod('TriggerValidation', params) invokeMethodAsync() 执行校验逻辑 StateHasChanged() UI 更新完成 receiveValidationResult(result) CustomEvent('validation-complete') 更新 UI 展示结果

总结

WebAssembly 是一种高效的二进制指令格式,允许 C#、C++、Rust 等语言编写的代码在浏览器中以接近原生的性能运行。它并非要替代 JavaScript,而是与之协同------JS 负责 UI 交互,Wasm 负责高性能计算。随着 WASI 等标准化接口的推进,WebAssembly 正从浏览器扩展到云端、边缘计算等更广泛场景,成为下一代跨平台计算基础设施的核心技术。