简体   繁体   English

如何将 observables 组合/合并为单个 observable 并将结果与​​输入关联?

[英]How to combine/merge observables into a single observable and associate result with the input?

I have a saveForm() function which is expected to perform below operations in order:我有一个 saveForm() 函数,它应该按顺序执行以下操作:

  1. Take form data and add it to FireStore collection as a document.获取表单数据并将其作为文档添加到 FireStore 集合中。
  2. On success, loop through(attachmentsFormArray) all the files the user has selected and upload each file to FireStorage.成功后,循环遍历 (attachmentsFormArray) 用户选择的所有文件并将每个文件上传到 FireStorage。
  3. When all files are uploaded completely, assign the documentUrl of each file to the corresponding file map on the FireStore document we saved in step #1.当所有文件都上传完毕后,将每个文件的 documentUrl 分配给我们在步骤 1 中保存的 FireStore 文档上的相应文件映射。 Then make api call to actually save the updated firestore document.然后进行 api 调用以实际保存更新的 Firestore 文档。

Below is my saveForm() function:下面是我的 saveForm() 函数:

saveForm() {
    let fixedDepositEntity = this.getEntityFromForm();
    this.fixedDepositsFirestoreCollection.add(fixedDepositEntity).then(documentRef => {
        if (this.attachmentsFormArray.controls.length !== 0) {
            this.attachmentsFormArray.controls.forEach(group => {

                let fileRef = this.fireStorage.ref(this.fixedDepositsStorageFolderPath + group.get('fileName').value);
                let uploadTask = fileRef.put(group.get('file').value);

                // observe percentage changes
                uploadTask.percentageChanges().subscribe(percent => {
                    group.get('percentComplete').setValue(Math.round(percent));
                });
                // get notified when the download URL is available
                uploadTask.snapshotChanges().pipe(
                    finalize(() => {
                        fileRef.getDownloadURL().subscribe(url => {
                            group.get('downloadUrl').setValue(url);
                        });
                    }))
                    .subscribe();
            });
        }
    });
}

Currently, the above code simply loops through the attachmentsFormArray and once the file is uploaded, finally it assigns the downloadUrl to the attachmentsFormArray.目前,上面的代码只是简单地循环了attachmentsFormArray,一旦文件上传,最后它会将downloadUrl 分配给attachmentsFormArray。

When the user selects the multiple file, I have the below handleFileInput() event handler:当用户选择多个文件时,我有以下 handleFileInput() 事件处理程序:

handleFileInput(files: FileList) {
    if (!files || files.length === 0) {
        return;
    }
    Array.from(files).forEach(file => {
        this.attachmentsFormArray.push(this.formBuilder.group({
            fileName: [file.name],
            fileSize: [file.size],
            label: [''],
            file: [file],
            downloadUrl: [''],
            percentComplete: [''],
            uploadTaskState: ['']

        }));
    });

The AngularFire library provides a snapshotChanges() method which returns Observable<UploadTaskSnapshot>. AngularFire 库提供了一个 snapshotChanges() 方法,它返回 Observable<UploadTaskSnapshot>。 I would want to combine/merge all these Observables (so that know once all files are completely uploaded) and then subscribe the resultant Observable.我想组合/合并所有这些 Observable(以便在所有文件完全上传后知道),然后订阅生成的 Observable。 But I am not sure how to associate the individual observable result with corresponding file object that the user selected (as described in #3).但我不确定如何将单个可观察结果与用户选择的相应文件对象相关联(如 #3 中所述)。

I know we can achieve this behavior with RxJs operators, but not sure which one to use in my scenario.我知道我们可以使用 RxJs 操作符来实现这种行为,但不确定在我的场景中使用哪一个。 Any help is appreciated in advance.任何帮助都提前表示赞赏。


EDIT 1: Implemented as per "Mrk Sef's" answer.编辑 1:按照“Mrk Sef 的”答案实施。 It works fine most of the times.大多数时候它工作正常。 However, once in a while the downloadUrl is not set.但是,有时不设置 downloadUrl。 I'm unable to understand the reason for this intermittent issue.我无法理解这个间歇性问题的原因。

saveForm() {
    try {
        this.fixedDepositsFormGroup.disable();
        let fixedDepositEntity = this.getEntityFromForm();
        this.fixedDepositsFirestoreCollection
            .add(fixedDepositEntity)
            .then(documentRef => {
                this.isBusy = true;
                // Changes will be mapped to an array of Observable, once this mapping
                // is complete, we can subscribe and wait for them to finish
                console.log('Voila! Form Submitted.');
                if (this.attachmentsFormArray.controls.length !== 0) {
                    const changes = this.attachmentsFormArray.controls.map(
                        group => {
                            const fileRef = this.fireStorage.ref(this.fixedDepositsStorageFolderPath + group.get('fileName').value);
                            const uploadTask = fileRef.put(group.get('file').value);

                            const percentageChanges$ = uploadTask.percentageChanges().pipe(
                                tap(percent => group.get('percentComplete').setValue(Math.round(percent)))
                            );
                            const snapshotChanges$ = uploadTask.snapshotChanges().pipe(
                                finalize(() => fileRef.getDownloadURL().subscribe(url => group.get('downloadUrl').setValue(url)))
                            );
                            return [percentageChanges$, snapshotChanges$];
                        }
                    ).reduce((acc, val) => acc.concat(val), []);; // Turn our array of tuples into an array

                    // forkJoin doesn't emit until all source Observables complete
                    forkJoin(changes).subscribe(_ => {
                        // By now all files have been uploaded to FireStorage
                        // Now we update the attachments property in our fixed-deposit document
                        const attachmentValues = (this.getControlValue('attachments') as any[])
                            .map(item => <Attachment>{
                                fileName: item.fileName,
                                fileSize: item.fileSize,
                                label: item.label,
                                downloadUrl: item.downloadUrl
                            });
                        documentRef.update({ attachments: attachmentValues });
                        console.log("Files Uploaded Successfully and Document Updated !");
                    });
                }
            })
            .finally(() => {
                this.fixedDepositsFormGroup.enable();
                this.isBusy = false;
            });
    } finally {

    }
}

在此处输入图片说明

A common design you see when observable are generated by a third party is to tag it with some custom information that you know at call-time but may not know when you're subscribed.当 observable 由第三方生成时,您看到的一个常见设计是使用一些您在调用时知道但在订阅时可能不知道的自定义信息来标记它。

For example, get the third word of each document whose title starts with 'M':例如,获取标题以“M”开头的每个文档的第三个单词:

const documents: Document[] = getDocumentsService();

wordStreams: Observable<[Document, HttpResponse]>[] = documents
  .filter(file => file.title.charAt(0) === 'M')
  .map(file => getThirdWordService(file.id).pipe(
    map(serviceResponse => ([file, serviceResponse]))
  );

merge(...wordStreams).subscribe(([file, serviceResponse]) => {
  console.log(`The third word of ${file.title} is ${serviceResponse.value}`)
});

The big takeaway is that by mapping a value into a tuple or an object (The same pattern works objects, maps, ect) you can carry that information forward through the operations in a stream.最大的收获是,通过将一个值映射到一个元组或一个对象(相同的模式适用于对象、映射等),您可以通过流中的操作将该信息传递出去。

The only problem is that if you're not careful, you may end up with a stream that isn't purely functional (can cause side effects to your program state).唯一的问题是,如果您不小心,您最终可能会得到一个不是纯函数式的流(可能会对您的程序状态造成副作用)。


I'm not really sure what your example is doing, but here's my best guess at what you want:我不太确定你的例子在做什么,但这是我对你想要的最好的猜测:

saveForm() {
  let fixedDepositEntity = this.getEntityFromForm();
  this.fixedDepositsFirestoreCollection
    .add(fixedDepositEntity)
    .then(documentRef => {
      // Changes will be mapped to an array of Observable, once this mapping
      // is complete, we can subscribe and wait for them to finish
      const changes = this.attachmentsFormArray.controls.map(
        group => {
          const fileRef = this.fireStorage.ref(this.fixedDepositsStorageFolderPath + group.get('fileName').value);
          const uploadTask = fileRef.put(group.get('file').value);

          const percentageChanges$ = uploadTask.percentageChanges().pipe(
              tap(percent => group.get('percentComplete').setValue(Math.round(percent)))
          );
          const snapshotChanges$ = uploadTask.snapshotChanges().pipe(
            mergeMap(_ => fileRef.getDownloadURL()),
            tap(url => group.get('downloadUrl').setValue(url))
          );
          return [percentageChanges$, snapshotChanges$];
        }
      ).flat(); // Turn our array of tuples into an array

      // forkJoin doesn't emit until all source Observables complete
      forkJoin(changes).subscribe(_ => 
        console.log("All changes are complete")
      );
    });
}

If instead, you want to delay writing values out until your subscribe, here's another option that more clearly tags the observable stream with some added data that gets used later:相反,如果您想延迟写入值直到您订阅,这里有另一个选项,可以更清楚地使用一些稍后使用的添加数据标记可观察流:

saveForm() {
  let fixedDepositEntity = this.getEntityFromForm();
  this.fixedDepositsFirestoreCollection
    .add(fixedDepositEntity)
    .then(documentRef => {
      // Changes will be mapped to an array of Observable, once this mapping
      // is complete, we can subscribe and wait for them to finish
      const changes = this.attachmentsFormArray.controls.map(
        group => {
          const fileRef = this.fireStorage.ref(this.fixedDepositsStorageFolderPath + group.get('fileName').value);
          const uploadTask = fileRef.put(group.get('file').value);

          const percentageChanges$ = uploadTask.percentageChanges().pipe(
              map(percent => ([group, percent]))
          );
          const snapshotChanges$ = uploadTask.snapshotChanges().pipe(
            mergeMap(_ => fileRef.getDownloadURL()),
            map(url => ([group, url]))
          );
          return [percentageChanges$, snapshotChanges$];
        }
      );

      const percentageChanges$ = changes.map(([a, b]) => a);
      const snapshotChanges$ = changes.map(([a, b]) => b);

      merge(...percentageChanges$).subscribe({
        next: ([group, percent]) => group.get('percentComplete').setValue(Math.round(percent)),
        complete: _ => console.log("All percentageChanges complete")
      });

      merge(...snapshotChanges$).subscribe({
        next: ([group, url]) => group.get('downloadUrl').setValue(url),
        complete: _ => console.log("All snapshotChanges complete")
      });
    });
}

It goes without saying that none of this is tested.不用说,这些都没有经过测试。 It's my hope you can use what's described here to retool your solution to include files or whatever other information you find pertinent.我希望您可以使用此处描述的内容来重新调整您的解决方案以包含文件或您认为相关的任何其他信息。


update更新

My solution created a stream called我的解决方案创建了一个名为的流

const snapshotChanges$ = uploadTask.snapshotChanges().pipe(
  mergeMap(_ => fileRef.getDownloadURL()),
  tap(url => group.get('downloadUrl').setValue(url))
);

Which wasn't really what you were after, you wanted a stream that only starts after uploadTask.snapshotChanges() completes.这并不是您真正想要的,您想要一个仅在 uploadTask.snapshotChanges() 完成后启动的流。 Finalize is strange in that it operates on failure as well as completion, I'm sure there's an operator that can be configured to do that, but I don't know how. Finalize 很奇怪,因为它在失败和完成时运行,我确定有一个操作符可以配置为这样做,但我不知道如何。

My solution creates a custom operator ( waitForEnd ) that emits a boolean value when the source completes or errors and ignores all other elements from the source stream我的解决方案创建了一个自定义运算符 ( waitForEnd ),它在源完成或出错时发出一个布尔值并忽略源流中的所有其他元素

const waitForEnd = () => 
  waitOn$ => new Observable(obsv => {
    const final = (bool) => {
      obsv.next(bool);
      obsv.complete();
    }
    waitOn$.subscribe({
      next: _ => {/*do nothing*/},
      complete: () => final(true),
      error: _ => final(false)
    });
    return {unsubscribe: () => waitOn$.unsubscribe()}
  });

let snapshotChanges$ = uploadTask.snapshotChanges().pipe(
  waitForEnd(),
  mergeMap(_ => fileRef.getDownloadURL()),
  tap(url => group.get('downloadUrl').setValue(url))
);

snapshotChanges$ will wait for uploadTask.snapshotChanges() to have ended, only then will it get the download URL and set the value before it completes as well. snapshotChanges$将等待uploadTask.snapshotChanges()结束,然后它才会在完成之前获取下载 URL 并设置值。

暂无
暂无

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

相关问题 如何将可观察变量数组合并为具有相同返回类型的单个可观察变量 - How to merge an array of Observables into a single Observable of the same return type 如何将多个 Observable 的发射值合并为一个 Observable 发射? - How to merge the emitted values of multiple Observables into a single Observable emission? Rxjs 将多个可观察对象组合成单个 boolean 可观察对象 - Rxjs Combine multiple observables to a single boolean observable 将两个可观察数组合并为一个可观察数组 - merge two observables array into single observable array 如何组合两个 observable 来创建新的 observable? - How to combine two observables to create new observable? 如何组合在Angular 7和RsJS中另一个Observable结果的循环内获取的Observables数据中的数据? - How to combine data from Observables data fetched inside a loop made on another Observable result in Angular 7 and RsJS? 将 observable 与多个 observable 结合 - Combine observable with multiple observables 如何将第一个可观察的结果转化为下一个可观察的结果? - how to use result of first observable into next observables? 合并两个 Observable 并作为 Angular 材料表的单个 Observable 传入 dataSource - Merge two Observables and pass into dataSource as a single Observable for Angular material table 如何将多个 rxjs observables 与单个订阅结合起来? - How to combine multiple rxjs observables with a single subscription?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM