多个托管 ASP.NET Core Blazor WebAssembly 应用

注意

此版本不是本文的最新版本。 有关本文的最新版本,请参阅 .NET 7 版本

本文介绍如何将托管的 Blazor WebAssembly 应用配置为托管多个 Blazor WebAssembly 应用。

配置

选择与托管要求(端口/域托管(例如 :5001/:5002firstapp.com/secondapp.com)或路由子路径托管(例如 /FirstApp/SecondApp))匹配的本文版本。

对于当前托管选择,本文涵盖端口/域托管(例如 :5001/:5002firstapp.com/secondapp.com)。

在以下示例中:

  • 托管的 Blazor WebAssembly 应用的项目名称是 MultipleBlazorApps,位于名为 MultipleBlazorApps 的文件夹中。
  • 在添加第二个客户端应用程序之前,解决方案中的三个项目是 Client 文件夹中的 MultipleBlazorApps.ClientServer 文件夹中的 MultipleBlazorApps.ServerShared 文件夹中的 MultipleBlazorApps.Shared
  • 初始(第一个)客户端应用是通过 Blazor WebAssembly 项目模板创建的解决方案的默认客户端项目。
  • 向解决方案添加第二个客户端应用,MultipleBlazorApps.SecondClient 位于名为 SecondClient 的文件夹中。
  • (可选)服务器项目 (MultipleBlazorApps.Server) 可以将页面或视图作为 Razor Pages 或 MVC 应用提供。
  • 第一个客户端应用可在浏览器中通过端口 5001 或主机 firstapp.com 访问。 第二个客户端应用可在浏览器中通过端口 5002 或主机 secondapp.com 访问。

对于当前选择,本文涵盖路由子路径托管(例如 /FirstApp/SecondApp)。

在以下示例中:

  • 托管的 Blazor WebAssembly 应用的项目名称是 MultipleBlazorApps,位于名为 MultipleBlazorApps 的文件夹中。
  • 在添加第二个客户端应用程序之前,解决方案中的三个项目是 Client 文件夹中的 MultipleBlazorApps.ClientServer 文件夹中的 MultipleBlazorApps.ServerShared 文件夹中的 MultipleBlazorApps.Shared
  • 初始(第一个)客户端应用是通过 Blazor WebAssembly 项目模板创建的解决方案的默认客户端项目。
  • 向解决方案添加第二个客户端应用,MultipleBlazorApps.SecondClient 位于名为 SecondClient 的文件夹中。
  • (可选)服务器项目 (MultipleBlazorApps.Server) 可以将页面或视图作为正式的 Razor Pages 或 MVC 应用提供。
  • 这两个客户端应用使用由 MultipleBlazorApps.Server 项目的 Properties/launchSettings.json 文件在其 applicationUrl 值中定义的默认端口。 第一个客户端应用可在 /FirstApp 子路径的浏览器中访问。 第二个客户端应用可在 /SecondApp 子路径的浏览器中访问。

本文中所示的示例需要对以下项进行其他配置:

  • 直接访问示例主机域 firstapp.comsecondapp.com 上的应用。
  • 获取客户端应用的证书以启用 TLS/HTTPS 安全性。
  • 将服务器应用配置为 Razor Pages 应用以获得以下功能:
    • 将 Razor 组件集成到页面或视图中。
    • 预呈现 Razor 组件。

上述配置超出了本文的范围。 有关更多信息,请参见以下资源:

使用现有托管的 Blazor WebAssembly 解决方案,或者通过传递 -ho|--hosted 选项(如果使用 .NET CLI)或在 IDE 中创建项目后选中 Visual Studio 中的“ASP.NET Core 托管”复选框,从 Blazor WebAssembly 项目模板创建新的托管 Blazor WebAssembly 解决方案

使用名为 MultipleBlazorApps 的解决方案文件夹并将项目命名为 MultipleBlazorApps

在名为 SecondClient 的解决方案中创建一个新文件夹。 在新文件夹中,添加名为 MultipleBlazorApps.SecondClient 的第二个 Blazor WebAssembly 客户端应用。 将项目添加为独立 Blazor WebAssembly 应用。 若要创建独立 Blazor WebAssembly 应用,如果使用 .NET CLI,请不要传递 -ho|--hosted 选项;如果使用 Visual Studio,请不要使用“ASP.NET Core 托管”复选框。

MultipleBlazorApps.SecondClient 项目进行以下更改:

  • FetchData 组件 (Pages/FetchData.razor) 从 Client/Pages 文件夹复制到 SecondClient/Pages 文件夹。 此步骤是必需的,因为独立 Blazor WebAssembly 应用不会为天气数据调用 Server 项目的控制器,它使用静态数据文件。 通过将 FetchData 组件复制到添加的项目,第二个客户端应用还会对服务器 API 进行 Web API 调用以获取天气数据。
  • 删除 SecondClient/wwwroot/sample-data 文件夹,因为不使用文件夹中的 weather.json 文件。

下表描述添加 SecondClient 文件夹和 MultipleBlazorApps.SecondClient 项目后,解决方案的文件夹和项目名称。

物理文件夹 项目名称 说明
Client MultipleBlazorApps.Client Blazor WebAssembly 客户端应用
SecondClient MultipleBlazorApps.SecondClient Blazor WebAssembly 客户端应用
Server MultipleBlazorApps.Server ASP.NET Core 服务器应用
Shared MultipleBlazorApps.Shared 共享资源项目

MultipleBlazorApps.Server 项目提供两个 Blazor WebAssembly 客户端应用,并通过 MVC 控制器向客户端应用的 FetchData 组件提供天气数据。 (可选)MultipleBlazorApps.Server 项目还可以将页面或视图作为传统的 Razor Pages 或 MVC 应用提供。 本文稍后将介绍启用提供页面或视图的步骤。

注意

本文中的演示将 FirstApp 的静态 web 资产路径名称用于 MultipleBlazorApps.Client 项目 SecondAppMultipleBlazorApps.SecondClient 项目。 名称“FirstApp”和“SecondApp”仅用于演示目的。 可以接受其他名称来区分客户端应用,例如 App1/App2Client1/Client21/2 或任何类似的命名方案。

通过端口或域将请求路由到客户端应用时,“FirstApp”和“SecondApp”均在内部使用以路由请求并为静态资产提供响应,不显示在浏览器的地址栏中。

注意

本文中的演示将 FirstApp 的静态 web 资产路径名称用于 MultipleBlazorApps.Client 项目 SecondAppMultipleBlazorApps.SecondClient 项目。 名称“FirstApp”和“SecondApp”仅用于演示目的。 可以接受其他名称来区分客户端应用,例如 App1/App2Client1/Client21/2 或任何类似的命名方案。

FirstApp”和“SecondApp”也会显示在浏览器的地址栏中,因为请求会使用这些名称路由到两个客户端应用。 支持其他有效的 URL 路由段,路由段并不是非常需要与用于在内部路由静态 Web 资产的名称匹配。 将“FirstApp”和“SecondApp”用于内部静态资产路由和应用请求路由只是为了本文示例方便。

在第一个客户端应用的项目文件 (MultipleBlazorApps.Client.csproj) 中,将 <StaticWebAssetBasePath> 属性 添加到值为 FirstApp<PropertyGroup>,以设置项目静态资产的基路径:

<StaticWebAssetBasePath>FirstApp</StaticWebAssetBasePath>

MultipleBlazorApps.SecondClient 应用的项目文件 (MultipleBlazorApps.SecondClient.csproj) 中,执行以下操作:

  • <StaticWebAssetBasePath> 属性添加到值为 SecondApp<PropertyGroup>

    <StaticWebAssetBasePath>SecondApp</StaticWebAssetBasePath>
    
  • MultipleBlazorApps.Shared 项目的项目引用添加到 <ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\Shared\MultipleBlazorApps.Shared.csproj" />
    </ItemGroup>
    

在服务器应用的项目文件 (Server/MultipleBlazorApps.Server.csproj) 中,为 <ItemGroup> 中添加的 MultipleBlazorApps.SecondClient 客户端应用创建项目引用:

<ProjectReference Include="..\SecondClient\MultipleBlazorApps.SecondClient.csproj" />

在服务器应用的 Properties/launchSettings.json 文件中,配置 Kestrel 配置文件 (MultipleBlazorApps.Server) 的 applicationUrl,以访问位于端口 5001 和 5002 的客户端应用。 如果将本地环境配置为使用示例域,则 applicationUrl 的 URL 可以使用 firstapp.comsecondapp.com,而不是使用端口。

注意

本演示中端口的使用让你无需配置本地托管环境就能够在本地浏览器中访问客户端项目,以便 Web 浏览器可以通过主机配置访问客户端应用 firstapp.comsecondapp.com。 在生产场景中,典型的配置是使用子域来区分客户端应用。

例如:

  • 端口已从此演示的配置中删除。
  • 主机已更改为使用子域,例如 www.contoso.com(适用于站点访问者)和 admin.contoso.com(适用于管理员)。
  • 可以为其他客户端应用包含其他主机,如果服务器应用也是提供页面或视图的 Razor Pages 或 MVC 应用,则至少需要再加一个主机。

如果计划从服务器应用提供页面或视图,请使用 Properties/launchSettings.json 文件中的以下 applicationUrl 设置,该设置允许以下访问:

  • (可选)Razor Pages 或 MVC 应用(MultipleBlazorApps.Server 项目)在端口 5000 处响应请求。
  • 对第一个客户端(MultipleBlazorApps.Client 项目)的请求的响应位于端口 5001。
  • 对第二个客户端(MultipleBlazorApps.SecondClient 项目)的请求的响应位于端口 5002。
"applicationUrl": "https://localhost:5000;https://localhost:5001;https://localhost:5002",

如果不计划服务器应用提供页面或视图,并且只为 Blazor WebAssembly 客户端应用提供服务,请使用以下设置,该设置允许以下访问:

  • 第一个客户端应用在端口 5001 上做出响应。
  • 第二个客户端应用在端口 5002 上做出响应。
"applicationUrl": "https://localhost:5001;https://localhost:5002",

在服务器应用的 Program.cs 文件中,删除在调用 UseHttpsRedirection 后显示的以下代码:

  • 如果计划从服务器应用提供页面或视图,请删除以下代码行:

    - app.UseBlazorFrameworkFiles();
    
    - app.MapFallbackToFile("index.html");
    
  • 如果计划服务器应用仅提供 Blazor WebAssembly 客户端应用,请删除以下代码:

    - app.UseBlazorFrameworkFiles();
    
    ...
    
    - app.UseRouting();
    
    - app.MapRazorPages();
    - app.MapControllers();
    - app.MapFallbackToFile("index.html");
    

    保留静态文件中间件:

    app.UseStaticFiles();
    
  • 添加将请求映射到客户端应用的中间件。 以下示例将中间件配置为在第一个客户端应用的请求端口为 5001 或第二个客户端应用的请求端口为 5002,或者请求主机为第一个客户端应用的 firstapp.com 或第二个客户端应用的 secondapp.com 时运行。

    注意

    在具有本地浏览器的本地系统上使用主机 (firstapp.com/secondapp.com) 需要额外的配置,这超出了本文的范围。 对于此场景的本地测试,建议使用端口。 典型的生产应用配置为使用子域,例如 www.contoso.com(适用于站点访问者)和 admin.contoso.com(适用于管理员)。 使用适当的 DNS 和服务器配置(这超出了本文的范围,具体取决于所使用的技术),应用会在以下代码中指定的任何主机上响应请求。

    在从 Program.cs 中删除 app.UseBlazorFrameworkFiles(); 行的位置放置以下代码:

    app.MapWhen(ctx => ctx.Request.Host.Port == 5001 || 
        ctx.Request.Host.Equals("firstapp.com"), first =>
    {
        first.Use((ctx, nxt) =>
        {
            ctx.Request.Path = "/FirstApp" + ctx.Request.Path;
            return nxt();
        });
    
        first.UseBlazorFrameworkFiles("/FirstApp");
        first.UseStaticFiles();
        first.UseStaticFiles("/FirstApp");
        first.UseRouting();
    
        first.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapFallbackToFile("/FirstApp/{*path:nonfile}", 
                "FirstApp/index.html");
        });
    });
    
    app.MapWhen(ctx => ctx.Request.Host.Port == 5002 || 
        ctx.Request.Host.Equals("secondapp.com"), second =>
    {
        second.Use((ctx, nxt) =>
        {
            ctx.Request.Path = "/SecondApp" + ctx.Request.Path;
            return nxt();
        });
    
        second.UseBlazorFrameworkFiles("/SecondApp");
        second.UseStaticFiles();
        second.UseStaticFiles("/SecondApp");
        second.UseRouting();
    
        second.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapFallbackToFile("/SecondApp/{*path:nonfile}", 
                "SecondApp/index.html");
        });
    });
    

    警告

    依赖于主机头的 API(如 HttpRequest.HostRequireHost)可能会受到客户端的欺骗。

    若要防止主机和端口欺骗,请使用以下方法之一:

  • 添加将请求映射到客户端应用的中间件。 以下示例将中间件配置为在请求子路径为 /FirstApp(对于第一个客户端应用)或 /SecondApp(对于第二个客户端应用)时运行。

    在从 Program.cs 中删除 app.UseBlazorFrameworkFiles(); 行的位置放置以下代码:

    app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/FirstApp", 
        StringComparison.OrdinalIgnoreCase), first =>
    {
        first.UseBlazorFrameworkFiles("/FirstApp");
        first.UseStaticFiles();
        first.UseStaticFiles("/FirstApp");
        first.UseRouting();
    
        first.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapFallbackToFile("/FirstApp/{*path:nonfile}",
                "FirstApp/index.html");
        });
    });
    
    app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/SecondApp", 
        StringComparison.OrdinalIgnoreCase), second =>
    {
        second.UseBlazorFrameworkFiles("/SecondApp");
        second.UseStaticFiles();
        second.UseStaticFiles("/SecondApp");
        second.UseRouting();
    
        second.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapFallbackToFile("/SecondApp/{*path:nonfile}",
                "SecondApp/index.html");
        });
    });
    
  • 在每个客户端应用中设置基路径:

    在第一个客户端应用的 index.html 文件 (Client/wwwroot/index.html) 中,更新 <base> 标记值以反映子路径。 尾部反斜杠是必需项:

    <base href="/FirstApp/" />
    

    在第二个客户端应用的 index.html 文件 (SecondClient/wwwroot/index.html) 中,更新 <base> 标记值以反映子路径。 尾部反斜杠是必需项:

    <base href="/SecondApp/" />
    

有关 UseStaticFiles 的详细信息,请参阅 ASP.NET Core Blazor 静态文件

有关 UseBlazorFrameworkFilesMapFallbackToFile 的详细信息,请参阅以下资源:

注意

指向 .NET 参考源的文档链接通常会加载存储库的默认分支,该分支表示针对下一个 .NET 版本的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉列表。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

从客户端应用到服务器 API 中的 /WeatherForecast 的请求指向 /FirstApp/WeatherForecast/SecondApp/WeatherForecast,具体取决于发出请求的客户端应用。 因此,从服务器 API 返回天气数据的控制器路由需要修改以包含路径段。

在服务器应用的天气预报控制器 (Controllers/WeatherForecastController.cs) 中,将到 WeatherForecastController 的现有路由 ([Route("[controller]")]) 替换为以下路由,其中考虑了客户端请求路径:

[Route("FirstApp/[controller]")]
[Route("SecondApp/[controller]")]

如果计划从服务器应用提供页面,请将 IndexRazor 页面添加到服务器应用的 Pages 文件夹:

Pages/Index.cshtml

@page
@model MultipleBlazorApps.Server.Pages.IndexModel
@{
    ViewData["Title"] = "Home";
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Home</title>
</head>
<body>
    <div class="main">
        <div class="content px-4">

            <div>
                <h1>Welcome</h1>
                <p>Hello from Razor Pages!</p>
            </div>
        </div>
    </div>
</body>
</html>

Pages/Index.cshtml.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace MultipleBlazorApps.Server.Pages;

public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace MultipleBlazorApps.Server.Pages
{
    public class IndexModel : PageModel
    {
        public void OnGet()
        {
        }
    }
}

注意

之前的 Index 页面是仅用于演示目的的最小示例。 如果应用需要额外的 Razor Pages 资产(例如布局、样式、脚本和导入内容),请从使用 Razor Pages 项目模板创建的应用中获取它们。 有关详细信息,请参阅 ASP.NET Core 中的 Razor Pages 简介

如果计划从服务器应用提供 MVC 视图,请添加一个 Index 视图和一个 Home 控制器:

Views/Home/Index.cshtml

@{
    ViewData["Title"] = "Home";
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Home</title>
</head>
<body>
    <div class="main">
        <div class="content px-4">

            <div>
                <h1>Welcome</h1>
                <p>Hello from MVC!</p>
            </div>
        </div>
    </div>
</body>
</html>

Controllers/HomeController.cs

using Microsoft.AspNetCore.Mvc;

namespace MultipleBlazorApps.Server.Controllers;

public class HomeController : Controller
{
    public IActionResult Index() => View();
}

注意

之前的 Index 视图是仅用于演示目的的最小示例。 如果应用需要额外的 MVC 资产(例如布局、样式、脚本和导入内容),请从使用 MVC 项目模板创建的应用中获取它们。 有关详细信息,请参阅 ASP.NET Core MVC 入门

有关在服务器应用的页面或视图中使用任一客户端应用的 Razor 组件的详细信息,请参阅预呈现和集成 ASP.NET Core Razor 组件

运行应用

运行 MultipleBlazorApps.Server 项目:

  • https://localhost:5001 访问初始客户端应用。
  • https://localhost:5002 访问新增的客户端应用。
  • 如果服务器应用配置为提供页面或视图,请在 https://localhost:5000 访问 Index 页面或视图。
  • https://localhost:{DEFAULT PORT}/FirstApp 访问初始客户端应用。
  • https://localhost:{DEFAULT PORT}/SecondApp 访问新增的客户端应用。
  • 如果服务器应用配置为提供页面或视图,请在 https://localhost:{DEFAULT PORT} 访问 Index 页面或视图。

在前面的示例 URL 中,{DEFAULT PORT} 占位符是由 MultipleBlazorApps.Server 项目的 Properties/launchSettings.json 文件在其 applicationUrl 值中定义的默认端口。

重要

使用 dotnet watch(或 dotnet run)命令 (.NET CLI) 运行应用时,请确认命令 shell 已在解决方案的 Server 文件夹中打开。

使用 Visual Studio 的“开始”按钮运行应用时,请确认 MultipleBlazorApps.Server 项目设置为启动项目(在解决方案资源管理器中突出显示)。

静态资产

当资产位于客户端应用的 wwwroot 文件夹中时,请在组件中提供静态资产请求路径:

<img alt="..." src="{PATH AND FILE NAME}" />

{PATH AND FILE NAME} 占位符是 wwwroot 下的路径和文件名。

例如,wwwrootvehicle 文件夹中的 Jeep 图像 (jeep-yj.png) 的来源:

<img alt="Jeep Wrangler YJ" src="vehicle/jeep-yj.png" />

Razor 类库 (RCL) 支持

Razor 类库 (RCL) 作为新项目添加到解决方案中:

  • 在“解决方案资源管理器”中右键单击此解决方案并选择“添加”>“新建项目”。
  • 使用 Razor 类库项目模板创建项目。 本部分中的示例使用项目名称 ComponentLibrary,它也是 RCL 的程序集名称。 请勿选中“支持页面和视图”复选框。

对于每个托管的 Blazor WebAssembly 客户端应用,通过右键单击“解决方案资源管理器”中的每个客户端项目并选择“添加”>“项目参考”,为 RCL 项目创建项目引用。

通过以下任一方法在客户端应用中使用 RCL 中的组件:

  • @using 指令放置在 RCL 命名空间的组件顶部,并为组件添加 Razor 语法。 以下示例适用于程序集名称为 ComponentLibrary 的 RCL:

    @using ComponentLibrary
    
    ...
    
    <Component1 />
    
  • 提供 RCL 的命名空间以及 Razor 组件的语法。 此方法不需要组件文件顶部的 @using 指令。 以下示例适用于程序集名称为 ComponentLibrary 的 RCL:

    <ComponentLibrary.Component1 />
    

注意

还可以将 @using 指令放入每个客户端应用的 _Import.razor 文件中,这使得 RCL 的命名空间全局可用于该项目中的组件。

当任何其他静态资产位于 RCL 的 wwwroot 文件夹中时,请按照 ASP.NET Core 的类库中的可重用 Razor UI 中的指南在客户端应用中引用静态资产:

<img alt="..." src="_content/{PACKAGE ID}/{PATH AND FILE NAME}" />

{PACKAGE ID} 占位符是 RCL 的包 ID。 如果项目文件中没有指定 <PackageId>,则包 ID 默认为项目的程序集名称。 {PATH AND FILE NAME} 占位符是 wwwroot 下的路径和文件名。

以下示例显示了 RCL wwwroot 文件夹的 vehicle 文件夹中 Jeep 图像 (jeep-yj.png) 的标记。 以下示例适用于程序集名称为 ComponentLibrary 的 RCL:

<img alt="Jeep Wrangler YJ" src="_content/ComponentLibrary/vehicle/jeep-yj.png" />

其他资源