使用 Web/Razor 模板构建 Blazor Server 应用程序





5.00/5 (3投票s)
本文演示了如何从 ASPNetCore 模板构建 Blazor Server 应用程序。
在本文中,我们将从标准的 AspNetCore Razor Web 应用程序模板开始,逐个构建 Blazor Server 应用程序。
本文的目的是探讨 Blazor 和 Razor 应用程序之间的区别。我们将从开箱即用的 Razor ASPNetCore Web 应用程序模板开始,然后逐步构建运行 Blazor Server SPA 所需的基础结构。这应该能帮助您更快地理解 Blazor,而不是简单地部署模板并进行试用。
虽然我坚信 Visual Studio,但我们在此练习中使用 Visual Studio Code,以便更贴近实际操作。
必备组件
- Visual Studio Code
- NET 6.0 SDK
为简单起见,所有代码和组件都在一个名为 Blazr
的命名空间中。
代码仓库
您可以在 BlazrServer Github 存储库中找到所有代码。
构建项目
- 在“文档”中创建一个“Repos”文件夹(如果您还没有的话)。
- 创建一个“Repos/BlazorServer”文件夹。
- 在该文件夹上打开 Visual Studio Code。
- 按 Ctl + ' 打开终端。
现在,我们准备将一个模板项目部署到当前文件夹。但是选择哪一个呢?
PS C:\Users\shaun\source\repos\BlazrServer > dotnet new --list
获取已安装模板的列表。
我们正在寻找
ASP.NET Core Web App webapp,razor [C#] Web/MVC/Razor Pages
使用方法:
PS > dotnet new razor
然后我们得到
The template "ASP.NET Core Web App" was created successfully.
This template contains technologies from parties other than Microsoft,
see https://aka.ms/aspnetcore/6.0-third-party-notices for details.
Processing post-creation actions...
Running 'dotnet restore' on C:\Users\shaun\source\repos\BlazorServer\BlazorServer.csproj...
Determining projects to restore...
Restored C:\Users\shaun\source\repos\BlazorServer\BlazorServer.csproj (in 90 ms).
Restore succeeded.
并在目录中部署一组文件夹和文件。
此时,我们可以运行项目
PS > dotnet watch run debug
得到这个
PS C:\Users\shaun\source\repos\BlazorServer> dotnet watch run debug
watch : Started
Building...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://:7280
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://:5280
info: Microsoft.Hostingetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\Users\shaun\source\repos\BlazorServer\
和我们的网站。
要检查热重载,请更改 Index.cshtml 文件
<h1 class="display-4">Welcome To my Nascient Blazor App</h1>
并保存。我们得到
watch : Exited
watch : File changed: C:\Users\shaun\source\repos\BlazorServer\Pages\Index.cshtml
watch : Started
Building...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://:7280
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://:5280
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\Users\shaun\source\repos\BlazorServer\
并在页面上看到更改
热重载正在工作。我们拥有一个正在运行的 Razor Web 应用程序。
为了结束本节,让我们快速看一下 Program
。
// Initialize the WebApplication Builder
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddRazorPages();
// Build the App from the builder
var app = builder.Build();
// Configure the HTTP request pipeline
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
// Run the Application
app.Run();
这
- 创建
WebApplicationBuilder
类的实例。 - 将一组服务添加到 builder 的
ServiceCollection
中。这些服务定义了WebApplication
实例的依赖注入容器可以使用的服务。 - 构建
WebApplication
实例。 - 添加一组中间件来处理由
WebApplication
实例服务的 Web 请求管道。 - 运行配置好的
WebApplication
实例。
将 Blazor 组件添加到 Razor 页面
添加一个 Components 文件夹和一个 /Component/HelloBlazor.razor 组件文件。
它显示一条消息和时间:时间很有用,因为我们可以轻松地看到渲染事件何时发生。
@inherits ComponentBase
@namespace Blazr
<div>
<h1>Hello Blazor at @(time.ToLongTimeString())</h1>
<div class="m-3">
Todays Message is : @Message
</div>
<div class="m-3">
<button class="btn btn-primary" @onclick="GetTime">Set Time</button>
</div>
</div>
@code {
[Parameter] public string Message {get; set;} = string.Empty;
private DateTime time = DateTime.Now;
protected override void OnInitialized()
=> time = DateTime.Now;
private void GetTime()
=> time = DateTime.Now;
}
将 Component.cshtml 添加到 Pages。它使用服务器端 `Html.RenderComponentAsync
` 来渲染组件并加载 Blazor Server JavaScript 代码。
@page
@{
ViewData["Title"] = "Component page";
}
<div class="text-center">
<h1 class="display-4">Welcome To my Component Page</h1>
@(await Html.RenderComponentAsync<Components.HelloBlazor>
(RenderMode.ServerPrerendered, new { Message = "Hello there!" }))
<script src="_framework/blazor.server.js"></script>
</div>
在 _layout.cshtml 中,添加一个新的顶部菜单项,以便我们可以导航到新页面。
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Component">Component</a>
</li>
您现在应该能够导航到 Component 并看到页面渲染。单击按钮,......什么都没有发生。
组件已在服务器上渲染,但未配置 Blazor 服务。打开开发人员工具 <F12>,您将看到一个 JS 错误。
没有 _framework/blazor.server.js 可供下载。
配置服务器以运行 Blazor 服务
首先,我们添加 Blazor 服务器端服务。更新 Program
。 `AddServerSideBlazor
` 添加所有 Blazor 特定服务。
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
现在检查浏览器,您将看到两个错误。blazor.server.js 现在可以下载了,但它无法运行,因为服务器上没有 Blazor Hub 中间件来处理 SignalR 请求。
这需要在 Program
中配置 Blazor 中间件。
app.MapRazorPages();
app.MapBlazorHub();
现在一切都运行正常,没有错误。但是按钮点击不起作用:时间没有更新!
转到 HelloBlazor.razor。请注意,VS Code 在识别 @onclick
时遇到问题。
我们需要 Microsoft.AspNetCore.Components.Web
。
@inherits ComponentBase
@namespace Components
@using Microsoft.AspNetCore.Components.Web // New
现在按钮可以工作并更新时间了。
我们的 Razor 服务器端页面上运行着一个 Blazor 组件。老手们会感到似曾相识!
构建 Blazor SPA
在 Razor 页面中运行的组件不是单页应用程序。是吗?
在构建完整版本(如 Blazor 模板中的)之前,让我们构建一个非常简单的 SPA。
向项目根目录添加一个 _Imports.razor 文件,并添加以下代码。这为所有 razor 组件设置了全局程序集。
@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
向项目添加一个 Routes 文件夹,并添加以下 razor 组件
@namespace Blazr
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
/Routes/Counter.razor
@namespace Blazr
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
=> currentCount++;
}
/Routes/Hello.razor
@namespace Blazr
<HelloBlazor></HelloBlazor>
添加一个 Apps 文件夹并添加
/Apps/BaseApp.razor
@using Microsoft.AspNetCore.Components;
@using Microsoft.AspNetCore.Components.Rendering;
@using Microsoft.AspNetCore.Components.Web;
@namespace Blazr
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<h2 class="navbar-brand">Blazor Simple App</h2>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class=" btn nav-link"
@onclick='() => this.ChangeRootComponent("Index")'>Index</a>
</li>
<li class="nav-item">
<a class="btn nav-link"
@onclick='() => this.ChangeRootComponent("Counter")'>Counter</a>
</li>
<li class="nav-item">
<a class="btn nav-link"
@onclick='() => this.ChangeRootComponent("Hello")'>Hello</a>
</li>
<li class="nav-item">
<a class="btn nav-link "
@onclick="this.GoServerIndex">Server Home</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="ms-5">
@body
</div>
@code {
[Inject] private NavigationManager? NavManager { get; set; }
private Dictionary<string, Type> Routes => new Dictionary<string, Type> {
{"Index", typeof(Blazr.Index)},
{"Counter", typeof(Blazr.Counter)},
{"Hello", typeof(Blazr.Hello)}
};
private Type rootComponent = typeof(Blazr.Index);
private RenderFragment body => (RenderTreeBuilder builder) =>
{
builder.OpenComponent(0, rootComponent);
builder.CloseComponent();
};
public void ChangeRootComponent(string route)
{
if (Routes.ContainsKey(route))
{
rootComponent = Routes[route];
StateHasChanged();
}
}
public void GoServerIndex()
=> this.NavManager?.NavigateTo("/Index", true);
}
rootComponent
是要渲染的组件的 Type
:默认是 Index.razor
。NavBar 调用 ChangeRootComponent
,后者更改 rootComponent
并通过调用 StateHasChanged
来请求组件重新渲染。
body
是一个 RenderFragment
,它只是将 rootComponent
添加到渲染树并进行渲染。实际上,我们会检查 rootComponent
是否实现了 IComponent
:所有组件都必须实现 IComponent
。我没有实现代码,以保持简单易读。
GoHome
使用 NavigationManager
来触发完整的浏览器重新加载,从而加载默认的服务器页面。
将链接添加到 _Layout.cshtml
<li class="nav-item">
<a class="nav-link text-dark" asp-area=""
asp-page="/SimpleBlazor">Blazor Simple App</a>
</li>
添加 /Pages/SimpleBlazor.cshtml
@page
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - BlazorServer</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/BlazorServer.styles.css" asp-append-version="true" />
</head>
<body>
@(await Html.RenderComponentAsync<Blazr.BaseApp>(RenderMode.ServerPrerendered))
<script src="_framework/blazor.server.js"></script>
</body>
</html>
您现在应该能够导航到 Simple App,并在顶部菜单栏链接之间导航。
章节总结
我们创建了一个服务器端 razor 页面,它将 Blazor 组件加载为其主要内容。该组件由导航栏和子组件组成。单击导航栏中的链接只会更换子组件。StateHasChanged
会在 Renderer 的队列中排队等待页面重新渲染。Renderer 运行渲染(实际上是代表页面的 RenderFragment
),并找出旧 DOM 和新 DOM 之间的任何差异。它将差异传递给浏览器端的 Blazor JS 代码,后者更新浏览器显示的 DOM。不涉及页面导航,只有 DOM 更改。
构建完整的 Blazor Server 应用程序
从存储库添加文件
我们需要添加 Blazor 应用程序的一些文件。
添加一个 /Routes/Shared 文件夹,并从存储库中添加以下文件
- MainLayout.razor
- MainLayout.razor.css
- NavMenu.razor
- NavMenu.razor.css
这些是 Blazor 模板文件,已设置命名空间并调整了 NavLinks。
将以下文件添加到 wwwroot/css
- blazor-site.css
这是重命名的 Blazor 模板 CSS 文件:我们已经有一个 site.css。
App 组件
添加 /Apps/App.razor 并添加以下代码:这是标准代码。
@namespace Blazr
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
路由
为了使路由正常工作,我们需要向希望 Router
作为路由处理的组件添加路由 `page
` 属性。
更新以下组件,添加页面路由。一个组件可以有多个路由。
Routes/Index.razor
@page "/"
@page "/App"
......
Routes/Counter.razor
@page "/Counter"
......
Routes/Hello.razor
@page "/Hello"
......
Razor Server-Side Pages
添加 /Pages/Shared/_AppLayout.cshtml。
这是 Blazor Server 启动页面,经过调整的样式表设置。
@using Microsoft.AspNetCore.Components.Web
@namespace Layouts
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<!DOCTYPE html<span class="pl-kos">>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="~/" />
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link href="css/blazor-site.css" rel="stylesheet" />
<link href="BlazorServer.styles.css" rel="stylesheet" />
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
<body>
@RenderBody()
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
添加 /Pages/App.cshtml。
这是 Blazor 应用程序启动页面。Blazor.App
被指定为启动类,即 App.razor。
@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = "_AppLayout";
}
<component type="typeof(Blazr.App)" render-mode="ServerPrerendered" />
导航更改
更新 /Routes/Shared/NavMenu.razor。
添加一个新的 NavLink
。
<div class="nav-item px-3">
<NavLink class="btn nav-link" @onclick="GoServerIndex">
<span class="oi oi-list-rich" aria-hidden="true"> Server Home
</NavLink>
</div>
并添加 GoServerIndex
方法以“硬”导航到服务器端主页。
public void GoServerIndex()
=> this.NavManager?.NavigateTo("/Index", true);
Web 更改
在 /Pages/Shared/_Layout.cshtml 中为主页面导航添加一个新链接。
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/App">Blazor App</a>
</li>
向 Program
添加一个回退终结点。所有回退都指向 Blazor 应用程序。
//.....
app.MapBlazorHub();
app.MapFallbackToPage("/App");
app.Run();
您现在应该能够导航到应用程序并按 F5 重新加载它。
章节总结
我们现在拥有一个运行路由的完整的 Blazor Server 应用程序。区分 Blazor 路由和浏览器导航很重要。物理上检测差异的一种方法是观察工具栏中的“刷新”按钮 - 前进按钮旁边的圆圈。当发生浏览器导航事件时,您可以看到它被激活。
路由发生在您单击 Blazor 应用程序中的左侧导航菜单时。您可能单击的是一个 anchor
,但浏览器事件被 Blazor Javascript 代码拦截,并被 Router
组件接收。它有一个 Routes/Component 字典 - 通过查找当前程序集中所有带有 @page
属性的组件来构建。它根据路由查找组件,并加载新组件。我们在 Simple Blazor 组件中创建了一个非常简单的版本。
将 Blazor App 设置为默认
当前的设置有一个 Index.cshtml 页面,没有设置 @page
。这被视为网站的默认页面 https://:nnnnn/。
如果有一个 Blazor 路由组件被设置为 @page "/"
,为什么它不使用它呢?这就是将带有路由属性的 Blazor 组件称为“Pages
”会引起混淆的地方。称它们为任何东西,除了页面:Routes
、RouteComponents
或 RouteViews
。Web 服务器对这些路由一无所知。请求会通过 Program
中配置的中间件管道运行。在我们的设置中,app.MapRazorPages()
将 Pages 中的 Razor 页面映射到 Web 路由。如果找到 index 或默认 Web 文件,它就会使用它。
要理解正在发生的事情,请查看 Program
中的终结点映射。
app.MapRazorPages();
app.MapBlazorHub();
app.MapFallbackToPage("/App");
app.Run();
当前的 Index.cshtml 被视为默认页面,MapRazorPages()
返回它。
要更改我们的设置,请为 Index.cshtml 设置页面属性。
@page "/index"
@model IndexModel
MapRazorPages
现在将 Index.cshtml 映射到 https://:nnnnn/Index,不再将其视为默认页面。
请求命中 app.MapFallbackToPage("/App")
,它返回 App.cshtml,我们的 Blazor 应用程序启动页面。
Blazor 应用程序内的导航
那么,如果 Blazor 应用程序中的链接导航到 Web 服务器 Index 会发生什么?我们可以看到这个 NavMenu
。
如果我们像这样编码 GoServerIndex
public void GoServerIndex()
=> this.NavManager?.NavigateTo("/Index");
Blazor 将请求视为本地的,因此进行路由,而不是导航。路由器找不到匹配的路由,因此显示“抱歉,此地址无效。”消息。试试看!
要“硬”导航,我们需要这样做
public void GoServerIndex()
=> this.NavManager?.NavigateTo("/Index", true);
这会强制 NavigationManager
进行导航,重新加载页面,并命中 `Program` 中间件管道。
摘要
我做了一些调整,使我的实现与开箱即用的模板不同。这些是
- 我删除了
FetchData
,它只会使事情复杂化。 - App
NavMenu
指向 Index /App 而不是 /。 - Index.razor 添加了
@page "/App"
。 - 所有 Blazor Pages 组件现在都在 Routes 中。
2 和 3 解决了“默认页面问题”,即默认页面是服务器 Razor 文件,而不是 Blazor 应用程序。
历史
- 2022 年 1 月 6 日:初始版本