今天老周要說的內容比較簡單,所以大伙伴們不必緊張,能識字的都能學會。
在開始之前先來一段廢話。
許多人都很關心,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。
可以了,再次運行,看效果。
然后點一下頁面上那個充滿誘惑的按鈕,下載文件。
好了,這樣弄基本咱們日常開發需求了。