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();
    }
}
相关推荐
执檀月夜游9 天前
HttpClientUtil 之 ApacheHttpClient 4.5.14替换 commons-httpclient 3.1
httpclient
界面开发小八哥9 天前
界面控件Kendo UI for Angular中文教程:如何构建带图表的仪表板?(三)
前端·ui·界面控件·kendo ui·angular.js·ui开发
布兰妮甜10 天前
Angular模块化应用构建详解
javascript·angular.js·模块化
疯一样的码农15 天前
如何使用Apache HttpClient来执行GET、POST、PUT和DELETE请求
http·httpclient
前端郭德纲17 天前
什么是Angular?
javascript·angular.js
疯一样的码农19 天前
如何使用Apache HttpClient发送带有HTML表单数据的POST请求
java·apache·httpclient
疯一样的码农19 天前
如何使用Apache HttpClient发送带有基本认证的HTTP请求
http·apache·httpclient
JerryXZR22 天前
Angular面试题汇总系列一
前端·javascript·angular.js
疯一样的码农23 天前
使用Apache HttpClient发起一个GET HTTP请求
网络协议·http·apache·httpclient
疯一样的码农23 天前
使用Apache HttpClient发起一个POST HTTP请求
网络协议·http·apache·httpclient