简体   繁体   English

使用从 nodeJs 到 angular 应用程序的流下载大文件

[英]download large file using streams from nodeJs to angular app

i am struggling to get this stream downloading file works if i try the same request using curl the stream works fine and streaming the data.如果我使用 curl 尝试相同的请求,我正在努力让这个 stream 下载文件工作,stream 工作正常并流式传输数据。 in my angular app the file is completely download before the client see the download file tab it seems like the subscribe only happened after all stream body fully downloaded在我的 angular 应用程序中,文件已在客户端完全下载之前看到下载文件选项卡似乎订阅仅在所有 stream 正文完全下载后才发生

what i am trying to achive is after first chunk of data send to angular app i want the file to start downloading.我想要实现的是在将第一块数据发送到 angular 应用程序之后,我希望文件开始下载。

instead after observing the network only after all file downloaded from the backend the downLoadFile methood is called and the ui expirence is stuck相反,只有在从后端下载所有文件后才观察网络,然后调用 downLoadFile 方法并且 ui 过期被卡住

this is a minimal example of what i am trying todo at the backend i have a genrator that genreate a huge file and pipe the request这是我在后端尝试做的一个最小示例我有一个生成器来生成一个巨大的文件和 pipe 请求

Node JS节点JS

const FILE = './HUGE_FILE.csv'

const lines = await fs.readFileSync(FILE, 'utf8').split('\n')

function* generator() {
    for (const i of files) {
        console.log(i)
        yield i
    }
}
app.get('/', async function (req, res) {
    res.setHeader('Content-Type', 'text/csv');
    res.setHeader('Content-Disposition', 'attachment; filename=\"' + 'download-' + Date.now() + '.csv\"');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Pragma', 'no-cache');
    const readable = Readable.from(generator());
    readable.pipe(res);
});

at the client side calling the endpoint and waiting for the resonse在客户端调用端点并等待响应

Angular code Angular代码

@Component({
    selector: 'is-optimizer-csv',
    templateUrl: './optimizer-csv.component.html',
    styleUrls: ['./optimizer-csv.component.scss']
})
export class OptimizerCsvComponent implements OnInit {

    private onDownloadButtonClicked() {
        const data = {...this.form.get('download').value, ...(this.advertiserId ? {advertiserId: this.advertiserId} : null)};
        this.loading$.next(true);
        this.optimizerService
            .downloadOptimizerCsvData(data)
            .pipe(
                take(1),
                tap(_ => this.loading$.next(false))
            )
            .subscribe();
    }

  
}





@Injectable({
    providedIn: 'root'
})
export class OptimizerService {
    constructor(private readonly http: Ht, private datePipe: DatePipe) {}

    downloadOptimizerCsvData(data: any) {
        this.http.get(`${environment.apiUrl}`,{
            responseType: 'arraybuffer',headers:headers} 
           ).subscribe(response => this.downLoadFile(response, "text/csv"));
  
      }
      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.');
        }
    }
}

it look like angular common http client does not support streaming out of the box.看起来 angular 常见的 http 客户端不支持开箱即用的流式传输。

see this github issue by @prabh-62请参阅@prabh-62 的此 github 问题

https://github.com/angular/angular/issues/44143 https://github.com/angular/angular/issues/44143

from this thread the why to solve your issue is by implementing the fetch stream logic using native fetch从这个线程解决您的问题的原因是通过使用本机fetch实现提取 stream 逻辑

The reason why your file gets immediately downloaded is because you are subscribing upon the construtor in which your service is injected gets initialized.Meaning your component is just telling the service to fire the download method with the HttpClient call in it, while it seems you are having a (click)="onDownloadButtonClicked()" event in your template.你的文件被立即下载的原因是因为你订阅了你的服务注入的构造函数被初始化。意味着你的组件只是告诉服务使用HttpClient调用来触发下载方法,而看起来你是在您的模板中有(click)="onDownloadButtonClicked()"事件。

The solution is to delegate the firing of the action until the onClick event from the UI is fired.解决方案是委托触发动作,直到触发来自 UI 的onClick事件。 To do so这样做

1- Remove the subscription from your service method. 1- 从您的服务方法中删除subscription

 downloadOptimizerCsvData(data: any) {
        this.http.get(`${environment.apiUrl}`,{
            responseType: 'arraybuffer',headers:headers} 
           )
  
      }

2- Return an Observable to the consumer component 2- 将Observable返回给消费者组件

 downloadOptimizerCsvData(data: any) {
       return this.http.get(`${environment.apiUrl}`,{
            responseType: 'arraybuffer',headers:headers} 
           )
  
      }

3- Subscribe inside the onDownloadButtonClicked() by refactoring the method to somthing like 3-通过将方法重构为类似的方法在onDownloadButtonClicked()内订阅

 private onDownloadButtonClicked() {
        const data = {...this.form.get('download').value, ...(this.advertiserId ? {advertiserId: this.advertiserId} : null)};
        this.loading$.next(true);
        this.optimizerService
            .downloadOptimizerCsvData(data)
            .pipe(
                take(1),
                tap(_ => this.loading$.next(false))
            )
            .subscribe(response => this.optimizerService.downLoadFile(response, "text/csv"));
    }

If you want to download the file as soon as the view becomes available then you need to call the method programmatically inside the ngOnInit(){} lifecyclehook without a user based triggered event.如果您想在视图可用时立即下载文件,那么您需要在ngOnInit(){}生命周期挂钩内以编程方式调用该方法,而无需基于用户的触发事件。

Read here on this lifecyclehook and how you can use it在此阅读此生命周期钩子以及如何使用它

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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