Angular 文件下载进度
下载文件是 Web 应用的常见任务。这些文件可能是 PDF、ZIP 文件,也可能是其他任何您希望用户访问的二进制或文本文件。以下是 Angular 中下载文件的方法:使用简单的链接下载,或使用 JavaScript 下载(提供更多控制和进度指示)。
download
本文开发的操作符可以在 ngx-operators 库 📚 中找到。这是一个经过实践检验的 Angular RxJS 操作符集合。如果您能在 GitHub 上给它打个星⭐️,我将不胜感激,这有助于让更多人了解它。
角度下载链接
在 Angular 中,使用纯 HTML 代码即可轻松实现一个简单的下载链接。您将使用一个带有属性的锚标记指向文件href
。该download
属性告知浏览器不应继续访问链接,而是下载 URL 目标。您还可以指定其值来设置正在下载的文件的名称。
<a href="/downloads/archive.zip"
download="archive.zip">
archive.zip
</a>
您可以将以下任何属性与 Angular 绑定,以便动态设置 URL 和文件名:
<a [href]="download.url" [download]="download.filename">
{{ download.filename }}
</a>
较旧的浏览器(例如 Internet Explorer)可能无法识别该download
属性。在这种情况下,您可以在新的浏览器选项卡中打开下载文件,并将target
属性设置为_blank
。不过,请确保rel="noopener noreferrer"
在使用时始终包含target="_blank"
,以免暴露安全漏洞。
<a [href]="download.url" target="_blank" rel="noopener noreferrer">
{{ download.filename }}
</a>
如果没有该download
属性,则下载的文件名将完全取决于提供文件的服务器发送的 HTTP 标头Content-Dispositiondownload
。即使存在该属性,此标头中的信息也可能具有优先权。
推荐阅读:
构建一个好的下载...按钮? - Eric Bailey
基于链接的解决方案非常符合 HTML 标准,并让浏览器完成大部分工作。但是,如果您希望更好地控制下载过程,并希望显示一些自定义进度指示器,您也可以通过 Angular 的HttpClient下载文件。
使用 HttpClient 下载文件
在浏览器中,文件最好以Blob的形式表示:
Blob 对象表示一个 blob,它是一个类似文件的对象,包含不可变的原始数据
- MDN web 文档
通过指定responseType
选项,我们可以执行 GET 请求,返回表示下载文件的 blob。假设我们有一个指定对象DownloadService
正在执行此操作:
@Injectable({providedIn: 'root'})
export class DownloadService {
constructor(private http: HttpClient) {}
download(url: string): Observable<Blob> {
return this.http.get(url, {
responseType: 'blob'
})
}
}
然后,组件将能够调用此服务,订阅相应的可观察对象并最终保存文件,如下所示:
@Component({...})
export class MyComponent {
constructor(private downloads: DownloadService) {}
download(): void {
this.downloads
.download('/downloads/archive.zip')
.subscribe(blob => {
const a = document.createElement('a')
const objectUrl = URL.createObjectURL(blob)
a.href = objectUrl
a.download = 'archive.zip';
a.click();
URL.revokeObjectURL(objectUrl);
})
}
}
这里,我们在 Blob 到达时以编程方式创建一个锚标记。使用URL.createObjectURL我们可以生成指向 Blob 的下载链接。最后,我们click()
像用户使用常规浏览器下载链接一样使用该链接。文件下载完成后,我们将通过撤销创建的对象 URL 来丢弃该 Blob。
不过这种方法比较繁琐,而且可能无法在所有浏览器上流畅运行。因此,我建议你在保存 Blob 对象时使用流行的FileSaver.js库。保存过程只需一行代码:
import { saveAs } from 'file-saver';
download() {
this.downloads
.download('/downloads/archive.zip')
.subscribe(blob => saveAs(blob, 'archive.zip'))
}
如果您不想为此添加依赖项,而更愿意使用之前展示的手动方法,那么您不妨将用于保存 Blob 的代码重构到一个单独的服务中。在那里,您可能希望document
使用 Angular 的内置注入令牌DOCUMENT进行注入。您也可以为FileSaver.js创建自定义注入令牌URL
——另请参阅下文我们将如何为 FileSaver.js 执行此操作。
计算下载进度
通过在发出 HTTP 请求时将选项设置为observe
,events
我们不仅可以收到请求的最终响应主体,还可以访问中间的 HTTP 事件。Angular 中有多种类型的 HTTP 事件,全部合并在HttpEvent类型下。我们还需要显式传递该选项reportProgress
才能接收HttpProgressEvents。我们的 HTTP 请求最终将如下所示:
this.http.get(url, {
reportProgress: true,
observe: 'events',
responseType: 'blob'
})
由于我们不想将这些事件转发给每个组件,我们的服务必须做更多工作。否则,我们的组件就必须处理 HTTP 协议的细节——这就是服务的作用!因此,我们引入一个表示下载进度的数据结构:
export interface Download {
state: 'PENDING' | 'IN_PROGRESS' | 'DONE'
progress: number
content: Blob | null
}
ADownload
可以处于三种状态之一。要么它尚未开始,因此处于待处理状态。要么它已完成或仍在进行中。我们使用TypeScript 的联合类型来定义不同的下载状态。此外,下载有一个数字,表示下载进度,从 1 到 100。下载完成后,它将包含一个 Blob 作为其content
- 在此之前,此属性不可用,因此null
。
现在,我们希望将特定的 HTTP 事件抽象到我们新定义的数据结构中。这样,我们的组件就可以与底层网络协议解耦。由于我们要处理随时间推移而来的多个事件,因此 RxJS 操作符非常适合——那就让我们创建一个吧!
第一步是创建类型保护,帮助我们区分不同的 HTTP 事件。这样,我们就可以以类型安全的方式访问特定于事件的字段。
我们将重点关注事件HttpResponse和HttpProgressEvents。它们都包含 discriminator 字段,type
使我们能够轻松地在保护器中为类型断言返回布尔值。
import {HttpEvent, HttpEventType, HttpResponse, HttpProgressEvent} from '@angular/common/http'
function isHttpResponse<T>(event: HttpEvent<T>): event is HttpResponse<T> {
return event.type === HttpEventType.Response
}
function isHttpProgressEvent(event: HttpEvent<unknown>): event is HttpProgressEvent {
return event.type === HttpEventType.DownloadProgress
|| event.type === HttpEventType.UploadProgress
}
保护可以与简单的 if 语句一起使用,但是,TypeScript 会为我们缩小语句块内的事件类型:
const event: HttpEvent<Blob> = ...
console.log(event.loaded) // not accessible, possible compilation error
if (isHttpProgressEvent(event)) {
console.log(event.loaded) // type is narrowed, property is accessible
}
基于这些保护,我们现在可以创建自定义操作符了。它将利用scan操作符,该操作符允许我们累积来自可观察对象的连续值的状态。它最多需要两个参数:首先,我们提供一个函数,该函数将根据前一个状态和当前状态accumulator
计算下一个状态。其次,我们将传递一个表示初始状态的 。这将表示我们的下载处于待处理状态,没有任何进度或内容:Download
HttpEvent
seed
scan
Download
seed
{state: 'PENDING', progress: 0, content: null}
我们accumulator
将使用先前定义的保护来Download
通过来自 HTTP 事件的信息随时更新状态:
(previous: Download, event: HttpEvent<Blob>): Download => {
if (isHttpProgressEvent(event)) {
return {
progress: event.total
? Math.round((100 * event.loaded) / event.total)
: previous.progress,
state: 'IN_PROGRESS',
content: null
}
}
if (isHttpResponse(event)) {
return {
progress: 100,
state: 'DONE',
content: event.body
}
}
return previous
}
当遇到 时HttpProgressEvent
,我们会根据已加载的字节数和总字节数计算进度。当我们收到 ,HttpResponse
其主体中包含文件内容时,下载即完成。当收到除HttpProgressEvent
或 之外的任何其他事件时HttpResponse
,我们不会更改下载状态并按原样返回。这样,例如,我们可以将信息保留在progress
属性中,而其他不允许我们计算进度的事件可以暂时忽略。
scan
让我们最终定义与accumulator
and一起使用的自定义运算符seed
:
export function download(
saver?: (b: Blob) => void
): (source: Observable<HttpEvent<Blob>>) => Observable<Download> {
return (source: Observable<HttpEvent<Blob>>) =>
source.pipe(
scan((previous: Download, event: HttpEvent<Blob>): Download => {
if (isHttpProgressEvent(event)) {
return {
progress: event.total
? Math.round((100 * event.loaded) / event.total)
: previous.progress,
state: 'IN_PROGRESS',
content: null
}
}
if (isHttpResponse(event)) {
if (saver && event.body) {
saver(event.body)
}
return {
progress: 100,
state: 'DONE',
content: event.body
}
}
return previous
},
{state: 'PENDING', progress: 0, content: null}
)
)
}
请注意,此download
运算符接受一个可选参数saver
。一旦收到 HTTP 响应,就会使用 内部的下载内容调用此函数accumulator
。这使我们能够传递将下载内容持久化到文件的策略,而无需将运算符直接耦合到 FileSaver.js。
使用自定义运算符时,服务代码现在如下所示:
import { saveAs } from 'file-saver';
...
download(url: string, filename?: string): Observable<Download> {
return this.http.get(url, {
reportProgress: true,
observe: 'events',
responseType: 'blob'
}).pipe(download(blob => saveAs(blob, filename)))
}
解耦 FileSaver.js
通过将 FileSaver.js 排除在我们的自定义操作符之外,最终的代码更易于维护。download
无需模拟导入即可测试该saveAs
操作符(相关测试参见此处)。如果我们将相同的模式应用于服务,测试起来也同样简单。因此,让我们saveAs
在名为 的文件中创建一个自定义注入令牌来实现这一点saver.provider.ts
:
import { InjectionToken } from '@angular/core'
import { saveAs } from 'file-saver';
export type Saver = (blob: Blob, filename?: string) => void
export const SAVER = new InjectionToken<Saver>('saver')
export function getSaver(): Saver {
return saveAs;
}
然后使用令牌在 Angular 模块中注册提供程序:
import {SAVER, getSaver} from './saver.provider'
@NgModule({
...
providers: [
{provide: SAVER, useFactory: getSaver}
]
})
export class AppModule { }
然后我们的服务可以注入保存方法,从而实现更加松散的耦合:
@Injectable({providedIn: 'root'})
export class DownloadService {
constructor(
private http: HttpClient,
@Inject(SAVER) private save: Saver
) {}
download(url: string, filename?: string): Observable<Download> {
return this.http.get(url, {
reportProgress: true,
observe: 'events',
responseType: 'blob'
}).pipe(download(blob => this.save(blob, filename)))
}
}
显示进度条
让我们使用Angular Material 中的进度条来显示下载进度。我们将创建一个名为 的组件属性来绑定下载download$
。现在,组件只需将可观察的 download 赋给该属性即可:
@Component({...})
export class MyComponent {
download$: Observable<Download>
constructor(private downloads: DownloadService) {}
download(): void {
this.download$ = this.downloads.download(
'/downloads/archive.zip',
'archive.zip'
)
}
}
然后,我们可以通过AsyncPipe 结合 NgIf来订阅这个可观察对象。下载过程中,我们将以“缓冲”模式显示进度条(您也可以使用“查询”模式),否则进度条是确定的。然后可以轻松地从 中获取进度条的值Download
。
<mat-progress-bar *ngIf="download$ | async as download"
[mode]="download.state == 'PENDING' ? 'buffer' : 'determinate'"
[value]="download.progress">
</mat-progress-bar>
专业提示:如果您需要在模板中将某个对象映射到两个以上的值,或者三元语句无法满足您的需求:请将可观察对象映射到您需要的类型,或者使用自定义管道,而不是从模板中调用组件函数。这两种方法都很容易编写,更具声明性,并且性能更佳。
与往常一样,如果您有任何疑问,请随时在下方留言或在 Twitter 上关注我@n_mehlhorn。您也可以在 Twitter 上关注我,并加入我的邮件列表,以便及时了解新文章的发布,并获取有关 Angular 和 Web 开发的小技巧。
这是 StackBlitz 的运行演示。下载的文件只有 3MB,因此您可能需要启用节流功能才能看到更多进度条。
文章来源:https://dev.to/angular/angular-file-download-with-progress-985