了解如何在 .Net Core 3.0 中构建您的第一个 Blazor WebAssembly 应用程序
在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris
第一次听到这个名字,我以为它听起来像火焰。这真是个奇妙的联想。Blazor 是微软在前端和后端全面使用 .Net 平台和 C# 语言的新方案。让我们在本文中了解如何使用它来构建应用程序。
在本文中,我们将介绍:
- 为什么选择 Blazor?这是另一个 Silverlight 插件吗?还是其他更好的插件?让我们来一探究竟。
- 什么?让我们来聊聊 Blazor 是什么,以及不同的托管模型如何让你在前端和后端托管 Blazor。这各有优缺点,我们来聊聊。最终的选择权在你手中。
- 演示,我们将共同构建一个 Blazor 演示,展示如何使用路由、组件和 HTTP
参考
您可以查看一些优秀的文档来了解更多信息:
为什么
好的,这是 Blazor 的销售宣传。
Blazor 承诺:
- 使用 C# 而不是 JavaScript创建丰富的交互式 UI。
- 共享用 .NET 编写的服务器端和客户端应用程序逻辑。
- 将UI呈现为 HTML 和 CSS,以获得广泛的浏览器支持,包括移动浏览器。
很好,所以您的意思是,作为 .Net 开发人员,我可能更喜欢一直编写 C#,而不是在 JavaScript 和 C# 之间进行大量上下文切换?
正是如此。此外,您将能够在其中一种托管模型中在后端和前端之间共享代码
分享代码,现在很有趣
这次没有插件。
那么不是新的 Silverlight?
不。
你说,而不是JavaScript,什么意思?
你将能够在任何地方编写 C# 代码。JavaScript 如今非常流行,但说实话,如果你经常使用 C#,那么学习一门新语言、新范式等等就像是一次上下文切换。你还可以完全重用代码。如果你能重用大量的逻辑和模型,并在前端和后端都使用它们,那不是很棒吗?
好的,你引起了我的注意:)
好的,我们继续。
它渲染 WebAssembly
WebAssembly,我想我听说过它,你能告诉我更多吗?
当然,
WebAssembly,也称为 WASM,不要与Wassim混淆;),顺便说一句,你应该关注他,优秀的 OSS 开发人员 :)
你关注了吗?关注了吗?太好了 :)
好了,回到 WASM,它是一个开放标准,为可执行程序定义了一种可移植的二进制代码格式,以及相应的文本汇编语言
等一下,浏览器中的汇编语言一定非常快吧?
是的,没错。这意味着你可以在浏览器中获得高性能的应用程序。
那么不再有 JavaScript 了?
嗯,不,它们注定要共存,这是一个很长的故事。
好的
什么
让我们尝试在这里讨论两个不同的主题:
- 架构,Blazor 如何工作,如何传达变更等等?让我们来直观地展示一些漂亮的图片。
- 托管模型,您可以在客户端和服务器端托管 Blazor,让我们尝试解释一下它们之间的区别
建筑学
首先要明确一点。Blazor 是一个客户端 Web UI 框架。它负责处理用户交互并在需要时渲染更新。与 Angular、React 和 Vue 类似,Blazor 的组件渲染在 HTML 页面上:
组件不会直接渲染到 DOM 中,而是渲染到 DOM 的虚拟/内存表示中,称为RenderTree
。当发生更改时,会计算diff并将其应用于 DOM,如下图所示:
托管模型
关于 Blazor,我们首先需要了解的是它可以托管在不同的地方。它可以像 ASP.NET Web Forms 应用一样托管在 IIS 中。Blazor 应用还可以通过以下方式之一托管:
- WebAssembly 上的浏览器中的客户端。
- ASP.NET Core 应用程序的服务器端。
客户端
Blazor WebAssembly 应用直接在基于 WebAssembly 的 .NET 运行时的浏览器中执行。Blazor WebAssembly 应用的功能与 Angular 或 React 等前端 JavaScript 框架类似。
根据您告诉我的有关 WebAssembly 的信息,听起来它真的很快?
是的。或者说,Blazor 仍在开发中,并且速度会越来越快。
还有什么?
您无需编写 JavaScript,只需编写 C# 即可。.NET 运行时会随应用程序程序集和任何所需依赖项一起下载。无需任何浏览器插件或扩展程序。
前端使用 C#,很棒 :) 并且没有插件,所以它不是下一个 Silverlight?
正确的。
这肯定有一个缺点,我肯定不能在浏览器中使用整个 .Net 平台?
嗯,您可以使用.Net 标准库,但当然,应用程序是在浏览器安全沙箱中执行的。
什么意思?
您无法访问文件系统或打开任意网络连接
哦,好的,是的,这很有道理。
还有其他问题吗?
是的,实际上,部署怎么样?
部署,可以使用GitHub Pages或者静态站点托管。
好的 :)
服务器端
在这种托管模型中,组件和渲染输出彼此解耦。因此,我们有两个不同的进程,一个负责组件,另一个负责 UI 更新。
在服务器端方法中,组件在服务器上运行,而不是像以前的托管模型那样在客户端运行。因此,要使更改能够到达服务器并进而到达组件,需要实时连接。之后,它与以前的托管模型的思路基本相同,我们渲染组件,生成 UI diff 并将其发送到浏览器,最终将其应用于 DOM。
我应该选择哪种托管模式?
现在我们提出的问题是正确的。让我们来探讨一下每种托管模式的优缺点。
客户端模型
优点
-
无需服务器端依赖,可独立运行,无需服务器
-
卸载服务器,客户端承担更多责任并完成所有工作
缺点
-
受浏览器限制,应用程序的功能受浏览器限制
-
需要支持 WebAssembly,你需要支持 WebAssembly 的硬件和软件
-
尺寸,应用程序的有效载荷更大,因此加载应用程序需要更长的时间
-
.Net 的限制、运行时和工具支持不如服务器端版本成熟
服务器端模型
优点
-
尺寸,尺寸更小,应用程序加载速度更快
-
工具/调试,它在服务器上运行,因此它充分利用现有的工具和调试
-
可以使用完整的.Net,可以使用任何与.Net Core兼容的.Net API
缺点
- 延迟,每次交互都是一次网络调用
- 不离线,如果客户端无法连接到服务器,应用程序将停止工作
- 可扩展性,服务器必须管理客户端连接和客户端状态
正如您上面所看到的,有不同的优点和缺点,您必须决定您可以接受什么。
演示
理论已经足够了,你想看一些代码吗?
是的,请
好的,我们将执行以下操作:
- 先决条件,让我们安装所需的库,例如 .Net Core 3.0 和构建 Blazor 应用程序所需的模板
- 组件,组件是一个核心概念,让我们学习如何定义组件以及如何使用输入和输出
- HTTP,让我们学习如何使用 HTTP
- 路由,一个应用程序几乎总是由多个页面组成,让我们学习如何设置路由并在页面之间导航
先决条件
由于 Blazor 不断更新,请确保您查看最新的安装说明:
您需要下载 .Net Core 3.0。请查看此链接,找到适合您操作系统的发行版。
此外,你还需要一些模板,以便搭建 Blazor 应用的框架。打开终端并输入
dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.0.0-preview9.19465.2
完成这些之后,我们将获得一些 Blazor 特定的模板,即blazorwasm
和blazorserver
。让我们像这样创建一个项目blazorwasm
:
dotnet new blazorwasm -o blazor-web
这应该为你搭建一堆文件,它看起来应该像这样:
现在我们有了一个项目,让我们首先编译它:
dotnet build
这不仅会编译它,还会隐式地获取所有需要的 NuGet 包。你应该看到类似这样的内容:
接下来让我们运行这个例子并看看我们得到了什么:
dotnet run
您应该在终端中看到以下打印:
接下来,我们去http://localhost:5000
太棒了,一切正常!:)
接下来让我们了解所有的构建模块。
成分
Blazor 是一个以组件为中心的框架。这意味着我们应用程序中的所有内容都是由组件组成的。
让我们试着弄清楚我们的应用程序是如何组成的。根组件被称为App
,可以在文件 中找到App.razor
。该文件的内容如下:
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
以上告诉我们两件事。
- 正常路由,正常路由由组件负责
Found
。它使用MainLayout
组件设置了一个DefaultLayout
- 未找到,组件
NotFound
处理任何未定义的路由并输出错误文本
主布局
接下来我们来看一下这个MainLayout
组件。它可以在下面找到Shared/MainLayout.razor
,如下所示:
@inherits LayoutComponentBase
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
<a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
</div>
<div class="content px-4">
@Body
</div>
</div>
上面告诉我们我们有两个部分sidebar
和main
。sidebar
使用组件呈现导航菜单NavMenu
并main
使用呈现内容@Body
。
索引组件
接下来,让我们通过查看一个非常简单的组件来尝试理解组件Index
,该组件存储在文件中Pages/Index.razor
:
@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
文件的顶部@page
告诉我们它处理的是哪个路由,在本例中/
是默认路由。然后是一段 HTML。
柜台
该组件可以在下方找到Pages/Counter.razor
,其外观如下:
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
int currentCount = 0;
void IncrementCount()
{
currentCount++;
}
}
这有点意思,因为它演示了如何显示值和设置方法。在顶部,我们可以看到它处理了路由/counter
。
currentCount
接下来,我们看看如何使用 Razor 指令输出变量@
。
之后,我们看看如何设置方法IncrementCount()
来处理按钮的点击:
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
最后,我们使用 Razor 指令@code
编写一些 C# 代码
@code {
int currentCount = 0;
void IncrementCount()
{
currentCount++;
}
}
正如您在上面看到的,我们声明了变量currentCount
和方法IncrementCount()
,看起来几乎可行,对吧?:)
组件通信
组件的意义在于能够将你的应用拆分成不同的组件,每个组件都有各自的职责。因此,让我们像这样将Counter
组件添加到我们的组件中:Index
<Counter />
保存并重新启动应用程序。你的浏览器现在应该如下所示:
组件输入
智能组件和哑组件之间通常存在区别。智能组件会保存状态,而哑组件只会渲染数据。哑组件的一个例子是ProductDetail
只列出所传入数据的组件。那么,我们该如何设置呢?让我们按照以下步骤操作:
- 在目录
ProductDetail.razor
下创建文件Pages
- 创建呈现标题和描述的内容
- 将组件放置
Index
在组件中
首先,让我们添加一些代码ProductDetail.razor
:
<h2>@title</h2>
<div>@description</div>
@code {
string title = "Product detail";
string description ="description";
}
接下来,我们将其放入Index
组件中<ProductDetail/>
,然后保存并启动我们的应用程序。它现在应该如下所示:
我们不应该依赖内部的静态数据而应该ProductDetail
使用input
属性。
那么我们该怎么做呢?
我们采取以下措施:
- 对名为的变量使用装饰器
Parameter
- 使变量
public
成为property
- 测试一下
在我们的ProductDetail
组件中,将其更改为以下内容:
<h2>@title</h2>
<div>@description</div>
@code {
[Parameter]
public string title { get; set; } = "Product detail";
[Parameter]
public string description { get; set; } ="description";
}
我们已经完成了步骤 1+2,现在让我们通过在组件中分配值来尝试一下Index
:
<ProductDetail title="Tomato" description="It's a red vegetable" />
浏览器中的结果如下:
有效:)
组件输出
组件输出是什么意思?我们指的是如何从子组件向父组件进行通信。答案是通过函数。也许我们应该回到我们这样做的场景。让我们来谈谈Todo 应用。Todo应用是新的hello world,是一个入门应用,我们可以在其中学习所选框架的一些基础知识。原因之一是它涵盖了很多很棒的概念,比如显示列表,而且当我们通常单击列表中的特定项目时,我们可能还希望从子组件向父组件进行通信。
可以像这样创建一个简单的列表组件:
// Todos.razor
<div>
<h2>Todos</h2>
@foreach (var todo in todos)
{
<div>@todo.title</div>
}
</div>
@code {
public class Todo {
public string title { get; set; }
}
List<Todo> todos = new List<Todo>(){ new Todo { title="clean" }, new Todo { title="shop" } };
}
太棒了,它展示了@foreach
助手组件。但是,它没有展示从子组件到父组件的组件通信。所以我们来为此创建一个新组件,并将其命名为Products
。我们定义如下:
// Products.razor
<h2>Products</h2>
You clicked: @message
@foreach (var item in products)
{
<ProductDetail OnClick="Clicked" title="@item.Title" description="@item.Description" />
}
@code {
string message = "";
List<Product> products = new List<Product>
{
new Product(){ Title = "book", Description = "a Book" },
new Product(){ Title = "DVD", Description = "a DVD" }
};
void Clicked(string name)
{
message = name;
}
public class Product
{
public string Title { get; set; }
public string Description { get; set; }
}
}
让我们放大看看有趣的部分:
@foreach (var item in products)
{
<ProductDetail OnClick="Clicked" title="@item.Title" description="@item.Description" />
}
我们正在渲染一些ProductDetail
实例,并分配属性OnClick
并赋予其值Clicked
,这是一个如下所示的函数:
void Clicked(string name)
{
message = name;
}
一个非常简单的函数,以string
作为输入参数。
然而,为了理解如何ProductDetail
与父级进行通信,我们需要查看组件定义:
// ProductDetail.razor
<div>
<span>@title</span>
<span>@description</span>
<button @onclick="@(() => OnClick.InvokeAsync(title))" >Click me</button>
</div>
@code {
[Parameter]
public string title { get; set; } = "Product detail";
[Parameter]
public string description { get; set; } ="description";
[Parameter]
public EventCallback<string> OnClick { get; set; }
}
这里有两部分,第一部分是参数的定义OnClick
,如下所示:
[Parameter]
public EventCallback<string> OnClick { get; set; }
看一下上面的类型EventCallback<string>
,这意味着在调用时它需要传递一个string
作为参数。
另一个难题是如何调用它?答案是:
<button @onclick="@(() => OnClick.InvokeAsync(title))" >Click me</button>
从上面我们看到,我们调用时会将组件的标题作为参数,这就是它的工作原理。如果我们愿意,也可以将一些更独特的参数传递给父组件,例如InvokeAsync()
,但我们需要使用这样的参数来扩展组件。string
id
处理 HTTP
好的,我们已经讲了很多,包括如何开始,布局如何工作,如何使用组件进行架构以及它们如何通信。还剩下什么?答案就是“很多”。在结束本文之前,我只想再展示一件事,那就是如何使用 HTTP。
为此,我们将创建一个组件Planets
。它将与外部端点通信,获取数据并将其转换为我们可以渲染的内容。让我们先看一下它的定义,然后解释一下具体实现:
@inject HttpClient Http
<div>
@if(request == null)
{
<div>Loading...</div>
}
else
{
<div>
@foreach (var planet in request.results)
{
<div>@planet.Name</div>
}
</div>
}
</div>
@code {
PlanetRequest request = null;
protected override async Task OnInitializedAsync()
{
request =
await Http.GetJsonAsync<PlanetRequest>("https://swapi.co/api/planets");
}
public class PlanetRequest {
public Planet [] results { get; set; }
}
public class Planet {
public string Name { get; set; }
}
}
有三件有趣的事情正在发生:
- 注入 HttpClient,这是一个帮助我们进行 HTTP 调用的服务
- 调用 HttpClient,我们使用该方法
GetJsonAsync()
从定义的端点获取 JSON - 创建结果类型,我们正在创建
PlanetRequest
基于传入的 JSON 结构的类型,该类型能够挑选出在属性上找到的相关数据results
注入 HttpClient
这是在页面顶部使用以下代码完成的:
@inject HttpClient Http
我们正在请求一个类型的实例HttpClient
并将其命名为Http
。
调用 HttpClient
这里我们主动获取数据。我们通过定义一个OnInitializedAsync()
保证在初始化时运行的生命周期方法来实现这一点。
protected override async Task OnInitializedAsync()
{
request =
await Http.GetJsonAsync<PlanetRequest>("https://swapi.co/api/planets");
}
我们可以看到上面我们将其声明为async
因为我们用来await
等待Task
解析我们的数据。
创建结果类型
最后,我们定义了一个结果类型PlanetRequest
,它模拟了传入的 JSON 的样子,其形状如下:
{
"results": []
}
概括
我们现在想讲的就这些,不然就写成一本书了。关于 Blazor 还有很多内容可以讲,但这应该是一个简洁明了的介绍,希望能传达出 Blazor 是一个面向组件的框架,它允许你使用 C# 编写,同时仍然使用你熟悉的 HTML 和 CSS 等元素。
Blazor改变了游戏规则,这只是一个开始。:)
文章来源:https://dev.to/dotnet/blazor-the-future-of-net-web-apps-a-first-look-m8