温故知新,.Net Core遇见Blazor(FluentUI),属于未来的SPA框架

什么是Blazor

Blazor是一个使用.NET生成交互式客户端WebUI的框架:

  • 使用C#代替JavaScript来创建信息丰富的交互式UI。
  • 共享使用.NET编写的服务器端和客户端应用逻辑。
  • 将UI呈现为HTML和CSS,以支持众多浏览器,其中包括移动浏览器。
  • 与新式托管平台(如Docker)集成。

image

基于.Net Core提供客户端Web开发的优势

使用.NET进行客户端Web开发可提供以下优势:

  • 使用C#代替JavaScript来编写代码。
  • 利用现有的.NET库生态系统。
  • 在服务器和客户端之间共享应用逻辑。
  • 受益于.NET的性能、可靠性和安全性。
  • 在Windows、Linux和macOS上使用VisualStudio保持高效工作。
  • 以一组稳定、功能丰富且易用的通用语言、框架和工具为基础来进行生成。

Blazor组件

Blazor应用基于组件。Blazor中的组件是指UI元素,例如页面、对话框或数据输入窗体。

组件是内置到.NET程序集的.NET C#类,它们用于:

  • 定义灵活的UI呈现逻辑。
  • 处理用户事件。
  • 可以嵌套和重用。
  • 可作为Razor类库或NuGet包共享和分发。

组件类通常以Razor标记页(文件扩展名为.razor)的形式编写。Blazor中的组件有时被称为Razor组件。Razor是一种语法,用于将HTML标记与专为提高开发人员工作效率而设计的C#代码结合在一起。借助Razor,可使用Visual Studio中的IntelliSense编程支持在同一文件中的HTML标记与C#之间切换。Razor Pages和MVC也使用Razor。与基于请求/响应模型生成的Razor PagesMVC不同,组件专门用于处理客户端UI逻辑和构成。

Blazor使用UI构成的自然HTML标记。下面的Razor标记演示了一个组件(Dialog.razor),它显示一个对话框,并处理在用户选择按钮时发生的事件:

<div class="card" style="width:22rem">
    <div class="card-body">
        <h3 class="card-title">@Title</h3>
        <p class="card-text">@ChildContent</p>
        <button @onclick="OnYes">Yes!</button>
    </div>
</div>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public string Title { get; set; }

    private void OnYes()
    {
        Console.WriteLine("Write to the console in C#! 'Yes' button selected.");
    }
}

在上述示例中,OnYes是由按钮的onclick事件触发的C#方法。对话框的文本(ChildContent)和标题(Title)由在其UI中使用此组件的下述组件提供。

使用HTML标记将Dialog组件嵌入到另一个组件中。在以下示例中,Index组件(Pages/Index.razor)使用前面的Dialog组件。标记的Title属性向Dialog组件的Title属性传递标题的值。Dialog组件的文本(ChildContent)由<Dialog>元素的内容设置。向Index组件添加Dialog组件后,Visual Studio中的IntelliSense可加快使用语法和参数补全进行开发的速度。

@page "/"

<h1>Hello, world!</h1>

<p>
    Welcome to your new app.
</p>

<Dialog Title="Learn More">
    Do you want to <i>learn more</i> about Blazor?
</Dialog>

在浏览器中访问父级Index组件时,将呈现该对话框。当用户选择此按钮时,浏览器的开发人员工具控制台会显示由OnYes方法编写的消息:

image

组件呈现为浏览器文档对象模型(DOM)的内存中表现形式,它被称为“呈现树”,用于以灵活高效的方式更新UI。

文档对象模型 (DOM) 是HTML和XML文档的编程接口。它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。简言之,它会将web页面和脚本或程序语言连接起来。

一个web页面是一个文档。这个文档可以在浏览器窗口或作为HTML源码显示出来。但上述两个情况中都是同一份文档。文档对象模型(DOM)提供了对同一份文档的另一种表现,存储和操作的方式。 DOM是web页面的完全的面向对象表述,它能够使用如 JavaScript等脚本语言进行修改。

Blazor Web Assembly

Blazor Web Assembly单页应用(SPA)框架,用于使用.NET生成交互式客户端Web应用。Blazor Web Assembly使用无插件或将代码重新编译为其他语言的开放式Web标准。Blazor Web Assembly适用于所有新式Web浏览器,包括移动浏览器。

通过WebAssembly(缩写为wasm),可在Web浏览器内运行.NET代码。Web Assembly是针对快速下载和最大执行速度优化的压缩字节码格式。Web Assembly是开放的Web标准,支持用于无插件的Web浏览器。

Web Assembly代码可通过JavaScript(称为JavaScript互操作性,通常简称为JavaScript互操作或JS互操作)访问浏览器的完整功能。通过浏览器中的Web Assembly执行的.NET代码在浏览器的JavaScript沙盒中运行,沙盒提供的保护可防御客户端计算机上的恶意操作。

image

Blazor Web Assembly应用生成并在浏览器中运行时:

  • C#代码文件和Razor文件将被编译为.NET程序集。
  • 该程序集和.NET运行时将被下载到浏览器。
  • BlazorWebAssembly启动.NET运行时,并配置运行时,以为应用加载程序集。BlazorWebAssembly运行时使用JavaScript互操作来处理DOM操作和浏览器API调用。

已发布应用的大小(其有效负载大小)是应用可用性的关键性能因素。大型应用需要相对较长的时间才能下载到浏览器,这会损害用户体验。Blazor Web Assembly优化有效负载大小,以缩短下载时间:

  • 在中间语言(IL)裁边器发布应用时,会从应用删除未使用的代码。
  • 压缩HTTP响应。
  • .NET运行时和程序集缓存在浏览器中。

Blazor托管模型

主要的Blazor托管模型在Web Assembly上的浏览器中运行客户端。将Blazor应用、其依赖项以及.NET运行时下载到浏览器。应用将在浏览器线程中直接执行。UI更新和事件处理在同一进程中进行。应用资产作为静态文件部署到可为客户端提供静态内容的Web服务器或服务中。

image

如果创建了Blazor Web Assembly应用进行部署,但没有后端ASP.NET Core应用来为其文件提供服务,那么该应用被称为独立Blazor Web Assembly应用。如果创建了应用进行部署,但没有后端应用来为其文件提供服务,那么该应用被称为托管的Blazor Web Assembly应用。托管的Blazor Web Assembly Client应用通常使用WebAPI调用或SignalR(结合使用ASP.NET Core SignalRBlazor)通过网络与后端Server应用交互。

blazor.webassembly.js脚本由框架和句柄提供:

  • 下载.NET运行时、应用和应用依赖项。
  • 初始化运行应用的运行时。

Blazor Web Assembly托管模型具有以下优点:

  • 没有.NET服务器端依赖项。应用下载到客户端后即可正常运行。
  • 可充分利用客户端资源和功能。
  • 工作可从服务器转移到客户端。
  • 无需ASP.NETCoreWeb服务器即可托管应用。无服务器部署方案可行,例如通过内容分发网络(CDN)为应用提供服务的方案。

Blazor Web Assembly托管模型具有以下局限性:

  • 应用仅可使用浏览器功能。
  • 需要可用的客户端硬件和软件(例如WebAssembly支持)。
  • 下载项大小较大,应用加载耗时较长。
  • .NET运行时和工具支持不够完善。例如,.NETStandard支持和调试方面存在限制。

在传统Web应用和单页应用(SPA)之间选择

Atwood 定律:任何能够用JavaScript编写的应用程序,最终必将用JavaScript编写。- Jeff Atwood

目前可通过两种通用方法来构建Web应用程序:在服务器上执行大部分应用程序逻辑的传统Web应用程序,以及在Web浏览器中执行大部分用户界面逻辑的单页应用程序(SPA),后者主要使用WebAPI与Web服务器通信。也可以将两种方法混合使用,最简单的方法是在更大型的传统Web应用程序中托管一个或多个丰富SPA类子应用程序。

何时使用传统Web应用程序:

  • 应用程序的客户端要求简单,甚至要求只读。
  • 应用程序需在不支持JavaScript的浏览器中工作。
  • 团队不熟悉JavaScriptTypeScript开发技术。

何时使用SPA:

  • 应用程序必须公开具有许多功能的丰富用户界面。
  • 团队熟悉JavaScriptTypeScriptBlazor Web Assembly开发。
  • 应用程序已为其他(内部或公共)客户端公开API。

此外,SPA框架还需要更强的体系结构和安全专业知识。相较于传统Web应用程序,SPA框架需要进行频繁的更新和使用新框架,因此改动更大。相较于传统Web应用,SPA应用程序在配置自动化生成和部署过程以及利用部署选项(如容器)方面的难度更大。

使用SPA方法改进用户体验时必须权衡这些注意事项。

传统Web应用程序、SPA或Blazor应用之间进行选择时要考虑的一些基本因素

因素 传统 Web 应用 单页面应用程序 Blazor 应用
需要团队熟悉 JavaScript/TypeScript 最低 必需 最低
支持不带脚本的浏览器 支持 不支持 支持
客户端应用程序行为极少 适合 不必要 可行
丰富而复杂的用户界面要求 受限 适合 适合

何时选择传统Web应用

应用程序的客户端要求简单,可能要求只读

对许多Web应用程序而言,其大部分用户的主要使用方式是只读。只读(或以读取为主)应用程序往往比那些维护和操作大量状态的应用程序简单得多。例如,搜索引擎可能由一个带有文本框的入口点和用于显示搜索结果的第二页组成。匿名用户可以轻松提出请求,并且很少需要使用客户端逻辑。同样,一般而言,博客或内容管理系统中面向公众的应用程序主要包含的内容与客户端行为关系不大。此类应用程序容易构建为基于服务器的传统Web应用程序,在Web服务器上执行逻辑,并呈现要在浏览器中显示的HTML。事实上,网站的每个独特页面都有自己的URL,搜索引擎可以将其存为书签和编入索引(默认设置,无需将此功能添加为应用程序的单独功能),这也是此类情况的一个明显优势。

应用程序需在不支持JavaScript的浏览器中工作

如需在有限或不支持JavaScript的浏览器中工作的Web应用程序,则应使用传统的Web应用工作流编写(或至少可以回退到此类行为)。SPA需要客户端JavaScript才能正常工作;如果没有客户端JavaScript,SPA不是好的选择。

团队不熟悉JavaScript或TypeScript开发技术

如果团队不熟悉JavaScriptTypeScript,但熟悉服务器端Web应用程序开发,那相较于SPA,他们交付传统Web应用的速度可能更快。除非以学习SPA编程为目的,或需要SPA提供用户体验,否则对已经熟悉构建传统Web应用的团队而言,选择传统Web应用的工作效率更高。

何时选择SPA

应用程序必须公开具有许多功能的丰富用户界面

SPA可支持丰富客户端功能,当用户执行操作或在应用的各区域间导航时无需重新加载页面。SPA很少需要重新加载整个页面,因此加载速度更快,可在后台提取数据,并且对单个用户操作的响应更快。SPA支持增量更新,可保存尚未完成的窗体或文档,而无需用户单击按钮提交窗体。SPA支持丰富的客户端行为,例如拖放,比传统应用程序更容易操作。可以将SPA设计为在断开连接的模式下运行,对客户端模型进行更新,并在重新建立连接后将更新最终同步回服务器。如果应用要求包括丰富的功能,且超出了典型HTML窗体提供的功能,则选择SPA样式应用程序。

通常,SPA需要实现内置于传统Web应用中的功能,例如在反映当前操作的地址栏中显示有意义的URL(并允许用户将此URL存为书签或对其进行深层链接以便返回此URL)。SPA还应允许用户使用浏览器的后退和前进按钮寻找用户意料之中的结果。

团队熟悉JavaScript和/或TypeScript开发

编写SPA需要熟悉JavaScriptTypeScript以及客户端编程技术和库。团队应有能力像使用Angular一样使用SPA框架编写新式JavaScript

参考-SPA框架

应用程序已为其他(内部或公共)客户端公开API

如果已提供一个Web API供其他客户端使用,则相较于在服务器端窗体中复制逻辑,创建一个利用这些APISPA实现更加容易。用户与应用程序交互时,SPA广泛使用Web API来查询和更新数据。

何时选择Blazor

应用程序必须公开丰富用户界面

与基于JavaScript的SPA一样,Blazor应用程序可以支持丰富的客户端行为,而无需重载页面。这些应用程序对用户的响应更快,仅获取响应给定用户交互所需的数据(或HTML)。如果设计得当,可以将服务器端Blazor应用配置为以客户端Blazor应用的形式运行,只需在此功能受支持后对它进行稍加更改即可。

与使用JavaScript或TypeScript开发相比,团队更喜欢使用.NET开发

与使用JavaScriptTypeScript等客户端语言相比,许多使用.NET和Razor的开发人员的工作效率更高。由于已经使用.NET开发了应用程序的服务器端,因此,使用Blazor可以确保团队中的每名.NET开发人员都可以理解,并可能会生成应用程序前端的行为。

Blazor Server

Blazor将组件呈现逻辑从UI更新的应用方式中分离出来。Blazor ServerASP.NET Core应用中支持在服务器上托管Razor组件。可通过SignalR连接处理UI更新。

image

运行时停留在服务器上并处理:

  • 执行应用的C#代码。
  • 将UI事件从浏览器发送到服务器。
  • 将UI更新应用于服务器发送回的已呈现的组件。

Blazor Server用于与浏览器通信的连接还用于处理JavaScript互操作调用。

image

JavaScript互操作

对于需要第三方JavaScript库和访问浏览器API的应用,组件与JavaScript进行互操作。组件能够使用JavaScript能够使用的任何库或API。C#代码可调用到JavaScript代码,而JavaScript代码可调用到C#代码。

在ASP.NET Core Blazor中从.NET方法调用JavaScript函数

https://docs.microsoft.com/zh-cn/aspnet/core/blazor/call-javascript-from-dotnet?view=aspnetcore-5.0

从ASP.NET Core Blazor中的JavaScript函数调用.NET方法

https://docs.microsoft.com/zh-cn/aspnet/core/blazor/call-dotnet-from-javascript?view=aspnetcore-5.0

Blazor支持的平台

浏览者 Version
Apple Safari,包括 iOS 当前†
Google Chrome,包括 Android 当前†
Microsoft Edge 当前†
Mozilla Firefox 当前†

†最新指的是浏览器的最新版本。

Blazor模板选项

  • Blazor WebAssembly项目模板:blazorwasm
  • Blazor Server项目模板:blazorserver

Blazor生命周期方法

  • OnInitializedAsyncOnInitialized方法,执行代码来初始化组件。要执行异步操作,请在操作上使用OnInitializedAsyncawait关键字。

  • OnParametersSetAsyncOnParametersSet当组件已接收到的参数从其父和值被分配给属性被调用。这些方法在组件初始化后以及每次呈现组件时执行。

  • OnAfterRenderAsyncOnAfterRender在组件完成渲染后调用。此时填充元素和组件引用。使用此阶段使用呈现的内容执行其他初始化步骤,例如激活对呈现的DOM元素进行操作的第三方Java库。

创建我的第一个Blazor应用

https://github.com/CraigTaylor/BlazorApp

1. 先准备".NET SDK (Software Development Kit)"环境。

如果之前没有安装的先安装这个SDK:

dotnet-sdk-5.0.202-win-x64.exe

如果已经安装过了,检查下当前版本:

dotnet --version

image

2. 根据Blazor项目模板创建应用

1) 命令行方式创建

dotnet new blazorserver -o BlazorApp --no-https

这里通过DOTNET-CLI执行新建项目的命令,使用的是blazorserver这个项目模板,输出项目文件夹为BlazorApp,给该项目设置为不需要HTTPS模式--no-https

如果要创建Blazor WebAssembly项目,这里将blazorserver改成blazorwasm即可。

dotnet new blazorwasm -o BlazorApp --no-https

2) Visual Studio方式创建

打开Visual Studio 2019最新版本,创建新项目,找到Blazor相关的项目模板。

  • Blazor Server应用
  • Blazor WebAssembly应用

image

点击下一步,输入BlazorApp的项目名称,点击下一步进行创建。

image

目标框架,可以选.NET Core 3.1(长期支持),也可以选择.NET 5.0(当前),去掉勾选复选框配置HTTPS,点击创建即可。

image

创建成功之后,会自动打开项目解决方案。

image

3. 运行应用

1) 命令行方式创建

如果通过命令行方式创建,运行应用只需要执行:

dotnet watch run

通过DOTNET-CLIRun命令可以运行程序,这里添加watch参数的好处就是,等下如果改动了文件,会热重载,这样调试起来就很方便。

image

运行起来之后,会看到浏览器弹出应用首页。

image

如果要退出运行,只需要执行如下命令即可:

Ctrl + C

image

2) Visual Studio方式创建

Visual Studio里面运行就很简单了,直接点运行BlazorApp或者Ctrl + F5即可。

image

默认会打开一个终端控制台界面,用来显示Console日志。

image

运行起来之后,会看到浏览器弹出应用首页。

4. 项目结构

  • Program.cs 启用这个应用服务的入口节点。
  • Startup.cs 配置应用服务和中间件的地方。
  • App.razor 应用组件的根组件,也就是组件的入口位置。
  • BlazorApp/Pages 包含一些示例页面,也是页面目录。
  • BlazorApp.csproj 描述应用项目及其依赖关系。

5. 认识首页及组件

我们看到Pages/Index.razor这个主页。

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

这里已经有了SurveyPrompt这样一个组件,同时它有一个入参Title可以定制。

6. 查看子组件计数器

打开Pages/Counter.razor,这是一个计数器的组件。

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

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

我们可以从代码看到,顶部@page "/counter"实际上这个页面组件的目录,当我们切换到它时,路径也会变化。

image

每次点击Click me按钮的时候,就会从onclick事件去找到IncrementCount这个函数,在这个函数中,我们把currentCount做了加1,这时候,Current count的值会跟着刷新,可以看出只要绑定好了数据,它就会帮我们自动去更新界面显示了,还是很方便。

image

7. 在首页添加子组件计数器

打开Pages/Index.razor,我们在末尾追加Counter即可。

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

<Counter />

这时候,我们查看页面就已经可以看到它了。

image

8. 修改组件添加属性并使用

接下来,我们打开Pages/Counter.razor,我们给这个组件添加一个参数属性IncrementAmount,默认值是1, 这样我们可以从外部传递值进来修改它,并且我们让currentCount不再是继续加1了,而是改成加IncrementAmount的值,这样我们就可以控制每次累加的幅度。

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    [Parameter]
    public int IncrementAmount { get; set; } = 1;

    private void IncrementCount()
    {
        currentCount += IncrementAmount;
    }
}

然后,我们回到Pages/Index.razor,在Counter组件上给他传递IncrementAmount的值进来。

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

<Counter IncrementAmount="10" />

这时候,界面会自动重载,这时候,我们每次点击Click me,这个计时器组件就会加10了,哈哈,是不是很简单。

image

生成Blazor待办事项列表应用

https://github.com/CraigTaylor/TodoList

1. 先准备".NET SDK (Software Development Kit)"环境。

如果之前没有安装的先安装这个SDK:

dotnet-sdk-5.0.202-win-x64.exe

如果已经安装过了,检查下当前版本:

dotnet --version

image

2. 创建待办事项列表Blazor应用

dotnet new blazorserver -o TodoList --no-https

通过DOTNET-CLI脚本的New命令,新建一个blazorserver项目模板,输出目录命名为TodoList,同时不需要HTTPS的支持。

image

3. 添加名为“Todo”的Blazor组件

cd TodoList

image

先切换到TodoList项目文件夹中。

dotnet new razorcomponent -n Todo -o Pages

然后,通过DOTNET-CLI脚本的New命令,输出目录为Pages,新建名为Todorazorcomponent组件。

其中-n全称为--name,用来指定创建的名称,-o全称为--output,用来指定在这个目录进行创建。

Razor组件文件名要求首字母大写。打开Pages文件夹,确认Todo组件文件名以大写字母T开头。文件名应为Todo.razor

image

将在项目文件夹的Pages目录中,新建得到一个Todo.razor文件。

image

接下来,我们需要在Todo.razor文件顶部添加针对该文件组件的路由位置。

@page "/todo"

image

4. 将“Todo”的Blazor组件添加到左侧导航

位于Shared文件夹的NavMenu.razor组件是控制左侧导航的,我们需要将“Todo”的Blazor组件添加到左侧导航中。

NavMenu.razor组件中,找到ul元素下的li节点,参考前面的例子,新建属于Todo组件的导航位置。

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        ...
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="todo">
                <span class="oi oi-list-rich" aria-hidden="true"></span> ToDoList
            </NavLink>
        </li>
    </ul>
</div>

image

这里我们需要注意的几个关键点是,NavLinkhref需要填写组件的路由位置,其中span元素是菜单图标,紧跟着它的就是菜单名称,这里我们用ToDoList来作为菜单名称。

保存之后,我们通过执行run命令来运行,查看导航效果。

dotnet watch run

image

image

5. 添加“TodoItem”来存储待办事项数据。

这里一个小插曲就是为了更好的写出TodoItem.cs这个模型类,我遇到了两个不错的VSC插件,这里就顺带记录一下。

  • 名为C# Extensions的VSC插件,可以帮助我们支持在VSC中右键快速新建模型类和接口,有点类似VS中那种创建,至少帮你完成了当前命名空间的添加的活,所以值得推荐。

image

  • 名为C# XML Documentation Comments的VSC插件,平时我们在VS中写代码,习惯性的如果要添加函数或者属性的注释的话,上来就是///然后等待自动完成得到一串添加注释的注释体,这个插件就是来干这个事情的,效果还不错。

image

这两个插件,搜索C#这个关键词就可以看到了,基本上排在前面。

有了它们,我们创建TodoItem模型类就如虎添翼了。

在项目的Data文件夹中,我们右键,选择New C# Class,输入TodoItem,然后回车即可。

image

然后我们就自动完成,得到了一个基础的空的TodoItem.cs模型类,它的namespace都是已经处理好了,我觉得比手写强吧。

image

接下里,我们往TodoItem.cs模型类中添加两个属性(最好是走prop命令来新建),一个是Title来存储待办事项的标题,一个是IsDone来标记是否已完成该事项。

image

接下来,为了将来更好的阅读这个模型,我们养成一个好习惯,给对应的模型和字段都添加好注释,这时候C# XML Documentation Comments这个VSC插件就要派上用场了,通过在所需要注释代码的开头位置,输入///然后回车即可。

namespace TodoList.Data
{
    /// <summary>
    /// 待办事项
    /// </summary>
    public class TodoItem
    {
        /// <summary>
        /// 事项名称
        /// </summary>
        /// <value></value>
        public string Title { get; set; }

        /// <summary>
        /// 事项是否已完成
        /// </summary>
        /// <value></value>
        public bool IsDone { get; set; }
    }
}

image

6. 实现并完善“Todo”组件的逻辑。

有了上一步的TodoItem.cs模型类,我们算是准备好了承载待办事项业务的数据基础,接下来我们就可以基于它来实现并完善我们的Todo组件的核心逻辑了。

首先,我们新建一个TodoList来承载所有的待办事项,从业务上它应该是个TodoItem待办事项的数组,在Pages目录的Todo.razor文件的@code中,我们可以添加C#代码,我们添加一个TodoList,注意记得添加using使用。

@code {

    /// <summary>
    /// 待办事项列表
    /// </summary>
    /// <value></value>
    public List<TodoItem> TodoList { get; set; } = new List<TodoItem>();
}

image

然后,有了数据源TodoList,我们构建一个界面来承载这个数据源,这里最方便的就是ul-li组合了,在最外层ul中,我们针对TodoList做一个For-Each循环遍历,然后为每一项新建一个li来承载它,并且绑定每一项的Title,这样我们就可以看到这个元素了。

<ul>
    @foreach (var item in TodoList)
    {
        <li>@item.Title</li>
    }
</ul>

image

7. 添加对新增待办事项的支持。

有了前面的TodoList数据源和ul-li展示层,接下来我们来添加一些修改数据的支持了,首先我们要支持的就是往TodoList里面插入新数据。

为了插入新数据,那么我们需要在界面上放出一个输入框和一个提交按钮吧,所以接下来我们在ul-li的后面,添加下这个新增项组合。

<input placeholder="输入新待办事项标题" @bind="AddTitle" />
<button @onclick="AddToDoItem">新建</button>

image

我们用一个input元素来让用户输入新的待办事项,placeholder的内容是提示语:输入新待办事项标题,它的值我们通过@bind来做,我们绑定AddTitle这个变量的值。

并且,我们通过添加一个名为新建button元素来触发@onclick事件,这里让@onclick事件绑定AddToDoItem方法,这里留意,等添加完新项之后,我们清空下AddTitle的值,以便下次添加新项。

为了接收上诉输入数据和点击事件的绑定,我们也需要在@code中添加对应的支持,下面我们就添加对应的支持:

@code {

    ...

    /// <summary>
    /// 新增待办项标题
    /// </summary>
    /// <value></value>
    public string AddTitle { get; set; }

    /// <summary>
    /// 添加待办事项
    /// </summary>
    public void AddToDoItem()
    {
        if(!string.IsNullOrEmpty(AddTitle))
        {
            var todoItem = new TodoItem {
                Title = AddTitle
            };
            TodoList.Add(todoItem);
            AddTitle = string.Empty;
        }
    }
}

image

保存之后,会自动刷新界面,我们测试下,是否符合我们的预期。

image

测试后,我们看到,可以成功添加新项了。

image

8. 添加对修改待办事项完成状态的支持。

前面我们已经支持了展示待办事项和添加待办事项,那么最后的一环就是可以修改单个待办事项的完成状态了,也就是我们还没使用到TodoItemIsDone属性。

我们为每一个TodoItem项添加一个前置的复选框,以便于可以通过勾选的交互方式来改变它的完成状态。

<h3>Todo (@TodoList.Count(x=>!x.IsDone))</h3>

<ul>
    @foreach (var item in TodoList)
    {
        <li>
            <input type="checkbox" @bind="item.IsDone" />
            <span>@item.Title</span>
        </li>
    }
</ul>

我们在li中添加一个input元素,type设置成checkbox,我们用它来驱动待办事项的完成状态,所以我们需要给它绑定item.IsDone,不仅每一项的IsDone

同时,为了清晰看到我们是否真的改变了数据,我们在h3这个标题这里新增一个实时的统计数据,来展示目前还有多少未完成的待办事项。

image

结合ASP.NET Core SignalR和Blazor实现聊天室应用

https://github.com/CraigTaylor/BlazorServerSignalRApp

0. 什么是SignalR?

ASP.NET Core SignalR是一种开放源代码库,可简化将实时web功能添加到应用程序的功能。实时web功能使服务器端代码可以立即将内容推送到客户端。

适用于SignalR

  • 需要从服务器进行高频率更新的应用。示例包括游戏、社交网络、投票、拍卖、地图和GPS应用。
  • 仪表板和监视应用。示例包括公司仪表板、即时销售更新或旅行警报。
  • 协作应用。协作应用的示例包括白板应用和团队会议软件。
  • 需要通知的应用。社交网络、电子邮件、聊天、游戏、旅行警报和很多其他应用都需使用通知。

SignalR提供一个API,用于创建(RPC)的服务器到客户端远程过程调用。Rpc通过服务器端.NET Core代码从客户端调用JavaScript函数。

下面是的某些功能SignalR ASP.NET Core

  • 自动处理连接管理。
  • 将消息同时发送到所有连接的客户端。例如,聊天室。
  • 向特定客户端或客户端组发送消息。
  • 可缩放以处理不断增加的流量。

SignalR支持以下用于按正常回退)(处理实时通信的技术:

  • WebSockets
  • Server-Sent事件
  • 长轮询

SignalR自动选择服务器和客户端功能内的最佳传输方法。

1. 先准备".NET SDK (Software Development Kit)"环境。

如果之前没有安装的先安装这个SDK:

dotnet-sdk-5.0.202-win-x64.exe

如果已经安装过了,检查下当前版本:

dotnet --version

image

2. 创建Blazor Server应用

dotnet new blazorserver -o BlazorServerSignalRApp --no-https

通过DOTNET-CLI脚本的New命令,新建一个blazorserver项目模板,输出目录命名为BlazorServerSignalRApp,同时不需要HTTPS的支持。

image

3. 给项目添加"SignalR"客户端库

cd .\BlazorServerSignalRApp\
dotnet add package Microsoft.AspNetCore.SignalR.Client

image

先要切换到BlazorServerSignalRApp这个项目目录,然后通过dotnet add package $packageName来添加Microsoft.AspNetCore.SignalR.Client这个依赖包。

image

ASP.NET Core SignalR .Net客户端 - 通过ASP.NET Core SignalR .net客户端库,你可以SignalR从.net应用程序与中心进行通信。

4. 创建对话集线器Hub

在项目根目录,创建新目录Hubs,用来存放集线器。

mkdir Hubs

image

然后新建集线器,用来承载对话。

在vsc中,在Hubs文件夹上右键,选择New C# Class,新建一个名为ChatHub的集线器模型类。

image

接下来,我们添加对ChatHub的实现,需要引用System.Threading.TasksMicrosoft.AspNetCore.SignalR,添加一个SendMessgae的方法来支持发送消息业务,向所有的连接客户端都发送消息。

using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;

namespace BlazorServerSignalRApp.Hubs
{
    /// <summary>
    /// 对话集线器
    /// </summary>
    public class ChatHub : Hub
    {
        /// <summary>
        /// 发送消息
        /// </summary>
        /// <param name="user"></param>
        /// <param name="messgae"></param>
        /// <returns></returns>
        public async Task SendMessage(string user, string messgae)
        {
            // 向全部连接客户端发送指定消息
            await Clients.All.SendAsync("ReceiveMessage", user, messgae);
        }
    }
}

5. 为SignalR中心添加服务和终结点

前往Startup.cs中,在ConfigureServices函数中,配置我们需要的服务,这里主要是添加AddSignalR(SignalR服务)和AddResponseCompression(响应压缩中间件服务),这里需要添加下对Microsoft.AspNetCore.ResponseCompression的引用。

public void ConfigureServices(IServiceCollection services)
{
    // 添加SignalR服务
    services.AddSignalR();
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddSingleton<WeatherForecastService>();
    // 添加响应压缩中间件服务
    services.AddResponseCompression(opts =>
    {
        opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
        new[] { "application/octet-stream" });
    });
}

image

Configure函数中,在顶部添加UseResponseCompression,使用处理管道的配置顶部的“响应压缩中间件”。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // 使用响应压缩中间件
    app.UseResponseCompression();
}

Configure函数中,找到app.UseEndpoints添加新的终结点,在映射Blazor中心的终结点和客户端回退之间,为中心添加一个终结点。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapBlazorHub();
        // 添加对话集线器终结点
        endpoints.MapHub<ChatHub>("/chathub");
        endpoints.MapFallbackToPage("/_Host");
    });
}

image

6. 添加用于聊天的Razor组件代码

有了前面的基础,接下来,我们构建下聊天对话的前端组件代码。

dotnet new razorcomponent -n Chat -o Pages

然后,通过DOTNET-CLI脚本的New命令,输出目录为Pages,新建名为Chatrazorcomponent组件。

image

然后我们得到了Chat.razor这个组件,我们思考下聊天室这个业务场景里面,我们需要两个输入框和一个发送按钮来承载发送新消息的作用,同时我们还需要一个展示已收到消息的列表。

<h3>聊天室</h3>

<div class="form-group">
    <label>
        发送用户:
        <input @bind="User">
    </label>
</div>

<div class="form-group">
    <label>
        消息内容:
        <input @bind="Message">
    </label>
</div>

<button @onclick="SendMessage">发送</button>

<br/>

<ul>
    @foreach (var message in MessageList)
    {
        <li>@message</li>
    }
</ul>

@code {

    /// <summary>
    /// 发送用户
    /// </summary>
    /// <value></value>
    public string User { get; set; }

    /// <summary>
    /// 消息内容
    /// </summary>
    /// <value></value>
    public string Message { get; set; }

    /// <summary>
    /// 发送消息
    /// </summary>
    public void SendMessage(){

    }

    /// <summary>
    /// 消息列表
    /// </summary>
    /// <typeparam name="string"></typeparam>
    /// <returns></returns>
    public List<string> MessageList { get; set; } = new List<string>();
}

为了查看效果,我们添加Chat.razor组件到Index.razor页面。

@page "/"

<Chat />

image

通过dotnet watch run先看下效果。

image

7. 在Razor组件中连接聊天集线器

有了前面的界面基础,接下来我们在Chat.razor组件中先开始做连接集线器和接收消息的支持。

我们需要在组件启动的时候就开始指定连接,所以这里我们用到一个组件的函数OnInitializedAsync,我们通过重写实现它来连接集线器。

ComponentBase.OnInitializedAsync方法,当组件准备好启动时调用的方法,它从呈现树中的父级接收了其初始参数,需要支持异步的时候,优先使用OnInitializedAsync

与此同时,我们还需要新建一个HubConnection类型的hubConnection私有对象,来承载连接。

/// <summary>
/// 集线器连接
/// </summary>
/// <value></value>
private HubConnection hubConnection { get; set; }

为了查看到连接效果,我们可以设置一个连接状态IsConnectioned,来获取连接状态。

/// <summary>
/// 是否已连上集线器
/// </summary>
private bool IsConnectioned => 
    hubConnection != null ? 
    hubConnection.State == HubConnectionState.Connected : 
    false;

然后我们将IsConnectioned的值和发送按钮的可用状态进行绑定。

<button @onclick="SendMessage" disabled="@(!IsConnectioned)" >发送</button>

接下来,我们重写OnInitializedAsync来完成对集线器的定义和连接。

/// <summary>
/// 组件初始化完成事件
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
    // 定义聊天集线器(含终结点)
    hubConnection = new HubConnectionBuilder()
        .WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
        .WithAutomaticReconnect()
        .Build();

    // 开始连接集线器
    await hubConnection.StartAsync();
}

运行查看效果:

image

这里我们在HubConnection使用了WithAutomaticReconnect这个自动断开重连配置,在没有任何参数的情况下,会WithAutomaticReconnect()将客户端配置为分别等待0、2、10和30秒,然后再尝试重新连接尝试。

8. 在Razor组件中接收聊天集线器消息

Chat.razor组件的OnInitializedAsync中,我们顺带异步接收下集线器消息,这里注意的是,接收消息的方法名称必须和ChatHubSendAsync时定义的方法名称保持一致。

另外,需要注意的是,接收到新消息之后,我们添加到新消息到MessageList中之后,我们需要通过StateHasChanged来主动触发一次通知变更。

/// <summary>
/// 组件初始化完成事件
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
    // 定义聊天集线器(含终结点)
    hubConnection = new HubConnectionBuilder()
        .WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
        .WithAutomaticReconnect()
        .Build();

    // 接收集线器消息
    hubConnection.On<string, string>("ReceiveMessage", (user, message)=>
    {
        var messageItem = $"发送用户:{user},消息内容:{message}";
        MessageList.Add(messageItem);
        StateHasChanged();
    });

    // 开始连接集线器
    await hubConnection.StartAsync();
}

image

9. 在Razor组件中发送新聊天消息

之前在Chat.razor组件中,我们已经预留了一个SendMessage方法,接下来我们来完善它。

首先我们判断UserMessage是否填写,填写了之后,我们直接调用当前连接的SendAsync即可,这里需要注意的是,方法参数名称这里需要和ChatHub中函数名称保持一致。

/// <summary>
/// 发送消息
/// </summary>
public async Task SendMessage()
{
    // 判断输入是否合法
    if(!string.IsNullOrEmpty(User) && !string.IsNullOrEmpty(Message))
    {
        // 发送消息,方法名为"SendMessage",和"ChatHub"中方法名定义需一致。
        await hubConnection.SendAsync("SendMessage", User, Message);
    }
}

image

运行下,看下效果:

image

10. 退出Razor组件时关闭集线器连接

我们可以使用DisposeAsync方法来在退出组件时,主动关闭集线器连接,这里需要引用@implements IAsyncDisposable

@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager NavigationManager
@implements IAsyncDisposable
/// <summary>
/// 退出组件销毁时事件
/// </summary>
/// <returns></returns>
public async ValueTask DisposeAsync()
{
    // 关闭连接
    await hubConnection.DisposeAsync();
}

image

11. 跨多个客户端在聊天室测试群发消息

dotnet watch run

运行之后,我们把网址复制下,打开三个浏览器窗口,这时候,每一个窗口就是个独立的客户端,我们试着在一个窗口发送消息,发现这时候,三个窗口都可以顺利收到消息,说明群发成功。

image

我们可以试着在第二个、第三个聊天室发送消息,发现都可以顺利收到,哈哈,是不是很开心。

image

参考

posted @ 2021-05-05 00:47  TaylorShi  阅读(4262)  评论(3编辑  收藏  举报