Angular HttpClient 文件下载
前言
使用Angular框架开发工作中,实现文件下载业务时,我们可以使用Angular自带的HttpClient。下面我们就封装一下HttpClient实现文件下载,当接口返回文件流正常下载,后端返回json错误信息时,前端可以获取到错误信息进行toast提示
HttpRequest.ts
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams, HttpUrlEncodingCodec } from "@angular/common/http";
import { Injectable, Component } from "@angular/core";
import { throwError } from "rxjs";
import { catchError, map } from "rxjs/operators";
import { environment } from "src/environments/environment";
@Injectable({
providedIn: 'root'
})
export class HttpRequest{
public downFileBlobPromise(url: string, data = {}) {
let options: any = {}
let header: { [name: string]: string } = {}
header['Content-Type'] = 'application/x-www-form-urlencoded'
header['Accept'] = '*/*'
options['headers'] = header
options['responseType'] = "blob"
options['observe'] = "response"
let obj = Object.assign({}, options, { params: data })
return new Promise((resolve, reject) => {
this.http.get(url, obj).subscribe(async res => {
const txt = await this.convertRes2Blob(res)
resolve(txt)
}, err => {
reject(err)
})
})
}
private async convertRes2Blob(response: any) {
if (!response.headers.has("content-disposition")) {
const blob = new Blob([response.body], { type: 'application/octet-stream' })
const resultJson = await this.readBlob(blob)
return resultJson
}
const fileName = this.getFileName(response)
const blob = new Blob([response.body], { type: 'application/octet-stream' })
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(blob, fileName)
return null
} else {
const blobUrl = window.URL.createObjectURL(blob)
const tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = blobUrl
tempLink.setAttribute('download', fileName)
document.body.appendChild(tempLink)
tempLink.click()
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(blobUrl)
return null
}
}
private getFileName(response: any) {
const encode = response.headers.get('content-type')?.match(/charset=(.*)/) ? response.headers.get('content-type').match(/charset=(.*)/)[1] : null
let fileName: string = response.headers.get('content-disposition').match(/filename=(.*)/)[1].replaceAll("\"", "")
if (encode && encode == 'ISO8859-1') {
const fn = escape(fileName)
fileName = decodeURI(escape(fileName)).replace(new RegExp("%3A", "gm"), ":")
} else {
fileName = decodeURI(fileName)
}
return fileName
}
private readBlob(blob:Blob){
const f = new FileReader()
f.readAsText(blob, "UTF-8")
return new Promise((resolve,reject) => {
f.onload = (evt: any) => {
const re = evt.target.result
const result = JSON.parse(re)
resolve(result)
}
f.onerror = (evt:any) => {
reject(evt)
}
})
}
}
demo
constructor(
private router: Router,
private service: AccountIdentifyService,
private confirmationService: ConfirmationService,
private toast: Toast,
private req: HttpRequest,
) { }
export() {
this.exportLoading = true
this.req.downFileBlobPromise(`koa2/download/2.txt`, param).then((res:any) => {
this.exportLoading = false
if(res){
this.toast.error(res.mess)
}else{
this.toast.success("下载成功")
}
}).catch(err => {
this.exportLoading = false
this.toast.showError("下载异常")
})
}
后端接口koa2示例
router.get("/download/:filename",async function(ctx,next){
const filename = ctx.params.filename;
if(filename != "1.txt"){
setTimeout(() => {
ctx.body = {
resultStat: "1",
mess:"文件不存在",
};
return
}, 5000);
}
//request里面切出标识符字符串
let requestUrl = ctx.request.originalUrl;
//获取资源文件的绝对路径
let filePath = path.resolve(__dirname + "/uploads/" + decodeURI(filename));
console.log(filePath);
let resHred = readFile(ctx.headers.range, filePath);
ctx.status = resHred.code
ctx.set(resHred.head);
ctx.set('Content-Disposition', `attachment; filename=${encodeURIComponent(filename)}`);
ctx.set('Content-Type', 'application/octet-stream');
let stream = fs.createReadStream(filePath, resHred.code == 200 ? {} : { start: resHred.start, end: resHred.end });
stream.pipe(ctx.res);
// //也可使用这种方式。
// stream.on('data', e => ctx.res.write(e));
// // 接收完毕
// stream.on('end', e => ctx.res.end());
ctx.respond = false;
return
})
文件util
const fs = require('fs');
const path = require('path');
function saveFile(file) {
const reader = fs.createReadStream(file.path);
const fileExtension = path.extname(file.name);
const uniqueFileName = `${Date.now()}${fileExtension}`;
const writer = fs.createWriteStream(path.join(__dirname, 'uploads', uniqueFileName));
reader.pipe(writer);
return uniqueFileName;
}
function getFileStream(filename) {
return fs.createReadStream(path.join(__dirname, '../uploads', filename));
}
/**
* [读文件]
* @param {String} range [数据起始位]
* @param {String} filePath [文件路径]
* @param {Number} chunkSize [每次请求碎片大小 (900kb 左右)]
*/
function readFile(range, filePath, chunkSize = 499999 * 2) {
//mime类型
const mime = {
"css": "text/css",
"gif": "image/gif",
"html": "text/html",
"ico": "image/x-icon",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"js": "text/javascript",
"json": "application/json",
"pdf": "application/pdf",
"png": "image/png",
"svg": "image/svg+xml",
"swf": "application/x-shockwave-flash",
"tiff": "image/tiff",
"txt": "text/plain",
"mp3": "audio/mp3",
"wav": "audio/x-wav",
"wma": "audio/x-ms-wma",
"wmv": "video/x-ms-wmv",
"xml": "text/xml",
"mp4": "video/mp4"
};
// 获取后缀名
let ext = path.extname(filePath);
ext = ext ? ext.slice(1) : 'unknown';
//未知的类型一律用"text/plain"类型
let contentType = mime[ext.toLowerCase()];
//建立流对象,读文件
let stat = fs.statSync(filePath)
let fileSize = stat.size;
let head = {
code: 200,
head: {
'Content-Length': fileSize,
'content-type': contentType,
}
};
console.log("range: ",range);
if (range) {
// 大文件分片
let parts = range.replace(/bytes=/, "").split("-");
let start = parseInt(parts[0], 10);
let end = parts[1] ? parseInt(parts[1], 10) : start + chunkSize;
end = end > fileSize - 1 ? fileSize - 1 : end;
chunkSize = (end - start) + 1;
head = {
code: 206,
filePath,
start,
end,
head: {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'content-type': contentType,
'Content-Length': chunkSize,
'Accept-Ranges': 'bytes'
}
}
}
return head;
}
module.exports = {
saveFile,
getFileStream,
readFile
}
功能优化实现下载进度监控
import { HttpClient, HttpEvent, HttpEventType } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { environment } from "@env/environment";
import { filter, map, tap } from "rxjs/operators";
import { TlMessageService } from "./message.service";
@Injectable({
providedIn: 'root'
})
export class FileHttpRequest {
constructor(private http: HttpClient, private message: TlMessageService) {
}
download(url: string, params = {}, progress = false) {
const urlPrefix = environment.urlPrefix;
if(url.startsWith('./')){
url = url.substring(1)
}
if(!url.startsWith('/')){
url = "/" + url
}
const ignore = ["/TsmAas","/portal","/asset","/koa2"]
if(!ignore.includes("/" + url.split("/")[1])){
url = url.startsWith('/') ? (urlPrefix + url) : (urlPrefix + '/' + url)
}
let options: any = {}
let header: { [name: string]: string } = {}
header['Content-Type'] = 'application/x-www-form-urlencoded'
header['Accept'] = '*/*'
options['headers'] = header
options['responseType'] = "arraybuffer"
options['observe'] = "events"
options["reportProgress"] = true
options = Object.assign(options, { params })
console.log(options);
return new Promise((resolve, reject) => {
this.http.get(url, options).pipe(
map(event => this.getEventMessage(event, progress)),
filter(f => f != null)
).subscribe(async res => {
const txt = await this.getFileFromStream(res)
resolve(txt)
}, err => {
console.log(err);
reject(err)
})
})
}
status = false
private getEventMessage(event: any, progress = false): ArrayBuffer {
switch (event.type) {
case HttpEventType.ResponseHeader:
if (event.status !== 200) {
this.status = false
} else {
if (progress) {
this.message.send({
type: "downloadStart",
content: ""
})
}
this.status = true
}
return null
case HttpEventType.DownloadProgress:
const percentDone = Math.round(100 * event.loaded / event.total);
console.log(event, percentDone);
if (progress && this.status) {
if (percentDone >= 100) {
this.message.send({
type: "downloading",
content: "100"
})
setTimeout(() => {
this.message.send({
type: "downloadEnd",
content: "100"
})
}, 1500);
} else {
this.message.send({
type: "downloading",
content: percentDone + "",
})
}
}
return null;
case HttpEventType.Response:
return event;
default:
return null;
}
}
private getFileName(response: any) {
const encode = response.headers.get('content-type')?.match(/charset=(.*)/) ? response.headers.get('content-type').match(/charset=(.*)/)[1] : null
let fileName: string = response.headers.get('content-disposition').match(/filename=(.*)/)[1].replaceAll("\"", "")
if (encode && encode == 'ISO8859-1') {
const fn = escape(fileName)
fileName = decodeURI(escape(fileName)).replace(new RegExp("%3A", "gm"), ":")
} else {
fileName = decodeURI(fileName)
}
return fileName
}
private readBlob(blob: Blob) {
const f = new FileReader()
f.readAsText(blob, "UTF-8")
return new Promise((resolve, reject) => {
f.onload = (evt: any) => {
const re = evt.target.result
const result = JSON.parse(re)
resolve(result)
}
f.onerror = (evt: any) => {
reject(evt)
}
})
}
private async getFileFromStream(response: any) {
if (!response.headers.has("content-disposition")) {
const blob = new Blob([response.body], { type: 'application/octet-stream' })
const resultJson = await this.readBlob(blob)
return resultJson
}
const fileName = this.getFileName(response)
const blob = new Blob([response.body], { type: 'application/octet-stream' })
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(blob, fileName)
return null
} else {
const blobUrl = window.URL.createObjectURL(blob)
const tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = blobUrl
tempLink.setAttribute('download', fileName)
document.body.appendChild(tempLink)
tempLink.click()
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(blobUrl)
return null
}
}
}
TlMessageService 消息订阅,传递文件下载进度
import { Injectable } from "@angular/core";
import { BehaviorSubject, Observable, Subject } from "rxjs";
export type TlMessage = {
type: "success" | "error" | "warn" | "info" | "upload" | "downloadStart" | "downloading" | "downloadEnd",
content: string
}
@Injectable({
providedIn: 'root',
})
export class TlMessageService {
//private subject = new Subject<any>();
private subject = new BehaviorSubject<TlMessage>({type:"info",content:""});
send(message: TlMessage) {
this.subject.next(message);
}
get(): Observable<TlMessage> {
return this.subject.asObservable();
}
}