今天咱们来扯一下 Blazor 应用程序怎么访问 HttpContext。其实这句话有坑,为了避免大伙伴们掉茅坑,老周直接说明:Blazor 是不能访问 HttpContext 的。哪怕你在服务容器中注册了 IHttpContextAccessor 也不行,无法返回有效的上下文。
为啥?这得从 Blazor 的运行方式说起。银河系周知,Blazor 允许用 .NET 代码编写客户端逻辑,在某种程度(注意,是某种程度)上替代“奸商”……哦不,是 JS。Javascript 虽然语法上很像C某某,但和C某某比还是差得很远,代码阅读起来总感觉XYZ。尽管不能完全替换,但能实现一部分也是很好的。Blazor 主要是照顾像老周这种屌丝程序猴,一旦来项目了,一个人承担后台前台,连美工、图片处理都要一手包。所以用写后台代码的方式写前台更习惯。
上面一段全是废话!Blazor 只是在第一次运行的时候(不管是 Server 版还是 WASM 版)才会产生 HTTP 请求,之后的交互都由 Web Socket 来担任。往上说一层就是 SignalR。说直白一点就是,它只有第一次访问服务器才有 HttpContext,因为 Blazor 应用还没加载。一旦它加载成功了,后面就没 HttpContext 的事了。
结论:要取得 HttpContext 中需要的数据,只能在第一次请求时获得,然后你想尽办法让这些数据传给 Blazor 应用程序。本文老周将演示两种方法——基本能对付过去。
方法一:级联参数
Blazor 需要一个HTML页面来承载,然后 Page 之间的切换都在这个HTML页面上进行。项目模板默认使用 Razor Pages—— 生成一个名为 _Host 的页面(当然你改用 MVC 的视图来加载也可以,一样的原理)。然后使用 component 标记帮助器来加载 Blazor 应用程序。Blazor 应用是通过组件来构建的,一般会有一个名为 App 的组件,充当根组件。
<component type="typeof(App)" render-mode="ServerPrerendered" />
App 组件放个 Router,作用无非就是能找到 Blazor 应用页面就呈现,找不到就显示反馈。
<Router AppAssembly="@typeof(App).Assembly"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </Found> <NotFound> <PageTitle>Not found</PageTitle> <LayoutView Layout="@typeof(MainLayout)"> <p role="alert">Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router>
其实,Blazor 页面元素就是显示在 RouteView 组件下面的。所以,要使用级联参数组件,就可以把 CascadingValue 组件作为 RouteView 父级。
咱们先看一下数据的传递顺序:
1、要读 HttpContext 对象里的东西,获取数据的代码得在 _Host 页面上写;
2、在用 component 组件加载 App 组件时通过参数把数据传递给 App 内部;
3、App 组件内将数据传递给 CascadingValue;
4、各个 Blazor 页面组件都能够从 CascadingValue 组件中获取到数据。
HttpContext --> _Host.cshtml --> App.razor --> CascadingValue --> XXX
下面是实现过程:(假设我们要获取 URL 查询参数)
1、在 App 组件中定义名为 Version 的属性,字符串类型。要应用 Parameter 特性,说明它是一个组件参数,在 component 标记帮助器中可以传参。
2、把 RouteView 组件放到 CascadingValue 组件下面,并让它的值(Value)引用 Version 属性的值。
// App.razor 文件 <Router AppAssembly="@typeof(App).Assembly"> <Found Context="routeData"> <CascadingValue Name="ver" TValue="string" Value="@Version"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </CascadingValue> </Found> <NotFound> <PageTitle>Not found</PageTitle> <LayoutView Layout="@typeof(MainLayout)"> <p role="alert">Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> @code { [Parameter] public string? Version { get; set; } }
3、在 _Host.cshtml 文件中(或你自己定义的 MVC 视图文件)中,读取 URL 查询中的 “v” 字段的值,然后通过 component 组件把值传递 App 组件。
…… @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @{ Layout = "_Layout"; // 从URL参数中读参数 if(!HttpContext.Request.Query.TryGetValue("v",out var value)) { value = "0.0.0"; } } <component type="typeof(App)" render-mode="ServerPrerendered" param-Version="@value.ToString()" />
用 component 组件传递参数,可以用 param-* 特性,后面的 * 表示 App 组件接收参数的成员名称,这里是我们前面定义的 Version 属性,直接写成 param-Version 即可。如果属性名为 Name,那就写成 param-Name。
4、假设现在 Index.razor 组件要使用数据。需要在 Index 组件中定义一个属性成员,一定要应用 CascadingParameter 特性。注意这里 Name = "ver"。这个名字和刚才 App 组件中 CascadingValue 的 Name 是匹配的。
<p>接收到的数据:@Data</p> @code { [CascadingParameter(Name = "ver")] public string? Data{ get; set; } }
要呈现 Data 属性的内容,只需在 HTML 中引用即可。
在运行程序后,访问时加上 v=5.0.3,这个字段的值就能传到 Index 组件中。
二、单实例服务
这个方案是运用了依赖注入的功能,咱们定义一个类,它的属性用于存储参数。
public class PassDataService { public int Key1 { get; set; } public string? Key2 { get; set; } }
然后把它注册为单实例服务。
var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); …… builder.Services.AddSingleton<PassDataService>(); var app = builder.Build();
在 _Host.cshtml 文件或你自定义的 MVC 视图文件中,读出 HttpContext 中数据,然后设置到 PassDataService 实例的属性上。
@page "/" @namespace TestApp.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @inject PassDataService _datasv; @{ Layout = "_Layout"; // 读取URL查询 if(!HttpContext.Request.Query.TryGetValue("key1", out var kVal1)) { kVal1 = "0"; } if(!HttpContext.Request.Query.TryGetValue("key2", out var kVal2)) { kVal2 = string.Empty; } // 赋值 _datasv.Key1 = int.TryParse(kVal1, out int k1) ? k1 : 0; _datasv.Key2 = kVal2; } <component type="typeof(App)" render-mode="ServerPrerendered" />
@inject 指令可以注入需要的服务实例。
@inject <类型> <变量名>
在 Index 组件中,只要注入 PassDataService 服务实例,就能获取到数据。
@page "/" @inject PassDataService _datasv <PageTitle>Index</PageTitle> <h1>Hello, world!</h1> <p>获取到的数据:</p> <div>Key1 = @_datasv.Key1</div> <div>Key2 = @_datasv.Key2</div>
运行应用程序,在 URL 后面加上 key1 和 key2 参数。就得到传递的数据。