7

【ASP.NET Core】Blazor+MiniAPI完成文件下载

 2 years ago
source link: https://www.cnblogs.com/tcjiaan/p/15774567.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+MiniAPI完成文件下载

今天老周要说的内容比较简单,所以大伙伴们不必紧张,能识字的都能学会。

在开始之前先来一段废话。

许多人都很关心,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();

运行起来,测测效果。

367389-20220107122409241-1591120551.png

点一下页面上的链接,嗯,Perfect !

367389-20220107122558022-586161458.png

记事本打开看看下载的文件。

367389-20220107122653464-41361955.png

 当然了,你也可以像官方示例那样,用 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。

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

367389-20220107125100699-2121881446.png

 然后点一下页面上那个充满诱惑的按钮,下载文件。

367389-20220107125253982-418345652.png

好了,这样弄基本咱们日常开发需求了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK