简体   繁体   中英

Angular 5/6: Handle File Download (w/Friendly File Name) from ASP.NET Core API HTTP

I currently have an ASP.NET Core 2.1 API endpoint that users can hit to download a CSV file:

[HttpPost("Gimme")]
public IActionResult Gimme([FromBody] MyOptions options)
{
    MemoryStream reportStream = _reportGenerator.GenerateReportStream(options.StartDate, options.EndDate);

    return new FileStreamResult(reportStream, "text/csv") { FileDownloadName = "report.csv" };
}

I can test it via POSTMan and it returns a CSV with the correct data, no problem.

Enter Angular. The web site we're interacting with our API from uses an Angular-Driven Single Page Application. I've searched for various ways on how to handle files coming from API endpoints, and a lot seem to revolve around getting a Blob and creating an inline URL that is then navigated to via window.open() in the JavaScript. Or creating in-memory a tags to then call click() on.

My question: Does the latest Angular really have no way to handle this out of the box? I'm no Angular expert but I would've thought there'd be an example up on their site or some built-in mechanism for serving a downloaded file to the browser. There seems, however, to just be a lot of hackery involved.

My current, WIP solution does return the file to the browser for download, but regardless of the file name specified in the API, it is downloaded as a temporary file name (some GUID-looking thing) instead:

带有随机文件名的 CSV 下载

Below is what I have both in my Component and Service classes. I updated the downloadFile() method to use the answer supplied in this SO answer to get friendly file names working, but I would still rather find a less-hacky solution.

// Component

DownloadReport(options: ReportOptions) {
    this._service.getCSV(options).subscribe(data => this.downloadFile(data));
}

downloadFile(blob: Blob) {
    const fileName = 'report.csv';
    if (navigator.msSaveBlob) {
        // IE 10+
        navigator.msSaveBlob(blob, fileName);
    } else {
        const link = document.createElement('a');
        const url = URL.createObjectURL(blob);
        link.setAttribute('href', url);
        link.setAttribute('download', fileName);
        link.style.visibility = 'hidden';
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        URL.revokeObjectURL(url);
    }
}

// Service

getCSV(reportOptions: ReportOptions): Observable<any> {
    let headers = new HttpHeaders();
    headers = headers.set('Accept', 'text/csv');
    return this._httpClient
        .post(`${this.apiRoot}/Gimme`, reportOptions, { headers: headers, responseType: 'blob' })
        .catch(this.handleError);
}

As you can see, I'm currently implementing the createObjectURL hack to get this working (a mash of Found On Internet solutions). Is there a better way? A "Best Practices" sort of way?

This is what I use. It's basically the same thing you are using, but looks a little cleaner to me with the Anchor typing. I researched the right way to do this for a long time and couldn't find a non-hacky way to do it.

  download(): void {
    this.service.getFile({ id: id, name: name })
      .subscribe(data => {
        if (window.navigator && window.navigator.msSaveOrOpenBlob) {
          //save file for IE
          window.navigator.msSaveOrOpenBlob(data, name);
        } else {
          const objectUrl: string = URL.createObjectURL(data);
          const a: HTMLAnchorElement = document.createElement('a') as HTMLAnchorElement;

          a.href = objectUrl;
          a.download = name;
          document.body.appendChild(a);
          a.click();

          document.body.removeChild(a);
          URL.revokeObjectURL(objectUrl);
        }
      });
  }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM