目录
WeatherForecastServerService.cs
代码库
文章的代码库在这里 - https://github.com/ShaunCurtis/AllinOne
解决方案和项目
使用Blazor WebAssembly模板创建一个名为Blazor的新解决方案。不要选择在Aspnetcore上托管它。您将获得一个名为Blazor 的项目。
现在使用ASP.NET Core Web App模板向解决方案添加第二个项目。称之为Blazor.Web。将其设置为启动项目。
解决方案现在应如下所示:
Blazor项目变更
该解决方案在网站的子目录中运行WASM上下文。要使其正常工作,需要对Blazor项目进行一些修改。
- 将wwwroot的内容移动到Blazor.Web并删除wwwroot中的所有内容。
- 向项目文件中添加一个StaticWebAssetBasePath条目,设置为wasm 。这在使用它的上下文中区分大小写,因此请坚持使用小写字母。
- 添加必要的包。
项目文件应如下所示:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<StaticWebAssetBasePath>wasm</StaticWebAssetBasePath>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.4" PrivateAssets="all" />
<PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
</Project>
MainLayout
MainLayout需要修改以处理这两种情况。该解决方案更改了每个上下文的配色方案。WASM Teal和Server Steel。
@inherits LayoutComponentBase
<div class="page">
@*change class*@
<div class="@_sidebarCss">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
<div class="content px-4">
@Body
</div>
</div>
</div>
@code {
[Inject] NavigationManager NavManager { get; set; }
private bool _isWasm => NavManager?.Uri.Contains("wasm", StringComparison.CurrentCultureIgnoreCase) ?? false;
private string _sidebarCss => _isWasm ? "sidebar sidebar-teal" : "sidebar sidebar-steel";
}
将以下Css样式添加到下面的组件Css文件.sidebar中。
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
/* Added Styles*/
.sidebar-teal {
background-image: linear-gradient(180deg, rgb(0, 64, 128) 0%, rgb(0,96,192) 70%);
}
.sidebar-steel {
background-image: linear-gradient(180deg, #2a3f4f 0%, #446680 70%);
}
/* End Added Styles*/
导航菜单
添加代码和标记——它添加了一个链接以在上下文之间切换。
<div class="top-row pl-4 navbar navbar-dark">
@*Change title*@
<a class="navbar-brand" href="">Blazor</a>
<button class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon">
</button>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
@*Add links between contexts*@
<li class="nav-item px-3">
<NavLink class="nav-link" href="@_otherContextUrl" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"> @_otherContextLinkName
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"> Home
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"> Counter
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"> Fetch data
</NavLink>
</li>
</ul>
</div>
@code {
[Inject] NavigationManager NavManager { get; set; }
private bool _isWasm => NavManager?.Uri.Contains("wasm", StringComparison.CurrentCultureIgnoreCase) ?? false;
private string _otherContextUrl => _isWasm ? "/" : "/wasm";
private string _otherContextLinkName => _isWasm ? "Server Home" : "WASM Home";
private string _title => _isWasm ? "AllinOne WASM" : "AllinOne Server";
private bool collapseNavMenu = true;
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
FetchData.razor
通过在开头添加/来更新用于获取预测的URL ,该文件现在位于根目录中而不是wasm。
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("/sample-data/weather.json");
}
Blazor.Web
更新项目文件:
<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="..\Blazor\Blazor.csproj" />
</ItemGroup>
</Project>
添加Razor网页的页面叫做WASM.cshtml——为WASM SPA启动页。
@page "/wasm"
@{
Layout = null;
}
<!DOCTYPE html<span class="pl-kos">>
<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>Blazor</title>
@*Change base*@
<base href="/wasm/" />
@*Update Link hrefs*@
<link href="/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="/css/app.css" rel="stylesheet" />
<link href="/wasm/Blazor.styles.css" rel="stylesheet" />
</head>
<body>
<div id="app">Loading...</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
@*Update js sources *@
<script src="/wasm/_framework/blazor.webassembly.js"></script>
</body>
</html>
添加第二个Razor网页的页面叫做Server.cshtml——为Servr SPA启动页。
@page "/"
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = null;
}
<!DOCTYPE html>
<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>Blazor</title>
<base href="/" />
<link href="/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="/css/site.css" rel="stylesheet" />
<link href="/wasm/Blazor.styles.css" rel="stylesheet" />
</head>
<body>
<component type="typeof(Blazor.App)" render-mode="ServerPrerendered" />
<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>
Index.cshtml
将@page指令更新为@page "/index".
Startup.cs
更新Startup以处理WASM和服务器中间件路径。
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
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 =>
{
// Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
var uriHelper = s.GetRequiredService<NavigationManager>();
return new HttpClient
{
BaseAddress = new Uri(uriHelper.BaseUri)
};
});
}
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
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();
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/wasm"), app1 =>
{
app1.UseBlazorFrameworkFiles("/wasm");
app1.UseRouting();
app1.UseEndpoints(endpoints =>
{
endpoints.MapFallbackToPage("/wasm/{*path:nonfile}", "/wasm");
});
});
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapRazorPages();
endpoints.MapFallbackToPage("/Server");
});
}
}
运行应用程序
应用程序现在应该可以运行了。它将在服务器上下文中启动。通过左侧菜单中的链接切换到WASM上下文。当您在上下文之间切换时,您应该会看到颜色的变化。
添加数据服务
虽然上述配置有效,但它需要一些演示代码来展示它如何处理更传统的数据服务。我们将修改解决方案以使用非常基本的数据服务来展示应该使用的DI和接口概念。
将数据和服务文件夹添加到Blazor项目。
WeatherForecast.cs
向Data添加一个WeatherForecast类。
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
IWeatherForecastService.cs
为Services添加一个IWeatherForecastService接口。
public interface IWeatherForecastService
{
public Task<List<WeatherForecast>> GetRecordsAsync();
}
WeatherForecastServerService.cs
向Services添加一个WeatherForecastServerService类。通常这会连接到数据库,但这里我们只是创建了一组虚拟记录。
public class WeatherForecastServerService : IWeatherForecastService
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private List<WeatherForecast> records = new List<WeatherForecast>();
public WeatherForecastServerService()
=> this.GetForecasts();
public void GetForecasts()
{
var rng = new Random();
records = Enumerable.Range(1, 10).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
}).ToList();
}
public Task<List<WeatherForecast>> GetRecordsAsync()
=> Task.FromResult(this.records);
}
WeatherForecastAPIService.cs
向Services添加一个WeatherForecastAPIService类。
public class WeatherForecastAPIService : IWeatherForecastService
{
protected HttpClient HttpClient { get; set; }
public WeatherForecastAPIService(HttpClient httpClient)
=> this.HttpClient = httpClient;
public async Task<List<WeatherForecast>> GetRecordsAsync()
=> await this.HttpClient.GetFromJsonAsync<List<WeatherForecast>>($"/api/weatherforecast/list");
}
WeatherForecastController.cs
最后将一个WeatherForecastController类添加到控制器文件夹中的Blazor.Web项目。
Fusing System.Collections.Generic;
using System.Threading.Tasks;
using Blazor.Data;
using Microsoft.AspNetCore.Mvc;
using <span class="pl-en">MVC = Microsoft.AspNetCore.Mvc;
using Blazor.Services;
namespace Blazor.Web.APIControllers
{
[ApiController]
public class WeatherForecastController : ControllerBase
{
protected IWeatherForecastService DataService { get; set; }
public WeatherForecastController(IWeatherForecastService dataService)
=> this.DataService = dataService;
[MVC.Route("/api/weatherforecast/list")]
[HttpGet]
public async Task<List<WeatherForecast>> GetList() => await DataService.GetRecordsAsync();
}
}
Blazor项目Program.cs
通过它的.IWeatherForecastService将API服务添加到Blazor项目中的Program.cs。
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<IWeatherForecastService, WeatherForecastAPIService>();
await builder.Build().RunAsync();
}
}
Blazor.Web Startup.cs
在Blazor.Web项目中将服务器服务添加到Startup.cs中,再通过它的IWeatherForecastService。
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddScoped<IWeatherForecastService, WeatherForecastServerService>();
.....
}
构建并运行项目
解决方案现在应该构建并运行。
|
|
它是如何工作的?
从根本上说,Blazor服务器和Blazor WASM应用程序之间的区别在于它运行的上下文。在解决方案中,所有SPA代码都构建在Web Assembly项目中,并由WASM和服务器上下文使用。没有“共享”代码库代码,因为它是完全相同的前端代码,具有相同的入口点——App.razor。两个上下文之间的不同之处在于后端服务的提供者。
已声明Web程序集项目<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">。它构建标准Blazor.dll文件和WASM特定代码,包括Web程序集“启动配置文件”blazor.boot.json。
在Web程序集上下文中,初始页面加载blazor.webassembly.js。这将加载blazor.boot.json,它告诉blazor.webassembly.js如何在浏览器中“启动”Web程序集代码。它运行Program构建WebAssemblyHost,加载定义的服务,并启动Renderer将应用html元素替换为Program中指定的根组件。这将加载路由器,它读取Url,获取适当的组件,将其加载到指定的布局中,然后开始渲染过程。SPA启动并运行。
在服务器上下文中,服务器端代码在初始加载页面中选取组件引用并静态呈现它。它将呈现的页面传递给客户端。这会加载并运行blazor.server.js,它会回调服务器SignalR Hub并获取动态呈现的应用根组件。SPA启动并运行。服务容器和渲染器位于Blazor Hub 中——通过services.AddServerSideBlazor()在Web服务器启动时调用Startup来启动。
我们实现的数据服务展示了依赖注入和接口。UI组件——在我们的例子中FetchData使用IWeatherForcastService在Services中注册的服务。在 WASM上下文中,服务容器启动WeatherForecastAPIService,而在服务器上下文中,服务容器启动WeatherForecastServerService。两个不同的服务,遵循相同的接口,由使用该接口的UI组件消费。UI组件并不关心它们使用哪个服务,它只需要实现IWeatherForcastService。
总结
希望本文能够深入了解Blazor SPA的工作原理以及服务器和WASM Blazor SPA之间的真正区别。
https://www.codeproject.com/Articles/5299017/Building-a-Blazor-WASM-and-Server-All-In-One-Solut