Angular 6从rest api下载文件

80

我有一个REST API,我将我的pdf文件放在其中,现在我希望我的angular应用程序通过我的Web浏览器在单击时下载它,但我遇到了HttpErrorResponse。

"JSON中的位置0处的意外令牌%"

"SyntaxError:在JSON中的位置0处出现意外令牌%\n 在JSON.parse(

这是我的端点

    @GetMapping("/help/pdf2")
public ResponseEntity<InputStreamResource> getPdf2(){

    Resource resource = new ClassPathResource("/pdf-sample.pdf");
    long r = 0;
    InputStream is=null;

    try {
        is = resource.getInputStream();
        r = resource.contentLength();
    } catch (IOException e) {
        e.printStackTrace();
    }

        return ResponseEntity.ok().contentLength(r)
                .contentType(MediaType.parseMediaType("application/pdf"))
                .body(new InputStreamResource(is));

}

这是我的服务

  getPdf() {

this.authKey = localStorage.getItem('jwt_token');

const httpOptions = {
  headers: new HttpHeaders({
    'Content-Type':  'application/pdf',
    'Authorization' : this.authKey,
    responseType : 'blob',
    Accept : 'application/pdf',
    observe : 'response'
  })
};
return this.http
  .get("http://localhost:9989/api/download/help/pdf2", httpOptions);

}

并调用

this.downloadService.getPdf()
  .subscribe((resultBlob: Blob) => {
  var downloadURL = URL.createObjectURL(resultBlob);
  window.open(downloadURL);});
8个回答

106

我解决了它,方法如下:

// header.component.ts
this.downloadService.getPdf().subscribe((data) => {

  this.blob = new Blob([data], {type: 'application/pdf'});

  var downloadURL = window.URL.createObjectURL(data);
  var link = document.createElement('a');
  link.href = downloadURL;
  link.download = "help.pdf";
  link.click();

});



//download.service.ts
getPdf() {

  const httpOptions = {
    responseType: 'blob' as 'json')
  };

  return this.http.get(`${this.BASE_URL}/help/pdf`, httpOptions);
}

6
我不喜欢发表简单的感谢评论,因为它们通常没有附加价值……但是这一次我必须这样做。今天我已经花费了太多小时在努力搜索这个。我一直在搜索你从this.bloblink.click()的代码,你帮我省下了一个彻夜未眠的苦恼!谢谢!!!! - bemon
1
嗨...我也做了同样的事情,但我认为Angular路由正在处理该链接,然后在文件中,我将得到我的主页(如果未找到任何内容,则Angular将加载它)文件中的内容,而不是我从API发送的内容。有人能帮我解决这个问题吗? - Meysam
3
window.URL.createObjectURL 已被弃用。这是不正确的。 - tatsu
2
你应该将 this.blob 传递给 createObjectURL,而不是 data。 var downloadURL = window.URL.createObjectURL(this.blob); - Ε Г И І И О
3
请使用URL.createObjectURL而不是window.URL.createObjectURL,显然较新的浏览器认为window.URL.createObjectURL存在安全风险。 - Christopher Thomas
显示剩余5条评论

92

我用以下方法解决了这个问题(请注意,我合并了在stackoverflow上找到的多个解决方案,但我无法找到参考文献。欢迎在评论中添加)。

在我的服务中,我有:

public getPDF(): Observable<Blob> {   
//const options = { responseType: 'blob' }; there is no use of this
    let uri = '/my/uri';
    // this.http refers to HttpClient. Note here that you cannot use the generic get<Blob> as it does not compile: instead you "choose" the appropriate API in this way.
    return this.http.get(uri, { responseType: 'blob' });
}

在组件中,我有以下代码(这是从多个答案合并的部分):
public showPDF(fileName: string): void {
    this.myService.getPDF()
        .subscribe(x => {
            // It is necessary to create a new blob object with mime-type explicitly set
            // otherwise only Chrome works like it should
            var newBlob = new Blob([x], { type: "application/pdf" });
            
            // IE doesn't allow using a blob object directly as link href
            // instead it is necessary to use msSaveOrOpenBlob
            if (window.navigator && window.navigator.msSaveOrOpenBlob) {
                window.navigator.msSaveOrOpenBlob(newBlob, fileName);
                return;
            }
            
            // For other browsers: 
            // Create a link pointing to the ObjectURL containing the blob.
            const data = window.URL.createObjectURL(newBlob);
            
            var link = document.createElement('a');
            link.href = data;
            link.download = fileName;
            // this is necessary as link.click() does not work on the latest firefox
            link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
            
            setTimeout(function () {
                // For Firefox it is necessary to delay revoking the ObjectURL
                window.URL.revokeObjectURL(data);
                link.remove();
            }, 100);
        });
}

上面的代码在IE、Edge、Chrome和Firefox中都可以工作。然而,我并不是很喜欢它,因为我的组件被浏览器特定的东西所污染,这些东西肯定会随着时间的推移而改变。


作为一个经验法则,你应该。我找不到泄漏的证据。但是,我编辑了帖子以添加元素删除。谢谢你指出这一点。 - Yennefer
@Pier,你能否为这个功能打开一个Github仓库?我正在尝试使用Node.Js/Express和Angular实现相同的解决方案。我遇到了类似的问题,但无法解决。几个月前我放弃了这个开发项目。在看到你的解决方案后,我计划重新挑战它。你能帮助我吗? - Mr. Learner
1
window.URL.createObjectURL已经过时了一段时间,这个答案已经过时并且不再起作用。 - tatsu
2
这个答案仍然适用于Chrome:78.0.3904.97和Firefox:70.0.1。然而,我找不到一个可靠的方法来解决这个问题。你会如何修复它? - Yennefer
你如何处理来自服务器的错误?例如,HTTP 400。 - USQ91
显示剩余6条评论

9

对于Angular 12+,我想到了以下内容:

this.ApiService
    .getFileFromApi()
    .pipe(take(1))
    .subscribe((response) => {
        const downloadLink = document.createElement('a');
        downloadLink.href = URL.createObjectURL(new Blob([response.body], { type: response.body.type }));

        const contentDisposition = response.headers.get('content-disposition');
        const fileName = contentDisposition.split(';')[1].split('filename')[1].split('=')[1].trim();
        downloadLink.download = fileName;
        downloadLink.click();
    });

订阅使用Angular HttpClient中的简单get()方法。

// api-service.ts

getFileFromApi(url: string): Observable<HttpResponse<Blob>> {
  return this.httpClient.get<Blob>(this.baseApiUrl + url, { observe: 'response', responseType: 'blob' as 'json'});
}

3
你可以使用Angular指令完成此操作:
@Directive({
    selector: '[downloadInvoice]',
    exportAs: 'downloadInvoice',
})
export class DownloadInvoiceDirective implements OnDestroy {
    @Input() orderNumber: string;
    private destroy$: Subject<void> = new Subject<void>();
    _loading = false;

    constructor(private ref: ElementRef, private api: Api) {}

    @HostListener('click')
    onClick(): void {
        this._loading = true;
        this.api.downloadInvoice(this.orderNumber)
            .pipe(
                takeUntil(this.destroy$),
                map(response => new Blob([response], { type: 'application/pdf' })),
            )
            .subscribe((pdf: Blob) => {
                this.ref.nativeElement.href = window.URL.createObjectURL(pdf);
                this.ref.nativeElement.click();
            });
    }
    
    // your loading custom class
    @HostBinding('class.btn-loading') get loading() {
        return this._loading;
    }

    ngOnDestroy(): void {
        this.destroy$.next();
        this.destroy$.complete();
    }
}

在模板中:

<a
      downloadInvoice
      [orderNumber]="order.number"
      class="btn-show-invoice"
  >
     Show invoice
  </a>


这种方法更整洁,我很喜欢它。但是它有一个问题。当您在订阅中单击主机元素时,会再次触发onClick函数,这个循环会无限继续下去。您可能需要检查一下。 - adedayojs

1
你可以通过在后端设置Content-Type: 'application/octet-stream;来强制浏览器下载内容。
如果你无法应用这个配置,或者更喜欢在前端处理,你可以使用file-saver。FileSaver.js可以大大简化整个样板代码。
this.downloadService.getPdf().subscribe((resultBlob: Blob) => {
   saveAs(resultBlob, 'help.pdf', { autoBom: false });
});

0

我的回答基于 @Yennefer 的回答,但我想要使用服务器上的文件名,因为在前端我没有它。我使用了 Content-Disposition 头来传输这个信息,因为浏览器会将其用作直接下载。

首先,我需要从请求中获取头信息(注意 get 方法选项对象):

public getFile(): Observable<HttpResponse<Blob>> {   
    let uri = '/my/uri';
    return this.http.get(uri, { responseType: 'blob', observe: 'response' });
}

接下来,我需要从头文件中提取文件名。
public getFileName(res: HttpResponse<any>): string {
    const disposition = res.headers.get('Content-Disposition');
    if (!disposition) {
        // either the disposition was not sent, or is not accessible
        //  (see CORS Access-Control-Expose-Headers)
        return null;
    }
    const utf8FilenameRegex = /filename\*=UTF-8''([\w%\-\.]+)(?:; |$)/;
    const asciiFilenameRegex = /filename=(["'])(.*?[^\\])\1(?:; |$)/;

    let fileName: string = null;
    if (utf8FilenameRegex.test(disposition)) {
      fileName = decodeURIComponent(utf8FilenameRegex.exec(disposition)[1]);
    } else {
      const matches = asciiFilenameRegex.exec(disposition);
      if (matches != null && matches[2]) {
        fileName = matches[2];
      }
    }
    return fileName;
}

该方法检查ASCII和UTF-8编码的文件名,优先使用UTF-8。

一旦我有了文件名,就可以更新链接对象的下载属性(在@Yennifer的答案中,这是link.download = 'FileName.ext'window.navigator.msSaveOrOpenBlob(newBlob, 'FileName.ext');这两行代码)

关于此代码的一些注意事项:

  1. Content-Disposition不在默认的CORS白名单中,因此根据您的服务器配置,可能无法从响应对象中访问它。如果是这种情况,在响应服务器中,设置头Access-Control-Expose-Headers以包括Content-Disposition

  2. 某些浏览器将进一步清理文件名。我的Chrome版本似乎用下划线替换:"。我相信还有其他的,但这超出了范围。


0
//Step: 1
//Base Service
this.getPDF() {
 return this.http.get(environment.baseUrl + apiUrl, {
      responseType: 'blob',
      headers: new HttpHeaders({
        'Access-Control-Allow-Origin': '*',
        'Authorization': localStorage.getItem('AccessToken') || ''
      })
    });
}

//Step: 2
//downloadService
getReceipt() {
    return new Promise((resolve, reject) => {
      try {
        // {
        const apiName = 'js/getReceipt/type/10/id/2';
        this.getPDF(apiName).subscribe((data) => {
          if (data !== null && data !== undefined) {
            resolve(data);
          } else {
            reject();
          }
        }, (error) => {
          console.log('ERROR STATUS', error.status);
          reject(error);
        });
      } catch (error) {
        reject(error);
      }
    });
  }


//Step 3:
//Component 
getReceipt().subscribe((respect: any) => {
  var downloadURL = window.URL.createObjectURL(data);
  var link = document.createElement(‘a’);
  link.href = downloadURL;
  link.download = “sample.pdf";
  link.click();
});

-1

这在IE和Chrome中也适用,几乎相同的答案,只是对于其他浏览器,答案会稍微简短一些。

getPdf(url: string): void {
    this.invoiceService.getPdf(url).subscribe(response => {

      // It is necessary to create a new blob object with mime-type explicitly set
      // otherwise only Chrome works like it should
      const newBlob = new Blob([(response)], { type: 'application/pdf' });

      // IE doesn't allow using a blob object directly as link href
      // instead it is necessary to use msSaveOrOpenBlob
      if (window.navigator && window.navigator.msSaveOrOpenBlob) {
          window.navigator.msSaveOrOpenBlob(newBlob);
          return;
      }

      // For other browsers:
      // Create a link pointing to the ObjectURL containing the blob.
      const downloadURL = URL.createObjectURL(newBlob);
        window.open(downloadURL);
    });
  } 

2
window.URL.createObjectURL已被弃用。 - tatsu

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