MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#中的WebAssembly与Blazor应用

2021-12-053.3k 阅读

C# 中的 WebAssembly 与 Blazor 应用

一、WebAssembly 基础

WebAssembly(缩写为 Wasm)是一种新的字节码格式,旨在为 web 带来高性能的可移植代码。它最初是作为一种在 web 浏览器中运行 C 和 C++ 代码的方式而设计的,但现在已经支持多种编程语言,包括 C#。

WebAssembly 的设计目标主要有以下几点:

  1. 高性能:WebAssembly 被设计为接近原生性能。它的二进制格式可以被现代 CPU 高效地解码和执行,通过减少解释和编译的开销,使得在浏览器内运行复杂计算密集型任务成为可能,例如 3D 游戏、视频编辑等应用。
  2. 可移植性:它可以在不同的操作系统(如 Windows、MacOS、Linux 等)和不同的浏览器(Chrome、Firefox、Safari 等)上运行,提供了一种统一的运行环境,开发者无需针对不同平台编写大量特定代码。
  3. 安全性:WebAssembly 在沙箱环境中运行,限制了对底层系统资源的直接访问,防止恶意代码对用户系统造成损害。它与 JavaScript 共享相同的安全模型,浏览器可以对其进行严格的安全检查。

WebAssembly 代码通常由高级语言(如 C、C++、Rust、C# 等)编译生成。编译过程将高级语言代码转换为 WebAssembly 字节码(.wasm 文件)。在浏览器中,.wasm 文件会被下载并通过 JavaScript 加载。JavaScript 可以与 WebAssembly 模块进行交互,调用其函数并传递数据,同时 WebAssembly 也可以调用 JavaScript 函数,这种双向通信机制使得 WebAssembly 能够无缝集成到现有的 web 开发生态系统中。

二、C# 与 WebAssembly 的结合

在 C# 生态系统中,通过特定的工具和框架,可以将 C# 代码编译为 WebAssembly。这主要依赖于.NET 平台的相关技术,特别是.NET Core 和 Mono 项目。

  1. 工具链
    • dotnet-sdk:.NET 开发工具包是编译 C# 代码为 WebAssembly 的基础工具。它提供了一系列命令行工具,用于创建、构建和运行.NET 项目。例如,可以使用 dotnet new 命令创建一个新的 Blazor 项目,dotnet build 命令编译项目,dotnet run 命令运行项目。
    • Mono:Mono 是一个开源的跨平台.NET 实现,它在将 C# 代码编译为 WebAssembly 的过程中发挥了重要作用。Mono 提供了运行时环境和编译器,使得 C# 代码可以在非 Windows 平台(如 Linux、macOS)以及 web 浏览器中运行。
  2. 编译过程: 当将 C# 代码编译为 WebAssembly 时,首先会使用 Roslyn 编译器将 C# 代码编译为中间语言(IL)。然后,通过特定的工具(如 Mono 工具链中的工具)将 IL 进一步转换为 WebAssembly 字节码。这个过程涉及到对 IL 代码的分析、优化以及与 WebAssembly 指令集的映射。例如,IL 中的方法调用、变量声明等操作会被转换为对应的 WebAssembly 指令。 下面是一个简单的 C# 代码示例,展示一个基本的类和方法:
public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

在将这段代码编译为 WebAssembly 后,Add 方法会被转换为 WebAssembly 指令,在浏览器环境中可以被调用并执行加法运算。

三、Blazor 框架介绍

Blazor 是一个使用 C# 构建交互式 web UI 的框架。它允许开发者使用 C# 和 HTML 来创建单页应用(SPA)和服务器端渲染(SSR)应用,而无需编写大量 JavaScript 代码。Blazor 基于 WebAssembly 技术,为 C# 开发者提供了一种在 web 前端开发领域大展身手的途径。

  1. Blazor 的优势
    • 熟悉的语言:对于 C# 开发者来说,Blazor 使得他们可以在前端和后端都使用相同的语言进行开发。这减少了学习曲线,提高了开发效率,因为开发者无需在不同的语言(如 JavaScript 和 C#)之间频繁切换。
    • 组件化开发:Blazor 采用组件化的开发模式,类似于 React 或 Vue.js。开发者可以将 UI 拆分为多个可复用的组件,每个组件都有自己的逻辑和样式。这使得代码的维护和扩展更加容易,同时也提高了代码的可测试性。
    • 高性能:由于基于 WebAssembly,Blazor 应用可以在浏览器中以接近原生的性能运行。WebAssembly 的高效执行使得 Blazor 应用在处理复杂 UI 交互和计算任务时表现出色。
  2. Blazor 的类型
    • Blazor WebAssembly:也称为客户端 Blazor,在这种模式下,应用的所有代码(包括.NET 运行时、应用逻辑和 UI 组件)都被编译为 WebAssembly 并下载到浏览器中。应用在客户端运行,与服务器的交互通过 HTTP 进行。这种模式适用于创建高度交互式的单页应用,用户体验流畅,因为所有交互都在本地处理,无需频繁与服务器通信。
    • Blazor Server:在 Blazor Server 模式下,应用的主要逻辑在服务器端运行。浏览器通过 SignalR 与服务器建立实时连接,UI 交互事件通过该连接发送到服务器,服务器处理这些事件并更新 UI,然后将更新后的 UI 部分发送回浏览器。这种模式的优点是应用的初始加载速度快,因为只需要下载少量的 JavaScript 和 HTML 代码,同时服务器端的资源可以被充分利用。但它对网络连接的稳定性要求较高,因为所有交互都依赖于与服务器的实时连接。

四、创建 Blazor WebAssembly 应用

  1. 创建项目: 首先,确保你已经安装了.NET SDK。打开命令行,使用以下命令创建一个新的 Blazor WebAssembly 项目:
dotnet new blazorwasm -o MyBlazorApp
cd MyBlazorApp

这将创建一个名为 MyBlazorApp 的新项目,并进入该项目目录。 2. 项目结构

  • Pages 文件夹:存放应用的页面组件。每个页面通常是一个 Razor 组件文件(.razor 后缀),例如 Index.razor 是应用的首页。
  • Shared 文件夹:包含可在整个应用中共享的组件,如导航栏、页脚等组件。
  • wwwroot 文件夹:存放静态资源,如 CSS 文件、JavaScript 文件、图片等。
  • App.razor:这是应用的入口组件,它定义了应用的整体布局和路由配置。
  • Program.cs:在 Blazor WebAssembly 应用中,Program.cs 负责启动应用,配置服务等。例如:
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using MyBlazorApp;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace MyBlazorApp
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            await builder.Build().RunAsync();
        }
    }
}

这里,WebAssemblyHostBuilder.CreateDefault(args) 创建一个默认的 WebAssembly 主机生成器,builder.RootComponents.Add<App>("#app")App 组件添加到页面的 #app 元素中,builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }) 配置了一个 HTTP 客户端服务,用于与服务器进行通信。 3. 编写页面组件: 以 Index.razor 为例,它是应用的首页组件。下面是一个简单的 Index.razor 示例:

@page "/"
@using Microsoft.AspNetCore.Components.Web

<h1>Welcome to my Blazor App</h1>

<button @onclick="IncrementCount">Click me</button>
<p>Current count: @count</p>

@code {
    private int count = 0;

    private void IncrementCount()
    {
        count++;
    }
}

在这个示例中,@page "/" 定义了该组件是应用的首页路由。<h1> 标签显示标题,<button> 标签定义了一个按钮,@onclick="IncrementCount" 绑定了按钮的点击事件到 IncrementCount 方法。<p> 标签显示当前的计数,@count 是一个 Razor 表达式,用于在 HTML 中显示 C# 变量 count 的值。在 @code 块中,定义了 count 变量和 IncrementCount 方法,当按钮被点击时,count 变量的值会增加。

五、Blazor 组件与数据绑定

  1. 组件参数: Blazor 组件可以通过参数接收数据。例如,创建一个简单的 Greeting 组件,它接收一个 Name 参数并显示个性化的问候语。 首先,在 Shared 文件夹中创建 Greeting.razor 文件:
@using Microsoft.AspNetCore.Components

<h2>Hello, @Name!</h2>

@code {
    [Parameter]
    public string Name { get; set; }
}

然后在 Index.razor 中使用这个组件:

@page "/"
@using Microsoft.AspNetCore.Components.Web
@using MyBlazorApp.Shared

<h1>Welcome to my Blazor App</h1>

<Greeting Name="John" />

@code {
}

在这个例子中,Greeting 组件的 Name 参数通过 <Greeting Name="John" /> 进行赋值,Greeting 组件会显示 Hello, John!。 2. 双向数据绑定: 双向数据绑定允许在 UI 元素和 C# 变量之间自动同步数据。例如,创建一个简单的文本输入框,当输入内容时,C# 变量的值也随之改变,同时变量值的改变也会反映在输入框中。 在 Index.razor 中添加如下代码:

@page "/"
@using Microsoft.AspNetCore.Components.Web

<h1>Two - way Binding Example</h1>

<input type="text" @bind="userInput" />
<p>You entered: @userInput</p>

@code {
    private string userInput = "";
}

这里,@bind="userInput" 实现了双向数据绑定。当用户在输入框中输入内容时,userInput 变量的值会自动更新,同时 userInput 变量值的任何改变也会立即反映在输入框中。

六、Blazor 中的依赖注入

依赖注入(Dependency Injection,简称 DI)是一种设计模式,它允许将对象的创建和依赖关系的管理从对象本身分离出来。在 Blazor 应用中,依赖注入被广泛使用,以提高代码的可测试性和可维护性。

  1. 注册服务: 在 Program.cs 中,可以注册各种服务。例如,假设我们有一个简单的 IWeatherService 接口和它的实现 WeatherService
public interface IWeatherService
{
    string GetWeatherForecast();
}

public class WeatherService : IWeatherService
{
    public string GetWeatherForecast()
    {
        return "Sunny";
    }
}

Program.cs 中注册该服务:

using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using MyBlazorApp;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace MyBlazorApp
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
            builder.Services.AddScoped<IWeatherService, WeatherService>();

            await builder.Build().RunAsync();
        }
    }
}

这里,builder.Services.AddScoped<IWeatherService, WeatherService>()WeatherService 注册为 IWeatherService 的实现,作用域为 Scoped,表示在每个请求或组件生命周期内共享同一个实例。 2. 使用服务: 在组件中,可以通过构造函数注入来使用注册的服务。例如,在 Index.razor 中:

@page "/"
@using Microsoft.AspNetCore.Components.Web
@inject IWeatherService WeatherService

<h1>Weather Forecast</h1>
<p>The weather forecast is: @WeatherService.GetWeatherForecast()</p>

@code {
}

这里,@inject IWeatherService WeatherService 声明了要注入的 IWeatherService 服务,然后在组件中可以直接使用 WeatherService.GetWeatherForecast() 方法来获取天气预报信息。

七、Blazor 与 JavaScript 互操作

尽管 Blazor 允许使用 C# 构建前端应用,但在某些情况下,仍然需要与 JavaScript 进行交互,例如使用现有的 JavaScript 库或调用浏览器特定的 API。

  1. 从 Blazor 调用 JavaScript: 首先,在 wwwroot 文件夹中创建一个 JavaScript 文件,例如 interop.js
export function showAlert(message) {
    alert(message);
}

然后在 Blazor 组件中调用这个 JavaScript 函数。在 Index.razor 中:

@page "/"
@using Microsoft.AspNetCore.Components.Web
@inject IJSRuntime JSRuntime

<h1>Call JavaScript from Blazor</h1>
<button @onclick="CallJsFunction">Call JavaScript Alert</button>

@code {
    private async Task CallJsFunction()
    {
        await JSRuntime.InvokeVoidAsync("interop.showAlert", "Hello from Blazor!");
    }
}

这里,@inject IJSRuntime JSRuntime 注入了 IJSRuntime 接口,它提供了与 JavaScript 运行时交互的方法。await JSRuntime.InvokeVoidAsync("interop.showAlert", "Hello from Blazor!") 调用了 interop.js 中的 showAlert 函数,并传递了一个字符串参数。 2. 从 JavaScript 调用 Blazor: 假设我们有一个 Blazor 组件方法,需要从 JavaScript 调用。在 Index.razor 中添加如下代码:

@page "/"
@using Microsoft.AspNetCore.Components.Web
@inject IJSRuntime JSRuntime

<h1>Call Blazor from JavaScript</h1>

@code {
    [JSInvokable]
    public static string GetMessage()
    {
        return "Message from Blazor";
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JSRuntime.InvokeVoidAsync("import", "./interop.js");
        }
    }
}

interop.js 中添加如下代码:

import { DotNet } from '@microsoft/dotnet - blazor';

export async function callBlazorFunction() {
    const dotnetAssembly = await DotNet.load({
        assemblyName: 'MyBlazorApp',
        dotnetRoot: 'dotnet'
    });
    const obj = dotnetAssembly.getExport('MyBlazorApp.Index');
    const message = await obj.invokeMethodAsync('GetMessage');
    console.log(message);
}

这里,[JSInvokable] 特性标记了 GetMessage 方法,使其可以被 JavaScript 调用。OnAfterRenderAsync 方法在组件渲染后导入 interop.js 文件。在 JavaScript 中,通过 DotNet.load 加载 Blazor 程序集,然后获取 Index 组件的实例并调用 GetMessage 方法。

八、部署 Blazor WebAssembly 应用

  1. 构建应用: 在项目目录下,使用以下命令构建发布版本的应用:
dotnet publish -c Release -o publish

这将在 publish 文件夹中生成发布版本的文件,包括编译后的 WebAssembly 代码、静态资源等。 2. 部署到服务器: 可以将 publish 文件夹中的内容部署到任何支持静态文件托管的服务器上,如 IIS、Apache 或云服务提供商(如 Azure Static Web Apps、AWS S3 等)。

  • IIS 部署:在 IIS 中创建一个新的网站或应用程序,将 publish 文件夹的内容复制到该网站的物理路径下。确保 IIS 配置正确,能够处理静态文件和 WebAssembly 文件的请求。
  • Azure Static Web Apps 部署:首先,在 Azure 门户中创建一个静态 Web 应用资源。然后,使用 GitHub 或其他代码托管平台与 Azure 集成,将项目的发布版本自动部署到 Azure Static Web Apps。在部署过程中,Azure 会自动检测项目类型并进行相应的配置。

通过以上步骤,一个 Blazor WebAssembly 应用就可以成功部署并在浏览器中访问。

通过深入了解 C# 中的 WebAssembly 与 Blazor 应用,开发者可以利用 C# 的强大功能和丰富的生态系统,在 web 前端开发领域创建出高性能、可维护的应用程序。无论是小型的单页应用还是大型的企业级应用,Blazor 都提供了一种极具吸引力的开发方式。