65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2022 年 1 月 6 日

CPOL

8分钟阅读

viewsIcon

13080

本文演示了如何从 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 存储库中找到所有代码。

构建项目

  1. 在“文档”中创建一个“Repos”文件夹(如果您还没有的话)。
  2. 创建一个“Repos/BlazorServer”文件夹。
  3. 在该文件夹上打开 Visual Studio Code。
  4. 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();

  1. 创建 WebApplicationBuilder 类的实例。
  2. 将一组服务添加到 builder 的 ServiceCollection 中。这些服务定义了 WebApplication 实例的依赖注入容器可以使用的服务。
  3. 构建 WebApplication 实例。
  4. 添加一组中间件来处理由 WebApplication 实例服务的 Web 请求管道。
  5. 运行配置好的 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”会引起混淆的地方。称它们为任何东西,除了页面:RoutesRouteComponentsRouteViews。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` 中间件管道。

摘要

我做了一些调整,使我的实现与开箱即用的模板不同。这些是

  1. 我删除了 FetchData,它只会使事情复杂化。
  2. App NavMenu 指向 Index /App 而不是 /
  3. Index.razor 添加了 @page "/App"
  4. 所有 Blazor Pages 组件现在都在 Routes 中。

2 和 3 解决了“默认页面问题”,即默认页面是服务器 Razor 文件,而不是 Blazor 应用程序。

历史

  • 2022 年 1 月 6 日:初始版本
© . All rights reserved.