【ASP.NET Core】Blazor+MiniAPI完成檔案下載

東邪獨孤發表於2022-01-07

今天老周要說的內容比較簡單,所以大夥伴們不必緊張,能識字的都能學會。

在開始之前先來一段廢話。

許多人都很關心,blazor 用起來如何?其實也沒什麼,做Web的無非就是後臺程式碼+前臺HTML(包含JS+CSS等)。Blazor 的初衷就是給我們們寫C#的人用的,儘管不能完全代替 JS,但起碼大多數情況下是可以的。某些特定情況下非用JS不可了,就使用.NET 與 JS 互操作就行了。不必大量使用,只在需要時用就行,不然會影響效能。這是什麼樣的場景呢?嗯,很熟悉的情場。

只要你以前寫過 Windows Forms 窗體專案就懂了。這就跟.NET 呼叫 Win32 API 一樣,大多數時候,你直接用.NET封裝的型別就能搞定,但某些情況下你還得呼叫Win32 API,一樣的道理。

雖然這幾年,JS的語法也有所增強,也有TS的擴充套件,但寫起來還是沒有C#爽。這是照顧我們們大多數“全能程式猿”而推出的,有幾家公司會專招一邦人來為你寫前端(更別指望會給你招個妹子),這麼人性化的公司可不多了。因此,Blazor 也不是什麼高大上的神器,但可以為我們們這些“萬能勞動力”減減壓而已。

----------------------------------------------------------------------------------------------------------------------

老周今天說的是 Blazor 中的檔案下載功能。其實,官方文件也給出了示例,你在開發過程完全可以照抄。抄程式碼也不是說一定是壞事,能夠利用現有資源就盡情地用,不要猶豫。你不可能自己生產出汽車然後才開車的,不然汽車工廠幹嗎去?所以,以前有一位黑客級大神總結出:

1、能用 Excel 解決的問題你寫個龜程式碼;

2、能用 PPT 解決的問題,你做啥視訊特效;

3、別人都做出來的軟體,你就用唄,何必自己造輪子;

4、借鑑(“抄”的雅稱)別人的程式碼前最好先摸清楚人家的思路,大概弄懂是個啥原理再用。

其實,Blazor只不過把一些常用的JS實現的功能用C#替代而已,Web 應用的基本原理是不變的。也就是說,在Blazor應用中,做出檔案下載功能的方法是很多滴。

官方示例的思路是:

A、伺服器生成 Stream 物件;

B、對生成的.NET 流物件進行封送,傳輸到客戶端(通過singalR),資料包裝進 Blob 物件中;

C、互操作方式呼叫預先定義好的 JS 函式,提取 Blob 中的資料(模擬點選 document 生成的 <a>標籤啟用下載)。

不管是 blazor server 還是 blazor webassembly 原理一樣。

 

老周補充一下這下方案,都是可行的。

A、寫一個MVC控制器(其實理解為 API 控制器也一樣,沒有View罷了),返回檔案內容,這個不難吧,然後在 Blazor 中只要利用一下指向此控制器的URL就行了,至於怎麼做嘛,你喜歡咋弄都行;

B、原理和上面一樣,只是不用寫個MVC控制器,我們們何不發揮一下那個簡練好用的 Mini-API 功能呢。

 

好了,前方精彩預警!

步驟1:我們建一個空白的 ASP.NET Core 應用專案。老周比較喜歡這個空白專案模板,靈活好用。ASP.NET Core 中所有技術都可以在同一個專案中融合使用。

步驟2:相信大家知道,C# 程式現在可以省略 Main 方法的定義,讓編譯器去生成預設程式碼。所以,ASP.NET Core 專案的程式碼比起過去版本一下子精簡了很多。開啟 Program.cs 檔案(專案生成的是這名字,若你有強迫症,可以改名)。在呼叫 Build 方法之前,為應用程式註冊以下服務。

var builder = WebApplication.CreateBuilder(args);
// 這些服務是必要的
builder.Services.AddServerSideBlazor();
// 我是圖方便,讓Razor頁的目錄直接設定於內容根目錄
builder.Services.AddRazorPages().WithRazorPagesAtContentRoot();
var app = builder.Build();

Blazor 應用優先選用伺服器端的,有特殊需求才考慮 Web Assembly。雖然不是什麼硬規矩,但 Web 應用的優良傳統都是伺服器承擔效能消耗,讓客戶端當上帝。故而,我們們要傳承 Web 應用的奉獻精神。

如果你剛接觸 Blazor,可能會疑惑,為什麼還要啟用 Razor Pages 功能呢?因為 Blazor 也是Web應用是吧,它是在HTML頁中載入的。嗯,你想一下,要是不先載入一個完整的HTML頁,Blazor 怎麼冒出來呢?所以,我們的應用程式要先載入一個“外殼”頁,然後再通過它來載入 Blazor 應用。

從這個模式我們們就知道了,Blazor 應用其實是單個HTML頁上的應用,Blazor 應用內的頁面切換隻是這個HTML頁面內部一些標籤的“輪換”罷了。即:Blazor 中的“頁”本質上是一個HTML元件;而HTML元件就是把一堆HTML標籤包起來,可以作為模板到處使用。這好比你的PC主機,有個機箱,把裡面的主機板、處理器、硬碟、記憶體、顯示卡什麼的全部裝好,當你要換個地方工作時,你只要搬動主機就行了,你不需要把記憶體、網路卡的都拆出來又重新組裝。

既然一個 Blazor 頁是一個元件,那麼,Blazor 應用在啟動後,是不是應該要有一個“控制中心”,來操縱不同元件之間的切換?雖然普通的元件也能作為 Blazor 應用載入,但不能在多個元件中導航了。所以,我們要先編寫這個“控制中心”,有了它,你就能到處穿越了,就像多拉B夢的時空門一樣。

一般,我們把這個充當“主謀”的元件命名為 App,Razor 元件的副檔名是.razor。所以,檔名就是 App.razor。來,我們們動手寫一下。

@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Routing
@using System.Reflection

<Router AppAssembly="typeof(Program).Assembly" Context="routedata">
    <Found>
        <RouteView RouteData="routedata" />
    </Found>
    <NotFound>
        <p>應用程式掛了……</p>
    </NotFound>
</Router>

前面的三個 @using 和 C# 中的 using 一個意思,引入我們們用到的名稱空間。當然了,如果你不想在每個元件檔案中都寫一遍,還可以在 App.razor 同級目錄下建一個名為 _Imports.razor 的檔案(首字母可大寫可小寫),然後把 @using 寫進去。

App 元件的根元素不是HTML元素,而是 Router 類,它可以根據應用內部的URL在不同元件間導航,客戶端瀏覽器的位址列不會變(前面說了,Blazor 是單頁面的)。AppAssembly 屬性指定 Blazor 元件要在哪個程式集中查詢,99.9996% 情況下都是我們當前專案所在程式集。Context 是個很有意思的屬性,它的功能是為當前元素(這裡是Router)所關聯的上下檔案物件分配一個變數名,這個名字你可以隨便取,這裡我命名為“routedata”,如果不指定,預設名字是“context”。

這裡頭啥意思呢?原來啊,元件中呈現元素是用一個叫“幀”的玩意兒來表示的。對應兩個委託型別:

delegate void RenderFragment(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
delegate Microsoft.AspNetCore.Components.RenderFragment RenderFragment<TValue>(TValue value)

注意到第二個委託有些意思,它返回了第一個委託型別的例項,但我們們最該關心的是它有個泛型引數 TValue,我們們上面所說的那個 Context 屬性,所關聯的上下文物件就是通過這個泛型引數來傳遞的。

傳遞上下文物件後能幹些啥呢?還是以我們們這個 App 元件來舉例。Router 接收到上下文物件(在執行的時候實際接收了被路由處理後的URL)後,Router 元素下面的子元素就可以訪問這個上下文物件了,而訪問方法就是引用 Context 屬性分配的變數名(此處是 routedata)。

Router 元素必須包含兩個子元素:

Found:如果從 AppAssembly 屬性所指定的程式集中找到了與路由規則匹配的 Blazor 元件,那麼,就把這個元件呈現在 RouteView 元素中;

NotFound:如果找不到匹配的元件,那就呈現它的子元素,這裡是一個“屁”元素,文字是“應用程式掛了……”。

 

步驟3:建一個新 Blazor 元件,名為 Home.razor,作為此 Blazor 應用的真正主頁。

@page "/"

<div>
    <p>
        下載檔案:
    </p>
    <a href="/download" target="_blank">點這裡</a>
</div>

作為 Blazor 的元件,要在首行明確標註 @page,“/”表示URL的根路徑,即預設開啟的“頁面”。

為了簡單演示,此處<a>元素指向了下載檔案的地址,點一下就開始下載。/download 指向一個 Mini-API,這個我們們到最後再寫。

 

步驟4:Blazor 元件完工了,接下來要弄一個 Razor 頁,它是一個完整的HTML文件,用來載入 Blazor 應用。命名為 appLoader.cshtml。注意,副檔名不同,不是 Razor 元件。

@page
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<html lang="zh-cn">
    <head>
        <meta charset="utf-8" />
        <base href="~/" />
    </head>
    <body>
        @*相關指令碼*@
        <script src="_framework/blazor.server.js"></script>
        @*載入啟動元件*@
        <component type="typeof(XXX.App)" render-mode="ServerPrerendered" />
    </body>
</html>

作為 Razor Page ,你懂的,首行要註明 @page,第二行是標記要使用 Tag Helper(標記幫助器)。因為稍後我們們要用 component 元素來載入 App 元件。

XXX是你那個 App 元件所在的名稱空間。有個重要的 JS 指令碼—— blazor.server.js,絕對不能忘了,否則客戶端無法啟動 Blazor 專用的 singnalR 連線。這個指令碼不在我們專案中,而包裝在.NET 類庫中,所以我們不用管它,記得引用就行。

 

步驟5:最後,我們們補全 Program.cs 中的程式碼。

// Blazor需要靜檔案的訪問
app.UseStaticFiles();
app.UseRouting();
// 此處比5.0簡練,不必通過Endpoint來新增對映
app.MapBlazorHub();
// blazor app 第一次訪問時,應用尚未載入,會404的
// 所以要先訪問一下某個page,讓這個page去載入app
app.MapFallbackToPage("/appLoader");

app.Run();

雖然我們們這專案中沒有 wwwroot 中的靜態資源,但JS要載入 blazor.server.js,獲取這個指令碼需要靜態檔案功能來支援。

MapBlazorHub 方法要記得呼叫,否則客戶端進來的 HTTP 請求無法由 Blazor 類庫來處理。

最下面一句 MapFallbackToPage 也很重要。前面我們們分析過,Blazor 應用需要一個完整的 HTML 頁面來載入,所以,當客戶端首次訪問根 URL(或其他元件URL)時,由於 Blazor 未啟動,元件無法載入。

所以,當首次訪問失敗時轉到 /appLoader 來載入並啟動 Blazor 應用。

 

步驟6:實現下載檔案的 Mini-API。

app.MapGet("/download", () =>
{
    // 隨機弄些玩意兒
    byte[] data = null;
    string txt = "床前明月光\n有逼就能裝\n手持玩具槍\n喝辣又吃香";
    data = System.Text.Encoding.UTF8.GetBytes(txt);
    return Results.File(data, "application/octet-stream", "abc.txt");
});

 

Program.cs 完整程式碼如下:

var builder = WebApplication.CreateBuilder(args);
// 這些服務是必要的
builder.Services.AddServerSideBlazor();
// 我是圖方便,讓Razor頁的目錄直接設定於內容根目錄
builder.Services.AddRazorPages().WithRazorPagesAtContentRoot();
var app = builder.Build();

// Mini-API,簡單檔案下載
app.MapGet("/download", () =>
{
    ……
});

// Blazor需要靜檔案的訪問
app.UseStaticFiles();
app.UseRouting();
// 此處比5.0簡練,不必通過Endpoint來新增對映
app.MapBlazorHub();
// blazor app 第一次訪問時,應用尚未載入,會404的
// 所以要先訪問一下某個page,讓這個page去載入app
app.MapFallbackToPage("/appLoader");

app.Run();

 

執行起來,測測效果。

 

 

點一下頁面上的連結,嗯,Perfect !

 

 

記事本開啟看看下載的檔案。

 

 

 當然了,你也可以像官方示例那樣,用 JS 動態建立個<a>標籤,然後模擬 Click。

來,我們們改一下。

在專案中新建一個目錄,命名為 wwwroot,然後在wwwroot下建一個指令碼檔案,命名為 test.js。用JS寫個函式。

function demoDown() {
    // 動態建立元素
    var ele = document.createElement("a");
    // 設定下載URL
    ele.href = '/download';
    ele.target = '_blank';
    // 模擬點選
    ele.click();
    ele.remove(); //沒有利用價值了,殺!
}

待會兒,我們得用互操作來呼叫這個JS函式。

開啟 appLoader.cshtml,改一下HTML,引用 test.js。

    <body>
        @*相關指令碼*@
        <script src="_framework/blazor.server.js"></script>
        <script src="~/test.js"></script>
        @*載入啟動元件*@
        <component type="typeof(SuatApp.App)" render-mode="ServerPrerendered" />
    </body>

 

再開啟 Home.razor 元件,改一下,把 a 元素改成 button。

@page "/"
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div>
    <p>
        下載檔案:
    </p>
    <button @onclick="OnClick">點這裡領取美人一名</button>
</div>

@code {
    private async Task OnClick()
    {
        // 互操作,呼叫JS函式
        await JS.InvokeVoidAsync("demoDown");
    }
}

@inject 用來獲取依賴注入的 JsRuntime 物件,在 OnClick 方法中用它來呼叫JS函式。被呼叫的 JS 函式就是我們剛剛寫的 demoDown。

 

可以了,再次執行,看效果。

 

 然後點一下頁面上那個充滿誘惑的按鈕,下載檔案。

 

 

好了,這樣弄基本我們們日常開發需求了。

 

相關文章