【ASP.NET Core】修改Blazor.Server的Hub地址后引发的问题

asp,net,core,修改,blazor,server,hub,地址,引发,问题 · 浏览次数 : 435

小编点评

`_initializers` 是一个内部的属性,用于存储 JavaScript 初始化脚本。 `context.Response.WriteAsJsonAsync()` 函数用于将 `_initializers` 的 JSON 数据写入 HTTP 响应中。 `_initializers` 是一个 JSON 数组,里面包含要注入的 JavaScript 脚本的名称。 `context.Request.Path` 获取请求路径,`context.Request.Path.StartsWithSegments()` 检查路径是否以 `/_blazor` 开头。 `context.Request.Path.Replace()` 用新地址替换 `_blazor` 开头的路径部分。 `context.Request.Path` 将根据路径重新构建成正确的路径。 `context.Response.WriteAsJsonAsync()` 函数将重新构建的路径作为 JSON 数据写入 HTTP 响应中。

正文

Blazor Server,即运行在服务器上的 Blazor 应用程序,它的优点是应用程序在首次运行时,客户端不需要下载运行时。但它的代码是在服务器上执行的,然后通过 SignalR 通信来更新客户端的 UI,所以它要求必须建立 Web Socket 连接。

用于 Blazor 应用的 SignalR Hub 是 ComponentHub,默认的连接地址是 /_blazor。多数时候我们不需要修改它,但人是一种喜欢折腾的动物,既然 MapBlazorHub 方法的重载也允许我们修改地址,那咱们何不试试。

app.MapBlazorHub("/myapp");
app.MapFallbackToPage("/_Host");

我把 ComponentHub 的通信地址改为 /myapp。这时候,客户端上就不能使用 blazor.server.js 中的默认行为了,咱们必须手动启动 Blazor 应用了(因为自动启动用的是默认的 /_blazor 地址)。

<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
    Blazor.start({
        configureSignalR: (connbuilder) => {
            connbuilder.withUrl("myapp");
        }
    });
</script>

在引用 blazor.server.js 文件时,加上一个 autostart = "false",表示 blazor 应用手动启动。哦,这个 autostart 是怎么来的?来,咱们看看源代码。在 BootCommon.ts 文件中,定义有一个名为 shouldAutoStart 的函数,而且它已导出。看名字就知道,它用来判断是否自动启动 Blazor 应用。

export function shouldAutoStart(): boolean {
  return !!(document &&
    document.currentScript &&
    document.currentScript.getAttribute('autostart') !== 'false');
}

现在,你明白这个 autostart 特性是怎么回事了吧。

在调用 Blazor.start 方法时咱们要设定一个配置项—— configureSignalR。它指定一个函数,函数的参数是 HubConnectionBuilder 对象。这是 signalR.js 中的类型。再调用 withUrl 方法更改连接地址,默认的代码是这样的。

const connectionBuilder = new HubConnectionBuilder()
  .withUrl('_blazor')
  .withHubProtocol(hubProtocol);

很遗憾的是,运行后发现并不成功。

 

其实咱们的代码并没有错,问题其实是出在 Blazor 自身的“八阿哥”上。别急,老周接下来一层层剥出这个问题,你会感叹,官方团队竟然会犯“高级错误”。

咱们先来解释这个奇葩的错误信息,什么JSON格式不对?什么无效的字符“<”?这个错误信息很容易误导你,咱们看看下面的图。

 

在请求到 blazor.server.js 脚本后,访问了一个 /initializers 地址。这个 fetch 是 Blazor 应用发出的,其目的是问一下服务器,在 Blazor 应用启动前后,有没用自定义的初始化脚本。

为啥会有这个请求?我们来看看服务器端的源代码。

public static ComponentEndpointConventionBuilder MapBlazorHub(
    this IEndpointRouteBuilder endpoints,
    string path,
    Action<HttpConnectionDispatcherOptions> configureOptions)
{
    ……

    var hubEndpoint = endpoints.MapHub<ComponentHub>(path, configureOptions);

    var disconnectEndpoint = endpoints.Map(
        (path.EndsWith('/') ? path : path + "/") + "disconnect/",
        endpoints.CreateApplicationBuilder().UseMiddleware<CircuitDisconnectMiddleware>().Build())
        .WithDisplayName("Blazor disconnect");

    var jsInitializersEndpoint = endpoints.Map(
        (path.EndsWith('/') ? path : path + "/") + "initializers/",
        endpoints.CreateApplicationBuilder().UseMiddleware<CircuitJavaScriptInitializationMiddleware>().Build())
        .WithDisplayName("Blazor initializers");

    return new ComponentEndpointConventionBuilder(hubEndpoint, disconnectEndpoint, jsInitializersEndpoint);
}

如你所见,当你调用 MapBlazorHub 方法时,它同时注册了两个终结点:

a、/_blazor/disconnect/:断开 SignalR 连接时访问,由 CircuitDisconnectMiddleware 中间件负责处理。

b、/_blazor/initializers/:对,这个就是咱们在浏览器中看到的那个,请求初始化脚本的。由 CircuitJavaScriptInitializationMiddleware 中间件负责处理。

老周改了 Hub 地址是 myapp,所以这两路径应变为 /myapp/disconnect/ 和 /myapp/initializers/。

这个 initialicaers/ 地址返回的数据要求是 JSON 格式的,是一个字符串数组,表示初始化脚本的文件路径。

咱们继续跟踪,找到 CircuitJavaScriptInitializationMiddleware 中间件。

public async Task InvokeAsync(HttpContext context)
{
    await context.Response.WriteAsJsonAsync(_initializers);
}

就这?对,就一行,_initializers 是选项类 CircuitOptions 的 JavaScriptInitializers 属性。

 internal IList<string> JavaScriptInitializers { get; } = new List<string>();

这厮还是 internal 的,也就是说你写的代码不能修改它。它是用 CircuitOptionsJavaScriptInitializersConfiguration 对象来设置的。

public void Configure(CircuitOptions options)
{
    var file = _environment.WebRootFileProvider.GetFileInfo($"{_environment.ApplicationName}.modules.json");
    if (file.Exists)
    {
        var initializers = JsonSerializer.Deserialize<string[]>(file.CreateReadStream());
        for (var i = 0; i < initializers.Length; i++)
        {
            var initializer = initializers[i];
            options.JavaScriptInitializers.Add(initializer);
        }
    }
}

老周解释一下:上面代码是说在 Web 目录下(默认就是静态文件专用的 wwwroot)下找到一个名为 {你的应用}.modules.json 的文件,然后读出来,再添加到 JavaScriptInitializers 属性中。

假如我们应用程序叫 BugApp,那么要找的这个JSON文件就是 BugApp.modules.json。这个JSON文件既可以自动生成,也可以你手动添加。

你在 wwwroot 目录下添加一个 js 文件,命名为 BugApp.lib.module.js。在生成项目时,会自动产生这个 JSON 文件。在生成后你是看不到 BugApp.modules.json 文件的,而是在 Debug|Release 目录下有个 BugApp.staticwebassets.runtime.json。

{
  "ContentRoots": [
    "C:\\XXXX\\BugApp\\wwwroot\\",
    "C:\\XXXX\\BugApp\\obj\\Debug\\net7.0\\jsmodules\\"
  ],
  "Root": {
    ……
      "BugApp.modules.json": {
        "Children": null,
        "Asset": {
          "ContentRootIndex": 1,
          "SubPath": "jsmodules.build.manifest.json"
        },
        "Patterns": null
      }
    },
    ……
  }
}

终于见到它了,它指向的是 obj 目录下的 jsmodules.build.manifest.json 文件,ContentRootIndex : 1 表示 ContentRoots 节点中的第二个元素,即 obj\Debug\net7.0\jsmodules\jsmodules.build.manifest.json。打开这个文件看看有啥。

[
  "BugApp.lib.module.js"
]

如果你找不到这个文件,说明你没有生成项目,生成一下就有了。注意,这个文件只有你【发布】项目后才会出现在 wwwroot 目录下的

看到没?就是一个 JSON 数组,然后列出我刚刚添加的 js 脚本。客户端访问 ./initializers 就是为了获得这个文件。现在你回想一下浏览器报的那个错误,是不是知道为什么会说无效的 JSON 文件了吧。

客户端所请求的地址仍是默认的 /_blazor/initializers/ ,而我已经改为 /myapp 了,它本应该请求 /myapp/initializers 的,可是,blazor 并没这么做。那,我们能在 js 代码中配置吗?唉!官方团队犯的“高级”错误,居然把 URL 写死在代码中。可以看看 JSInitializers.Server.ts 文件中是怎么写的。

export async function fetchAndInvokeInitializers(options: Partial<CircuitStartOptions>) : Promise<JSInitializer> {
  const jsInitializersResponse = await fetch('_blazor/initializers', {
    method: 'GET',
    credentials: 'include',
    cache: 'no-cache',
  });

  ……
}

你看是不是这样?都 TM 的硬编码了,还怎么配置?哦,还没回答一个问题:既然找到问题所在了,那为什么会报无效 JSON 格式的错误?答:因为 /_blazor 被我改了,所以请求 /_blazor/initializers 是 404 的,但,我们为了让 Blazor 能启动,调用了 MapFallbackToPage 方法作为后备。

app.MapFallbackToPage("/_Host");

这样就导致在访问 /_blazor/initializers 得到404后转而返回 /_Host,也就是说,/initializers 获取一个 HTML 文档,HTML 文档的第一个字符不就是“<”吗,所以就是无效字符了,不是JSON。

所以,你说,这不是“八阿哥”是啥?如果你非要改掉默认地址,又想正常获取初始化脚本,咋整?

A方案:下载 TypeScript 源码,自己修改,然后编译。

B方案:我们在 HTTP 管道上加个中间件,把 /myapp 改回 /_blazor。

这里老周演示一下 B 方案。

// Blazor signalR Hub 的自定义地址
const string NewBlazorHubUrl = "/myapp";
app.UseStaticFiles();
// 要在路由中间件之前改地址
app.Use(async (context, next) => 
{
    if(context.Request.Path.StartsWithSegments("/_blazor", StringComparison.OrdinalIgnoreCase))
    {
        var repl = context.Request.Path.ToString().Replace("/_blazor", NewBlazorHubUrl);
        context.Request.Path = repl;
    }
    await next();
});
// 注意顺序
app.UseRouting();

app.MapBlazorHub(NewBlazorHubUrl);
app.MapFallbackToPage("/_Host");

因为新地址是 /myapp 开头,我们只要把以 /_blazor 开头的地址改为 /myapp 开头就行了。这个中间件一定要在路由中间件之前改地址。改地址后再做路由匹配才有意义。

当然,想简洁一点的,还可以用 URL Rewrite。

var rwtopt = new RewriteOptions()
             .AddRewrite("^_blazor/(.+)", "myapp/$1", true);
app.UseRewriter(rwtopt);
app.UseRouting();

app.MapBlazorHub("/myapp");
app.MapFallbackToPage("/_Host");

app.Run();

替换时用的正则表达式,我们匹配 _blazor 后的内容,即 initializers,然后替换为 myapp + initializers。“$1”引用正则中匹配的分组,即“.+”,匹配一个以上任意字符。URL 重写时,不需要指定开头的“/”,所以处理的是 _blazor/... 而不是 /_blazor/...。

前面我们提到了 BugApp.lib.module.js 脚本。干脆咱们也写一个自定义脚本。在 wwwroot 目录下添加一个 BugApp.lib.module.js 文件。BugApp 是项目名称,你要根据实际来改。

export function beforeStart() {
    console.log("Blazor应用即将启动");
}

export function afterStarted() {
    console.log("Blazor应用已启动");
}

在这个脚本中,我们要导出两个函数:

beforeStart:在 Blazor 启动之前被调用。

afterStarted:在 Blazor 启动之后被调用。

现在,再次运行程序,用开发人员工具查看“控制台”消息,会看到这两条输出。

 

 想玩直观一点的话,也可以修改 HTML 文档。

export function beforeStart() {
    let ele = document.createElement("div");
    // 设置样式
    ele.style = 'color: green; margin-top: 16px';
    // 文本
    ele.textContent = "Blazor应用即将启动";
    document.body.append(ele);
}

export function afterStarted() {
    let ele = document.createElement("div");
    ele.style = 'color: orange; margin-top: 15px';
    ele.textContent = "Blazor应用已经启动";
    document.body.append(ele);
}

运行之后,页面上会动态加了两个 <div> 元素。

XXX.lib.module.js 这个文件名是固定的,如果想自定义文件名,或想返回多个 js 文件,可以自己手动处理。

在 wwwroot 目录下添加一个名为 BugApp.modules.json 的文件。

[
    "abc.js",
    "def.js",
    "opq.js"
]

以JSON数组的格式把你想用的初始化脚本写上。再次运行程序,就会下载这个文件,读取三个文件并将其下载。

 

你得注意,你指定的这些脚本必须是可访问,有效的,不然 Blazor 会启动失败。

好了,今天就说到这儿了,主要是发现了一个“八阿哥”。

与【ASP.NET Core】修改Blazor.Server的Hub地址后引发的问题相似的内容:

【ASP.NET Core】修改Blazor.Server的Hub地址后引发的问题

Blazor Server,即运行在服务器上的 Blazor 应用程序,它的优点是应用程序在首次运行时,客户端不需要下载运行时。但它的代码是在服务器上执行的,然后通过 SignalR 通信来更新客户端的 UI,所以它要求必须建立 Web Socket 连接。 用于 Blazor 应用的 Signal

【ASP.NET Core】标记帮助器——替换元素名称

标记帮助器不仅可以给目标元素(标记)插入(或修改)属性,插入自定义的HTML内容,在某些需求中还可以替换原来标记的名称。 比如我们在使用 Blazor 时很熟悉的 Component 标记帮助器。在 Razor 文档中你将使用 元素来设置要呈现的组件。而在实际处理时,会去掉

【ASP.NET Core】MVC控制器的各种自定义:修改参数的名称

在上一篇中,老周演示了通过实现约定接口的方式自定义控制器的名称。 至于说自定义操作方法的名称,就很简单了,因为有内置的特性类可以用。看看下面的例子。 [Route("[controller]/[action]")] public class StockController : Controller

【ASP.NET Core】MVC控制器的各种自定义:应用程序约定的接口与模型

从本篇起,老周会连发N篇水文,总结一下在 MVC 项目中控制器的各种自定义配置。 本文内容相对轻松,重点讨论一下 MVC 项目中的各种约定接口。毕竟你要对控制器做各种自定义时,多数情况会涉及到约定接口。约定接口的结构都差不多,均包含一个 Apply 方法,实现类需要通过这个方法修改关联的模型设置。

【ASP.NET Core】用配置文件来设置授权角色

在开始之前,老周先祝各个次元的伙伴们新春快乐、生活愉快、万事如意。 在上一篇水文中,老周介绍了角色授权的一些内容。本篇咱们来聊一个比较实际的问题——把用于授权的角色名称放到外部配置,不要硬编码,以方便后期修改。 由于要配置的东西比较简单,咱们并不需要存在数据库,而是用 JSON 文件配置就可以了。将

在 WPF 中集成 ASP.NET Core 和 WebView2 用于集成 SPA 应用

背景 我们有些工具在 Web 版中已经有了很好的实践,而在 WPF 中重新开发也是一种费时费力的操作,那么直接集成则是最省事省力的方法了。 修改项目文件 我们首先修改项目文件,让 WPF 项目可以包含 ASP.NET Core 的库,以及引用 WebView2 控件。

Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 1: 'no such table: Users'.

今天使用asp.net core + sqlite 创建了一个demo项目,本地运行一切正常。可以添加,修改,删除数据。一旦发布到服务器上(Linux系统)就报错,错误信息如下: ![](https://img2023.cnblogs.com/blog/2912666/202308/2912666-

造轮子之多语言管理

多语言也是我们经常能用到的东西,asp.net core中默认支持了多语言,可以使用.resx资源文件来管理多语言配置。但是在修改资源文件后,我们的应用服务无法及时更新,属实麻烦一些。我们可以通过扩展IStringLocalizer,实现我们想要的多语言配置方式,比如Json配置,PO 文件配置,E

【ASP.NET Core】MVC控制器的各种自定义:IActionHttpMethodProvider 接口

IActionHttpMethodProvider 接口的结构很简单,实现该接口只要实现一个属性即可——HttpMethods。该属性是一个字符串序列。 这啥意思呢?这个字符串序列代表的就是受支持的 HTTP 请求方式。比如,如果此属性返回 GET POST,那么被修饰的对象既支持 HTTP-GET

在FreeSQL中实现「触发器」和软删除功能

前言 最近做新项目,技术栈 AspNetCore + FreeSQL 这个ORM真的好用,文档也很完善,这里记录一下两个有关「触发器」的功能实现 修改实体时记录更新时间 模型代码 我的模型都是基于这个 ModelBase 派生的,自带三个属性字段 public abstract class Mode