[英]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() 函数,它应该按顺序执行以下操作:
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.我希望您可以使用此处描述的内容来重新调整您的解决方案以包含文件或您认为相关的任何其他信息。
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.