Angular: 如何从HttpClient下载文件?

81

我需要从我的后端下载一个Excel文件,它会返回一个文件。

但是当我请求时,我得到了以下错误:

类型错误:你提供的地方应该是流(stream),而你提供了 'undefined'。你可以提供 Observable、Promise、Array 或 Iterable。

我的代码如下:

this.http.get(`${environment.apiUrl}/...`)
      .subscribe(response => this.downloadFile(response, "application/ms-excel"));

我尝试使用get和map(...),但没有起作用。

详情: Angular 5.2

参考资料:

import { HttpClient } from '@angular/common/http';
import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/finally';
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/catch';

响应的Content-Type:

Content-Type: application/ms-excel

有什么问题吗?


错误出现在哪一行和哪一列? - c69
3
需要将 downloadFile(....) 函数发布吗? - Sanoj_V
可能是如何在Angular 4中下载Excel/Zip文件的重复问题。 - Sanoj_V
我尝试了那个链接中的回答,但效果不佳。在这里,我找到了解决方案! - Jean Carlos
9个回答

137

后端返回Blob文件类型。以下函数接受任何文件类型并弹出下载窗口:

downloadFile(route: string, filename: string = null): void{

    const baseUrl = 'http://myserver/index.php/api';
    const token = 'my JWT';
    const headers = new HttpHeaders().set('authorization','Bearer '+token);
    this.http.get(baseUrl + route,{headers, responseType: 'blob' as 'json'}).subscribe(
        (response: any) =>{
            let dataType = response.type;
            let binaryData = [];
            binaryData.push(response);
            let downloadLink = document.createElement('a');
            downloadLink.href = window.URL.createObjectURL(new Blob(binaryData, {type: dataType}));
            if (filename)
                downloadLink.setAttribute('download', filename);
            document.body.appendChild(downloadLink);
            downloadLink.click();
        }
    )
}

10
非常好的回答!我还想补充一点,就是在 "downloadLink.click();" 后面加上 "downloadLink.parentNode.removeChild(downloadLink);",以保持清晰明了。 - Plamen
在尝试了这个页面上的几个建议之后,这个答案的变体解决了我的问题。 - JoSSte
5
为什么要将 'blob' 转化为 'json'?我认为你只需要写成 responseType: 'blob' - Mojtaba
1
一个问题:- 为什么我们不直接在这里使用标签? - Deepak Dholiyan
抱歉,在Edge浏览器上无法处理.msg和.eml文件类型,那么该怎么办呢? - Rohit Kumar
显示剩余6条评论

80

试试像这样:

type: application/ms-excel

/**
 *  used to get file from server
 */

this.http.get(`${environment.apiUrl}`,{
          responseType: 'arraybuffer',headers:headers} 
         ).subscribe(response => this.downLoadFile(response, "application/ms-excel"));


    /**
     * Method is use to download file.
     * @param data - Array Buffer data
     * @param type - type of the document.
     */
    downLoadFile(data: any, type: string) {
        let blob = new Blob([data], { type: type});
        let url = window.URL.createObjectURL(blob);
        let pwa = window.open(url);
        if (!pwa || pwa.closed || typeof pwa.closed == 'undefined') {
            alert( 'Please disable your Pop-up blocker and try again.');
        }
    }

4
我在使用Angular 9时遇到了一个错误:类型'"arraybuffer"'不能赋值给类型'"json"'.ts(2322) http.d.ts(1097, 9): 期望的类型来源于属性'responseType'。 - programmer-man
@programmer-man,你现在需要做的是类似这样:resposeType: 'arrayheaders' as 'json',请参考Hasan的回答。 - Sean Halls
1
我修改了下载文件,可以设置文件名,并且对我来说完美无缺: downLoadFile(data: any, type: string) { const fileName = 'file1.xlsx'; const a = document.createElement('a'); document.body.appendChild(a); a.style = 'display: none'; const blob = new Blob([data], {type: type}); const url = window.URL.createObjectURL(blob); a.href = url; a.download = fileName; a.click(); window.URL.revokeObjectURL(url); } - gabrielrincon

27

由于我使用的是Angular 8(已测试到13),因此实现其他响应花费了我一些时间。最终,我得出了以下代码(深受Hasan启发)。

请注意,为了设置名称,标头 Access-Control-Expose-Headers 必须包括 Content-Disposition 。 在Django RF中设置如下:

http_response = HttpResponse(package, content_type='application/javascript')
http_response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
http_response['Access-Control-Expose-Headers'] = "Content-Disposition"

在 Angular 中:

  // component.ts
  // getFileName not necessary, you can just set this as a string if you wish
  getFileName(response: HttpResponse<Blob>) {
    let filename: string;
    try {
      const contentDisposition: string = response.headers.get('content-disposition');
      const r = /(?:filename=")(.+)(?:;")/
      filename = r.exec(contentDisposition)[1];
    }
    catch (e) {
      filename = 'myfile.txt'
    }
    return filename
  }

  
  downloadFile() {
    this._fileService.downloadFile(this.file.uuid)
      .subscribe(
        (response: HttpResponse<Blob>) => {
          let filename: string = this.getFileName(response)
          let binaryData = [];
          binaryData.push(response.body);
          let downloadLink = document.createElement('a');
          downloadLink.href = window.URL.createObjectURL(new Blob(binaryData, { type: 'blob' }));
          downloadLink.setAttribute('download', filename);
          document.body.appendChild(downloadLink);
          downloadLink.click();
        }
      )
  }

  // service.ts
  downloadFile(uuid: string) {
    return this._http.get<Blob>(`${environment.apiUrl}/api/v1/file/${uuid}/package/`, { observe: 'response', responseType: 'blob' as 'json' })
  }


是的,它可以工作,我已经使用了几个星期了,但我发现它只能处理到一定的文件大小。如果返回的数据太大(比如1-2MB),下载窗口就不会出现。此外:即使它能工作,在真正的大文件上,你也要等到所有数据接收完毕才能看到保存对话框。这不是一个真正的下载... - Satria
3
谢谢,它有效了,除了正则表达式外。我正在使用asp.net,因此Content-Disposition有点不同,我的看起来像这样:'/(?:filename=)(.+)(?:;)/'。 - Eluvatar
1
谢谢!可以确认这也适用于Angular 13。 - Matt List
请喜欢这个答案,因为它使用从服务器发送的文件名,但是当文件名包含空格时,正则表达式会失败,因此我尝试修复正则表达式为 (?:filename="?)([^"]+)(?:("?;))。在regex101上进行快速测试。 - Mihir

9

使用API(Excel文件)中的Blob输出

并修改了@gabrielrincon的答案

downloadExcel(): void {
const payload = {
  order: 'test',
  };

this.service.downloadExcel(payload)
  .subscribe((res: any) => {
    this.blobToFile(res, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "Export.xlsx");
  });}

将 blob 转换为文件的通用函数

blobToFile(data: any, type: string, fileName: string) {
 const a = document.createElement('a');
 document.body.appendChild(a);
 a.style.display = 'none';
 const blob = new Blob([data], { type: type });
 const url = window.URL.createObjectURL(blob);
 a.href = url; a.download = fileName; a.click();
 window.URL.revokeObjectURL(url);}

在blob转换为文件的功能中,我们期望第一个参数是我们的blob数据,文件类型和包括扩展名的文件名1。我们正在创建一个HTML a标签元素2。然后将该元素附加到HTML 3中。然后隐藏a标签元素4。然后使用文件和类型创建新的blob对象5。我们将把blob对象转换为URL6。然后将该URL添加到我们的a标签的href属性7。我们正在打开我们的URL窗口,以便下载。


请问您能解释一下blobToFile函数吗? - cNgamba
  1. 在 blob 转文件函数中,我们期望第一个参数是我们的 blob 数据,文件类型和包括扩展名的文件名
  2. 我们创建了一个 HTML 的 a 标签元素
  3. 然后将元素附加到 HTML 中
  4. 然后隐藏 a 标签元素
  5. 接着使用文件和类型创建新的 blob 对象
  6. 我们将 blob 对象转换为 URL
  7. 然后将该 URL 添加到我们 a 标签的 href 属性中,在窗口中打开该 URL,以便下载。
- Shabeer M
谢谢。顺便问一下,我怎样才能在出现错误时显示一条消息? - cNgamba
例如,如果找不到文件,我想在我的用户界面上显示一条消息。 - cNgamba

5

当我搜索“rxjs使用post下载文件”时,结果带我来到了这里。

这是我的最终产品。它使用服务器响应中给定的文件名和类型。

import { ajax, AjaxResponse } from 'rxjs/ajax';
import { map } from 'rxjs/operators';

downloadPost(url: string, data: any) {
    return ajax({
        url: url,
        method: 'POST',
        responseType: 'blob',
        body: data,
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'text/plain, */*',
            'Cache-Control': 'no-cache',
        }
    }).pipe(
        map(handleDownloadSuccess),
    );
}


handleDownloadSuccess(response: AjaxResponse) {
    const downloadLink = document.createElement('a');
    downloadLink.href = window.URL.createObjectURL(response.response);

    const disposition = response.xhr.getResponseHeader('Content-Disposition');
    if (disposition) {
        const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
        const matches = filenameRegex.exec(disposition);
        if (matches != null && matches[1]) {
            const filename = matches[1].replace(/['"]/g, '');
            downloadLink.setAttribute('download', filename);
        }
    }

    document.body.appendChild(downloadLink);
    downloadLink.click();
    document.body.removeChild(downloadLink);
}

4
在花费了很多时间寻找答案后,我终于在这个网站 Angular HttpClient Blob 找到了一个漂亮的答案,解决了如何从我的 Node.js API restful 服务器下载简单图像到 Angular 组件应用程序的问题。它基本上包括以下内容: API Node.js restful:
   /* After routing the path you want ..*/
  public getImage( req: Request, res: Response) {

    // Check if file exist...
    if (!req.params.file) {
      return res.status(httpStatus.badRequest).json({
        ok: false,
        msg: 'File param not found.'
      })
    }
    const absfile = path.join(STORE_ROOT_DIR,IMAGES_DIR, req.params.file);

    if (!fs.existsSync(absfile)) {
      return res.status(httpStatus.badRequest).json({
        ok: false,
        msg: 'File name not found on server.'
      })
    }
    res.sendFile(path.resolve(absfile));
  }

Angular 6测试组件服务(在我的例子中为EmployeeService):

  downloadPhoto( name: string) : Observable<Blob> {
    const url = environment.api_url + '/storer/employee/image/' + name;

    return this.http.get(url, { responseType: 'blob' })
      .pipe(
        takeWhile( () => this.alive),
        filter ( image => !!image));
  }

模板

 <img [src]="" class="custom-photo" #photo>

组件订阅者和使用:

@ViewChild('photo') image: ElementRef;

public LoadPhoto( name: string) {
    this._employeeService.downloadPhoto(name)
          .subscribe( image => {
            const url= window.URL.createObjectURL(image);
            this.image.nativeElement.src= url;
          }, error => {
            console.log('error downloading: ', error);
          })    
}

3
也许我来晚了。但是@Hasan的最后一个答案非常好。
我只做了一点修改(原本无法接受标头,所以将其删除),然后就成功了。
downloadFile(route: string, filename: string = null): void {
    // const baseUrl = 'http://myserver/index.php/api';   
    this.http.get(route, { responseType: 'blob' }).subscribe(
      (response: any) => {
        let dataType = response.type;
        let binaryData = [];
        binaryData.push(response);
        let downloadLink = document.createElement('a');
        downloadLink.href = window.URL.createObjectURL(new Blob(binaryData, { type: dataType }));
        if (filename) {
          downloadLink.setAttribute('download', filename);
        }
        document.body.appendChild(downloadLink);
        downloadLink.click();
      }
    )
  }

2

使用 Blob 作为 img 的来源:

模板:

<img [src]="url">

组件:

 public url : SafeResourceUrl;

 constructor(private http: HttpClient, private sanitizer: DomSanitizer) {
   this.getImage('/api/image.jpg').subscribe(x => this.url = x)
 }

 public getImage(url: string): Observable<SafeResourceUrl> {
   return this.http
     .get(url, { responseType: 'blob' })
     .pipe(
       map(x => {
         const urlToBlob = window.URL.createObjectURL(x) // get a URL for the blob
         return this.sanitizer.bypassSecurityTrustResourceUrl(urlToBlob); // tell Anuglar to trust this value
       }),
     );
 }

关于信任安全值的更多参考资料。


我需要关于SafeResourceUrlbypassSecurityTrustResourceUrl的那些位来解决我遇到的问题。 - Zarepheth

1

保持简单,我对所有复杂的解决方案感到惊讶。

如果您的后端在其Content-Disposition中返回attachment标志和filename属性,则可以简单地进行此JavaScript调用。

window.location = `${environment.apiUrl}/...`;

浏览器会在不改变当前页面的情况下下载文件。


这是否适用于API授权? - user1274820

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接