【ASP.NET Core】修改Blazor.Server的Hub地址後引發的問題

来源:https://www.cnblogs.com/tcjiaan/archive/2023/03/11/17206566.html
-Advertisement-
Play Games

Blazor Server,即運行在伺服器上的 Blazor 應用程式,它的優點是應用程式在首次運行時,客戶端不需要下載運行時。但它的代碼是在伺服器上執行的,然後通過 SignalR 通信來更新客戶端的 UI,所以它要求必須建立 Web Socket 連接。 用於 Blazor 應用的 Signal ...


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 會啟動失敗。

好了,今天就說到這兒了,主要是發現了一個“八阿哥”。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.全局設置用戶名和郵箱 因為平時除了開發公司項目還會寫自己的項目或者去維護開源項目,一般情況下,公司會要求提交代碼時使用自己的真名或者拼音和公司郵箱,以前就只會設置全局用戶名或郵箱如下 git config --global user.name "username" git config --gl ...
  • 變數和數據類型 所有定義的變數都存在記憶體中,定義變數需要記憶體空間,不同類型的變數需要的記憶體空間是不同的 數據類型作用:告訴編譯器,我這個數據在記憶體中需要多大的空間,編譯器預算對象(變數)分配的記憶體空間大小。 1.常量與變數 1.1 常量 常量:程式運行中不能改變的量 整型常量:1 200 字元常量: ...
  • VS調試以及Qt基本使用 1.彙編語言 1.1 VS中C語言嵌套彙編代碼(瞭解) #include <stdio.h> int main() { //定義整型變數a, b, c int a; int b; int c; __asm { mov a, 3 //3的值放在a對應記憶體的位置 mov b, ...
  • 深夜檔分享,給大家介紹一個黑白的、“驚悚”的網站! 從名字來看(killed by microsoft),是不是猜到點端倪了? 這個神奇的網站居然收錄了微軟壽終正寢的那些軟體。這是一個免費的開放源碼列表,其中列出了已停產的微軟服務、產品、設備和應用程式。網站的目標是成為有關微軟已死項目歷史的真實信息 ...
  • 環境 odoo-14.0.post20221212.tar ORM API學習總結/學習教程 模型(Model) Model欄位被定義為model自身的屬性 from odoo import models, fields class AModel(models.Model): _name = 'a. ...
  • token解決cookie的弊端 cookie的弊端 **弊端一:**瀏覽器請求過伺服器後,下一次訪問時伺服器就會通過瀏覽器cookie中攜帶的sessionID去尋找對應session,但是如果伺服器做了負載均衡,用戶下一次請求可能會被定向到其他伺服器節點,那台伺服器上沒有用戶session信息, ...
  • C語言概述 1.什麼是C語言 C語言就是人和電腦交流的一種語言 語言是用來交流溝通的。有一方說,有另一方聽,必須有兩方參與,這是語言最重要的功能: 說的一方傳遞信息,聽的一方接收信息; 說的一方下達指令,聽的一方遵循命令做事情。 語言是人和人交流,C語言是人和機器交流。只是,人可以不聽另外一個人, ...
  • 1. 對象重用 1.1. 原因 1.1.1. 許多對象的初始化成本很高,權衡了增加的GC時間之後,還是重用對象的效率更高 1.2. 只適用於初始化成本很高且數量較少的一組對象 1.2.1. 被重用的對象會在堆中停留很長時間。如果堆中有大量對象,創建新對象的空間就更少了,因此GC操作會更頻繁。 1.3 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...