Angular封装HttpClient文件下载

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();
    }
}
相关推荐
勘察加熊人2 天前
andrular输入框input监听值传递
angular.js
勘察加熊人2 天前
Angular解析本地json文件
javascript·json·angular.js
勘察加熊人3 天前
angular实现list列表和翻页效果
javascript·angular.js
界面开发小八哥3 天前
界面控件Kendo UI for Angular 2024 Q3亮点 - 全新的页面模板
前端·ui·界面控件·kendo ui·angular.js·ui开发
勘察加熊人5 天前
angular登录按钮输入框监听
angular.js
勘察加熊人5 天前
angular使用http实现get和post请求
前端·http·angular.js
勘察加熊人6 天前
angular实现dialog弹窗
javascript·ecmascript·angular.js
勘察加熊人6 天前
angular实现calculate计算器
angular.js
无敌喜之郎6 天前
Angular中ChangeDetectorRef.detectChanges是如何实现的,对比vue种的nextTick有何不同
前端·vue.js·angular.js·视图同步
界面开发小八哥6 天前
界面控件DevExpress JS & ASP.NET Core v24.1亮点 - 支持Angular 18
javascript·ui·asp.net·界面控件·angular.js·devexpress