2

【ASP.NET Core】修改Blazor.Server的Hub地址后引发的问题 - 东邪独孤

 1 year ago
source link: https://www.cnblogs.com/tcjiaan/p/17206566.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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);

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

367389-20230311163857361-670794357.png

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

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

367389-20230311164555421-219463700.png

在请求到 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 启动之后被调用。

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

367389-20230311181737530-556558226.png

 想玩直观一点的话,也可以修改 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> 元素。

367389-20230311182414404-756513432.png

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

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

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

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

367389-20230311183038615-703704282.png

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK