文章目录
概述
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 项目
-
在Visual Studio中选择 Blazor WebAssembly 模板创建一个 .NET 10的 BlazorFileValidator 项目。
-
编写文件校验组件:在
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; } } -
注册为自定义元素
先在项目文件 (.csproj) 中添加对
Microsoft.AspNetCore.Components.CustomElements包的引用。接着修改Program.cs:csharpusing 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 标签一样使用它 -
构建并发布项目 :在 Visual Studio 中,右键点击项目 → 发布 → 选择 文件夹 → 选择发布路径(如 D:\demo\publish)→ 点击 发布 。
发布完成后,D:\demo\publish\wwwroot\ 目录结构如下:
textwwwroot/ ├── _framework/ ← Blazor 运行时核心文件 │ ├── blazor.webassembly.xxx.js ← xxx 是随机 hash │ ├── dotnet.xxx.js │ ├── dotnet.native.xxx.wasm │ └── BlazorFileValidator.xxx.wasm ├── css/ ← 应用样式 └── index.html ← Blazor 入口(参考用)
第二步:创建并配置 Vue 项目
-
创建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
-
配置 Vite
修改
vite.config.ts:typescriptimport { 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
-
复制 Blazor 发布文件中的 _framework文件夹 (包含 Blazor 的核心运行时)、css文件夹 (包含 Bootstrap 等样式)和 _content文件夹(包含样式等资源)到 Vue 项目。
-
在 Vue 的 main.ts 中引入 Bootstrap 样式
typescriptimport './assets/main.css' import { createApp } from 'vue' import App from './App.vue' // 👇 添加 Bootstrap 样式 import 'bootstrap/dist/css/bootstrap.min.css' createApp(App).mount('#app') -
修改 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/ 为根目录。 -
修改 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>
第四步:启动并测试
-
启动 Vue 开发服务器
bashcd D:\demo\vue-frontend npm run dev -
打开浏览器
访问
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 正从浏览器扩展到云端、边缘计算等更广泛场景,成为下一代跨平台计算基础设施的核心技术。