Angular HttpClient 文件下载
- 前言
- HttpRequest.ts
- demo
- 后端接口koa2示例
- 功能优化实现下载进度监控
前言
使用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'] = headeroptions['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 = blobUrltempLink.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] : nulllet 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 = truethis.req.downFileBlobPromise(`koa2/download/2.txt`, param).then((res:any) => {this.exportLoading = falseif(res){this.toast.error(res.mess)}else{this.toast.success("下载成功")}}).catch(err => {this.exportLoading = falsethis.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.codectx.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'] = headeroptions['responseType'] = "arraybuffer"options['observe'] = "events"options["reportProgress"] = trueoptions = 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 = falseprivate 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 nullcase 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] : nulllet 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.resultconst 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 = blobUrltempLink.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();}
}