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

Blazor Hydra - 在单个站点上托管多个 Blazor SPA

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (10投票s)

2020年11月24日

CPOL

12分钟阅读

viewsIcon

26409

如何将 Razor、Blazor WASM 和 Server SPA 整合到同一个框架下

引言

用一个新框架从零开始构建一个站点是很棒的,但并非我们都能获得这种奢侈。那些拥有经典 Razor/Server Page ASPNetCore 站点并希望进行迁移,但又无法进行“大爆炸式”迁移的人该怎么办?您希望大部分站点以经典模式运行,然后分小块逐步迁移部分内容。也许先在一小部分上进行试点。

本文将向您展示如何做到这一点。我将描述 Blazor Server、WASM 和 Razor 站点/应用程序如何交互,以及如何在单个 AspNetCore 网站上一起运行它们。

本文的前半部分将探讨技术挑战,深入研究 Blazor 的一些关键部分如何工作。在后半部分,我们将通过开箱即用的项目模板来演示一个简单的实际部署。

Hydra

代码仓库

Hydra 是本文所讨论概念的实现,代码将在讨论中得到广泛使用。

代码位于两个仓库中

  • CEC.Blazor.Examples 是主仓库。它包含了本文在内的多个文章的代码,并且是 Azure 上的演示站点 的源代码仓库。它使用本文讨论的方法在同一个网站上托管多个 WASM 和 Server SPA。Mongrel 是本文的特定站点。
  • Hydra 包含本文后半部分的代码。Hydra 的一个版本在演示站点上运行。

误解

Blazor Server 和 Blazor WASM 是应用程序:它们不是网站。在本文中,我将它们称为 SPA [单页应用程序]。Razor 站点是唯一的传统网站

Blazor SPA 中唯一的真正网页是启动页。启动后,它就是一个应用程序。它不进行页面帖子和获取。

Blazor 页面不是网页,它们是组件!

Web 服务器

查看代码仓库和测试站点,您会看到一个名为 Hydra.Web 的项目。这是托管 Web 站点。它是一个开箱即用的 AspNetCore Pages 模板网站。

让我们看看 Startup.cs

AspNetCore 由 IServiceCollection 定义了一个控制反转/依赖注入 Services 容器。如果您不确定什么是 IOC/DI 容器,请进行一些背景阅读。我们在 ConfigureServices 中配置它。

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddControllersWithViews();
    services.AddServerSideBlazor();
    services.AddServerSideHttpClient();
    services.AddSingleton<IWeatherForecastService, WeatherForecastService>();
    services.AddCECRouting();
}

您看到的是一组直接配置的服务,例如 WeatherForecastService,以及对 ServiceCollectionExtensions 的调用,如 AddServerSideBlazorAddCECRouting

为了揭开它们的神秘面纱,AddCECRouting 的代码如下所示

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddCECRouting(this IServiceCollection services)
    {
        services.AddScoped<RouterSessionService>();
        return services;
    }
}

ServiceCollectionExtensions 提供了一种将特定功能的所有服务集中在一起配置的方法。它们是 IServiceCollection 的扩展方法。AddServerSideBlazor 仅添加 Blazor Server 所需的所有服务,例如 NavigationManagerIJSRuntime

如果您想查看 ServiceCollectionExtension 的实现,AddServerSideHttpClient 定义在 CEC.Blazor.Hydra.Web/Extensions 中。

Configure 设置了 Web 服务器运行的中间件。我们将把它分解成几个部分。

第一部分是 ASPNetCore Web 服务器的标准代码。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this 
        // for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    app.UseHttpsRedirection();
    app.UseStaticFiles();

接下来,我们为每个 WASM SPA 定义一个 IApplicationBuilder.MapWhen。这里我展示了“red”的示例。这些定义了针对特定站点段(由 URL 定义)运行的中间件。这是 Red WASM。注意

  1. 我们使用特定 URL 段配置 UseBlazorFrameworkFiles - 我们将在下面的 WASM 部分中进一步讨论其重要性。框架文件路径 wwwroot/red/_framework/ 将是唯一的,为该 WASM SPA 提供唯一的路径。
  2. 该段的回退页面是 /_Red.cshtml
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/red"), app1 =>
{
    app1.UseBlazorFrameworkFiles("/red");
    app1.UseRouting();
    app1.UseEndpoints(endpoints =>
    {
        endpoints.MapFallbackToPage("/red/{*path:nonfile}", "/_Red");
    });
});

最后,我们定义默认段中间件。注意

  1. 我们定义 MapBlazorHub 将所有 SignalR 流量映射到 Blazor Hub。
  2. 我们为每个 Blazor Server SPA 定义一组特定于段的回退页面,以及 SPA 的特定启动页面。
    app.UseRouting();
    app.UseBlazorFrameworkFiles();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapBlazorHub();
        endpoints.MapRazorPages();
        endpoints.MapFallbackToPage("/examplesserver/{*path:nonfile}", "/_ExamplesServer");
        endpoints.MapFallbackToPage("/grey/{*path:nonfile}", "/_Grey");
        endpoints.MapFallbackToPage("/blue/{*path:nonfile}", "/_Blue");
        endpoints.MapFallbackToPage("/routing/{*path:nonfile}", "/_Routing");
        endpoints.MapFallbackToPage("/Index");
    });
}

Blazor WASM

每个 WASM SPA 都必须有一个单独的项目。<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> 声明项目为 WASM SPA,并指示编译器将项目构建为 WASM SPA。在 Program 中定义了一个单一的 RootComponent

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

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

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

您不必坚持使用 app。此声明定义了一个与 App 不同的类。它基本上表示在 idapp 的 HTML 标签中渲染 TComponent 类型的组件。不要被 RootComponentsAdd 所误导。声明一个。您可以声明更多,但它们都必须存在于渲染的页面上,所以相当没有意义:布局以更灵活的方式实现相同的结果。

多 SPA 托管的关键是在项目定义文件中指定一个唯一的 StaticWebAssetBasePath

<PropertyGroup>
  <StaticWebAssetBasePath>red</StaticWebAssetBasePath>
</PropertyGroup>

请记住,我们在 Startup.cs 的中间件映射中使用了 UseBlazorFrameworkFiles("/red")。这就是将一切联系起来的地方。这是区分大小写的!

Mongrel Red Project

上面的项目视图显示了项目中的所有文件。

  1. 布局和导航对于所有 Mongrel 项目都是通用的,因此已移至共享库。
  2. App.razor 已移至 Components 文件夹并重命名以使其唯一。我不喜欢到处都是 Apps
  3. wwwroot 已移除,因为启动 index.html 已移至 Hydra,CSS 也一样。

编译后,bin 目录如下所示

Mongrel Red Project

注意包含 blazor.webaseembly.jsblazor.boot.json_framework 目录。当 Hydra 引用此项目时,此目录将可用作 red/_framework。如果没有唯一的 StaticWebAssetBasePath,所有 SPA 都将被映射到 _framework

SPA 的“Startup”页面在 Hydra 上。_Red.cshtml

@page "/spared"
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}

<!DOCTYPE html<span class="pl-kos">></span>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, 
          maximum-scale=1.0, user-scalable=no" />
    <title>Mongrel.WASM.RED</title>
    <base href="/red/" />
    <link href="/css/site.css" rel="stylesheet" />
    <link href="/CEC.Blazor.Hydra.styles.css" rel="stylesheet" />
</head>
<body>
    <div id="app">
        <div class="mt-4" style="margin-right:auto; margin-left:auto; width:100%;">
            <div class="loader"></div>
            <div style="width:100%; text-align:center;"><h4>Web Application Loading</h4></div>
        </div>
    </div>

    <script src="/red/_framework/blazor.webassembly.js"></script>
</body>
</html>

注意

  1. base 设置为 "/red/"重要 - 您需要有前导和尾部斜杠。
  2. site.css 是一个 SASS 构建的 CSS 文件。
  3. CEC.Blazor.Hydra.styles.css 是系统 SASS 构建的 CSS,其中包含所有引用项目的组件样式。
  4. 我们定义了特定的 WebAssembly 脚本 src="/red/_framework/blazor.webassembly.js"。它在 {base}/_framework/blazor.boot.json 中查找其配置文件,其中 {base}<base href=""> 中定义。./blazor.boot.json 是加载 WASM 可执行文件的配置文件。重要的是要认识到 base 需要一个尾部斜杠才能使此功能正常工作。

最后一块拼图是 Hydra 的 Startup.cs 中的映射。中间件使用正确的 BlazorFrameworkFiles 进行配置,任何对 /red/ 的 GET 请求都将被定向到 /_Red.cshtml

SPA 启动后,任何导航到应用程序内已知 URL 的操作都由 SPA 路由。任何外部 URL 都是 Http GET 请求。在 Hydra 中,指向 /red/* 的 URL 将被定向到 /_Red.cshtml。因此,/red/counter 将在计数器页面上启动 SPA,而 /red/nopage 将加载 SPA,但您会看到“未找到此地址”的消息。

您需要对“/”路由稍微小心一些。我更喜欢将 SPA 的主页设置为带有“/index”的路由,并始终以此方式引用它。

Blazor Server

Blazor Server SPA 的配置方式略有不同。我们不需要一个具有单一入口点的 Program.cs。我们将启动页面配置为加载任何实现 IComponent 的类,因此我们的入口点可以来自任何地方。为了代码和路由管理,为每个 SPA 定义一个 Razor Library 项目:保持一切都分隔开。

我们在上面的讨论中已经看到了主站点的 Startup.cs 配置,我将其中的 Blazor 部分也包含进去了。它

  1. ConfigureServices 中通过调用 services.AddServerSideBlazor() 来添加特定的 Blazor 服务。
  2. Configure 中通过调用 endpoints.MapBlazorHub() 添加 Blazor Hub 中间件。

这里要理解的关键点是,所有 SPA 只有一个 Blazor Hub。在 ConfigureServices 中加载每个 SPA 的所有服务。这确实对您可以运行和不能运行的内容施加了一些限制和控制,但对于大多数情况,这不会成为问题。请记住,服务仅在需要时加载,并在处置时清理。确保您的服务范围正确。

每个 Server SPA 都有一个端点。您无需使用 MapWhen 配置它们,因为它们都使用相同的中间件。它们都定义在默认的 UseEndpoints 中。您已经在 Startup.cs 中看到了这些。

Server SPA 的客户端再次被定义为单个页面。与 WASM 页面(静态 HTML 包裹在 Razor 页面中)不同,Server SPA 页面是一个 Razor 页面。它有一个 Component 入口点,该入口点由 TagHelper 读取并由服务器处理。Razor 对页面的处理方式由 rendermode 决定。

@page "/spagrey"
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}
<!DOCTYPE html<span class="pl-kos">></span>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, 
          maximum-scale=1.0, user-scalable=no" />
    <title>Mongrel.Server.GREY</title>
    <base href="/grey" />
    <link href="/css/site.css" rel="stylesheet" />
    <link href="/CEC.Blazor.Hydra.styles.css" rel="stylesheet" />
</head>
<body>
    <component type="typeof(Mongrel.Server.Grey.Components.ServerGreyApp)" 
               render-mode="ServerPrerendered" />

    <script src="_framework/blazor.server.js"></script>
</body>
</html>

一旦浏览器收到服务器生成的 HTML,它就会渲染初始的静态 DOM,然后调用加载的 JavaScript。这时魔法就开始了。blazor.server.js 加载,读取页面中包含的配置数据,并通过 SignalR 与 BlazorHub 建立会话,并请求页面上由 <component> 定义的根 IComponent - 通常是 App.Razor 中定义的 App。在服务器上由 endpoints.MapBlazorHub() 配置的 Blazor 中间件接收请求并建立会话。Blazor Hub 渲染请求的 IComponent 组件树,并通过 SignalR 将更改传回客户端:在初始重新渲染时,这就是整个 DOM。SPA 现在已启动并运行。页面上会发生事件,会传回 Hub 会话。它处理事件并将任何 DOM 更改传回客户端。导航到已知路由的事件由 SPA 路由器处理:SPA 会话持续存在。超出已知路由的 URL 会导致 SPA 路由器提交对该 URL 的完整 HTTP GET 请求:SPA 会话结束。我通过观察浏览器中的页面标签来判断请求是路由还是导致页面刷新。

那么 SignalR 会话中发生了什么?Web 站点的 AspNetCore 编译代码库包含所有 Blazor 组件代码 - 它们只是标准的类。服务容器运行服务,同样是编译代码库中的标准类。SignalR 会话的核心是特定 SPA 会话的渲染器。它从 ComponentTree 构建和重建 DOM。将 Blazor Server SPA 视为两个实体:一个在服务器上的 Blazor Hub 中运行,负责大部分工作;另一个在客户端,拦截事件并将其传回服务器,然后使用返回的 DOM 更改重新渲染页面。事件从客户端到服务器,DOM 更改从服务器到客户端。

构建 Hydra 站点

我试图尽可能保持简单,将站点基于开箱即用的模板。我没有尝试将代码合并到共享库中。

按如下方式构建起始解决方案(所有项目均来自标准 VS 2019 模板)

  1. Hydra.Web - 一个 Razor MVC 项目。这是启动项目。
  2. Hydra.Grey - 一个 Razor 库项目
  3. Hydra.Blue - 另一个 Razor 库项目
  4. Hydra.Steel - 一个 Blazor WASM 项目(独立 - 非 NetCore 托管)
  5. Hydra.Red - 一个 Blazor WASM 项目(独立 - 非 NetCore 托管)

Initial Project View

Hydra.Web 设置为启动项目。如果您与我一起操作,请运行项目以检查 Razor 项目是否编译并运行。

清理两个库

从两个库中清除以下文件

  • wwwroot 及其内容
  • Component1.razor
  • ExampleJsInterop.cs

从 WASM 项目开始。

Hydra.Red

Initial Project View

更新项目文件。注意 StaticWebAssetBasePath 设置为 red

<Project Sdk=<span class="pl-pds">"Microsoft.NET.Sdk.BlazorWebAssembly"</span>>

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <StaticWebAssetBasePath>red</StaticWebAssetBasePath>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.3" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" 
                      Version="5.0.3" PrivateAssets="all" />
    <PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
  </ItemGroup>

</Project>

更新 NavMenu.razor,在链接顶部添加以下链接

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="/Hydra" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Hydra
            </NavLink>
        </li>
......

更新 MainLayout.razor.css,更改 .sidebar 背景。

.sidebar {
    background-image: linear-gradient(180deg, #400 0%, #800 70%);
}

Hydra.Steel

Initial Project View

更新项目文件。注意添加的 StaticWebAssetBasePath 设置为 steel

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <StaticWebAssetBasePath>steel</StaticWebAssetBasePath>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.3" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" 
                      Version="5.0.3" PrivateAssets="all"/>
    <PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
  </ItemGroup>

</Project>

更新 NavMenu.razor,在链接顶部添加以下链接。

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="/Hydra" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Hydra
            </NavLink>
        </li>
......

更新 MainLayout 中的 sidebar

<div class="sidebar sidebar-steel">
    <NavMenu />
</div>
// Hydra.Steel/Pages/FetchData.razor
....

    protected override async Task OnInitializedAsync()
    {
// Note /sample-data
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>
                    ("/sample-data/weather.json");
    }
....

我们将为此 SPA 使用 Hydra.Web 中的 CSS。

删除

  1. wwwroot 文件夹结构
  2. /Shared/MainLayout.razor.css
  3. /Shared/NavMenu.razor.css

Hydra.Web

Initial Project View

更新项目文件。我们添加了所有项目和对 Blazor WASM Server 库的包引用。

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" 
                      Version="5.0.3"/>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Hydra.Blue\Hydra.Blue.csproj" />
    <ProjectReference Include="..\Hydra.Red\Hydra.Red.csproj" />
    <ProjectReference Include="..\Hydra.Steel\Hydra.Steel.csproj" />
    <ProjectReference Include="..\Razor.Grey\Hydra.Grey.csproj" />
  </ItemGroup>

</Project>

从仓库中提取以下文件夹和文件

  1. wwwroot/css
  2. wwwroot/sample-data

wwwroot/css/app.css 是自定义构建的 BootStrap 发行版。

Pages 文件夹中添加两个 Razor 页面

  1. _Blue.cshtml
  2. _Red.cshtml
  3. _Steel.cshtml
  4. _Grey.cshtml

删除与这些相关的模型 cs 文件。

// Hydra.Web/Pages/_Red.cshtml
@page "/spared"
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}

<!DOCTYPE html<span class="pl-kos">></span>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, 
     initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Hydra.RED</title>
    <base href="/red/" />
    <link href="/red/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="/red/css/app.css" rel="stylesheet" />
    <link href="/red/Hydra.Red.styles.css" rel="stylesheet" />
</head>

<body>
    <div id="app">
        <div class="mt-4" style="margin-right:auto; margin-left:auto; width:100%;">
            <div class="loader"></div>
            <div style="width:100%; text-align:center;">
            <h4>Web Application Loading</h4></div>
        </div>
    </div>

    <script src="/red/_framework/blazor.webassembly.js"></script>
</body>

</html>
// Hydra.Web/Pages/_Steel.cshtml
@page "/spasteel"
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}

<!DOCTYPE html<span class="pl-kos">></span>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, 
     initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Hydra.Steel</title>
    <base href="/steel/" />
    <link href="/css/app.css" rel="stylesheet" />
    <link href="Hydra.Web.styles.css" rel="stylesheet" />
</head>

<body>
    <div id="app">
        <div class="mt-4" style="margin-right:auto; margin-left:auto; width:100%;">
            <div class="loader"></div>
            <div style="width:100%; text-align:center;">
            <h4>Web Application Loading</h4></div>
        </div>
    </div>

    <script src="/steel/_framework/blazor.webassembly.js"></script>
</body>

</html>
// Hydra.Web/Pages/_Grey.cshtml & _Blue.cshtml_
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}
<!DOCTYPE>
<html>
<head>
</head>
<body>
Holding page
</body>
</html>

Startup.cs

我们

  1. 在 Services 中添加对 Razor Pages 的支持。
  2. 为 Red 和 Steel SPA 添加特定的 Endpoint 配置。
    1. 挂载与 SPA <StaticWebAssetBasePath> 相关的 _framework 文件
    2. 为所有 /purple/* URL 设置回退到 _Colour.cshtml
  3. 同时添加 Blazor 支持。
    1. 添加 Blazor Services。
    2. 添加 Blazor Hub。
    3. 为 Blazor Server SPA 添加 Endpoints。
// Hydra.Web/StartUp.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    // Server Side Blazor doesn't register HttpClient by default
    // Thanks to Robin Sue - Suchiman https://github.com/Suchiman/BlazorDualMode
    if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
    {
        // Setup HttpClient for server side in a client side compatible fashion
        services.AddScoped<HttpClient>(s =>
        {
            <span class="pl-c">// Creating the URI helper needs to wait 
            until the JS Runtime is initialized, so defer it.</span>
            var uriHelper = s.GetRequiredService<NavigationManager>();
            return new HttpClient
            {
                BaseAddress = new Uri(uriHelper.BaseUri)
            };
        });
    }
}

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

    app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/red"), app1 =>
    {
        app1.UseBlazorFrameworkFiles("/red");
        app1.UseRouting();
        app1.UseEndpoints(endpoints =>
        {
            endpoints.MapFallbackToPage("/red/{*path:nonfile}", "/_Red");
        });

    });

    app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/steel"), app1 =>
    {
        app1.UseBlazorFrameworkFiles("/steel");
        app1.UseRouting();
        app1.UseEndpoints(endpoints =>
        {
            endpoints.MapFallbackToPage("/steel/{*path:nonfile}", "/_Steel");
        });

    });

    app.UseRouting();

    <span class="pl-c">// default EndPoint Configuration</span>
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapBlazorHub();
        endpoints.MapRazorPages();
        endpoints.MapFallbackToPage("/grey/{*path:nonfile}", "/_Grey");
        endpoints.MapFallbackToPage("/blue/{*path:nonfile}", "/_Blue");
        endpoints.MapFallbackToPage("/Index");
    });
}

Index.cshtml

更新 @page 指令并添加一些按钮用于导航。

@page "/"
....
<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">
                    building Web apps with ASP.NET Core</a>.</p>
    <div class="container">
        <a class="btn btn-danger" href="/red">Hydra Red</a>
        <a class="btn btn-dark" href="/steel">Hydra Steel</a>
        <a class="btn btn-primary" href="/blue">Hydra Blue</a>
        <a class="btn btn-secondary" href="/grey">Hydra Grey</a>
    </div>
</div>

这一切现在应该可以编译并运行。

Hydra.Blue

Initial Project View

这是我们将用于构建 Blazor Server SPA 的库。

项目文件

包含以下包和项目。

<Project Sdk="Microsoft.NET.Sdk.Razor"

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="5.0.3" />
  </ItemGroup>

</Project>

文件移动和重命名

Hydra.Red 复制 PagesShared 文件夹。

  1. Pages 文件夹
  2. Shared 文件夹
  3. App.razor
  4. _Imports.razor

_Imports.razor

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Hydra.Blue
@using Hydra.Blue.Shared
// Hydra.Blue/Pages/Index.razor
@page "/"
@page "/blue"
@page "/blue/index"
....
// Hydra.Blue/Pages/Counter.razor
@page "/counter"
@page "/blue/counter"
....
// Hydra.Blue/Pages/FetchData.razor
@page "/fetchdata"
@page "/blue/fetchdata"
....

    protected override async Task OnInitializedAsync()
    {
// Note /sample-data
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>
                    ("/sample-data/weather.json");
    }
....

更新 MainLayout 中的 sidebar

<div class="sidebar sidebar-blue">
    <NavMenu />
</div>

更新 NavMenu.razor

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="/Hydra" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Hydra
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="blue/index" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="blue/counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="blue/fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </li>
    </ul>
</div>

更新 App.razor,更改 AppAssembly,该值设置为 Hydra.Blue.App,以便它在该程序集中查找路由。

// Hydra.Blue/App.razor
<Router AppAssembly="@typeof(Hydra.Blue.App).Assembly" PreferExactMatches="@true">
    <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>

Hydra.Grey

Initial Project View

项目文件

包含以下包和项目。

<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="5.0.3" />
  </ItemGroup>

</Project>

文件移动和重命名

Hydra.Blue 复制 PagesShared 文件夹。

  1. Pages 文件夹
  2. Shared 文件夹
  3. App.razor
  4. _Imports.razor

_Imports.razor

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Hydra.Grey
@using Hydra.Grey.Shared
// Hydra.Grey/Pages/Index.razor
@page "/"
@page "/Grey"
@page "/Grey/index"
....
// Hydra.Grey/Pages/Counter.razor
@page "/Grey/counter"
....
// Hydra.Grey/Pages/FetchData.razor
@page "/fetchdata"
@page "/Grey/fetchdata"
....

    protected override async Task OnInitializedAsync()
    {
// Note /sample-data
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>
                    ("/sample-data/weather.json");
    }
....
Update the *sidebar* in `MainLayout`.
```html
    <div class="sidebar sidebar-grey">
        <NavMenu />
    </div>

更新 NavMenu.razor

<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">Hydra.Grey</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="/Hydra" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Hydra
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="grey/index" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="grey/counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="grey/fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </li>
    </ul>
</div>

更新 App.razor,更改 AppAssembly,该值设置为 Hydra.Grey.App,以便它在该程序集中查找路由。

// Hydra.Grey/App.razor
<Router AppAssembly="@typeof(Hydra.Grey.App).Assembly" PreferExactMatches="@true">
    <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>

Hydra.Web

// Hydra.Web/Pages/_Blue.html
@page "/spablue"
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}

<!DOCTYPE></span>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, 
          initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Hydra.Blue</title>
    <base href="/blue" />
    <link href="/css/app.css" rel="stylesheet" />
    <link href="/CEC.Blazor.Hydra.styles.css" rel="stylesheet" />
</head>

<body>
    <component type="typeof(Hydra.Blue.App)" render-mode="ServerPrerendered" />

    <script src="_framework/blazor.server.js"></script>
</body>

</html>
// Hydra.Web/Pages/_Grey.html
@page "/spagrey"
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}

<!DOCTYPE></span>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, 
     initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Hydra.Grey</title>
    <base href="/grey" />
    <link href="/css/app.css" rel="stylesheet" />
    <link href="/CEC.Blazor.Hydra.styles.css" rel="stylesheet" />
</head>

<body>
    <component type="typeof(Hydra.Grey.App)" render-mode="ServerPrerendered" />

    <script src="_framework/blazor.server.js"></script>
</body>

</html>

这一切现在应该可以编译并运行。

总结

这篇文章内容很多,需要消化。我花了些时间才整理好这篇文章,一旦我证明了这些概念。一些重要的概念需要理解

  1. 要编写 Blazor SPA,您需要摆脱“Web”范式。考虑老式桌面应用程序。有点复古!否则,您的 SPA 可能会一团糟。版本 x 将与版本 1 几乎没有相似之处。
  2. WASM SPA 是一个编译后的可执行文件 - 就像桌面应用程序一样。启动页是一个快捷方式。
  3. Server SPA 是指向 Web 站点上运行的库中某个类的指针。启动页是一个服务器端快捷方式,用于启动并运行它。一旦启动,SPA 就是一个两部分组成的组合:一部分是浏览器 SPA,另一部分是服务器上 Blazor Hub 的一个会话,通过 SignalR 会话紧密连接。
  4. 您需要非常小心 URL 引用:丢失的斜杠或大写字母可能会让您付出代价!坚持所有 URL 和定义 URL 的内容只使用小写字母的规则。很容易挖坑,我已经经历过了!

修订

  • 版本 1 - 2020 年 11 月 24 日,题为《在单个项目中使用单个站点运行 Blazor WASM 和 Server》
  • 版本 2 - 2021 年 2 月 25 日 - 标题更改和完全重写
© . All rights reserved.