问题
I'm trying to chain / pipe operations and return an Observable from a Service in angular which uses angular fire.
With promises I have this working
Service
saveDiploma(diploma: { title: any; description: any; picture: any }) {
return new Observable(observer => {
const id = this.db.createId();
this.storage.ref(`diplomas/${id}/original.jpg`)
.putString(diploma.picture, 'data_url')
.then(task => {
task.ref.getDownloadURL()
.then(url => {
const saved = {
title: diploma.title,
description: diploma.description,
url,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
createdBy: this.auth.auth.currentUser ? this.auth.auth.currentUser.uid : 'anonymous'
};
this.db.doc(`diplomas/${id}`)
.set(saved)
.then(() => {
observer.next(saved);
observer.complete();
})
.catch(e => observer.error(e));
})
.catch(e => observer.error(e));
})
.catch(e => observer.error(e));
});
}
Component
save() {
this.diplomasService.saveDiploma({
title: this.diplomaForm.value.title,
description: this.diplomaForm.value.description,
picture: this.currentImage
}).subscribe(diploma => {
console.log('saved diploma', diploma);
}, e => console.error('error while saving the diploma', e));
}
I'm trying to use Observables in the service instead of Promises and pipe them in order like so
saveDiploma(diploma: { title: any; description: any; picture: any }) {
const id = this.db.createId();
const ref = this.storage.ref(`diplomas/${id}/original.jpg`);
return ref.putString(diploma.picture, 'data_url').snapshotChanges().pipe(
concatMap(task => {
console.log('getDownloadURL');
return from(task.ref.getDownloadURL());
}),
concatMap(url => {
console.log('url', url);
const saved = {
title: diploma.title,
description: diploma.description,
url,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
createdBy: this.auth.auth.currentUser ? this.auth.auth.currentUser.uid : 'anonymous'
};
return from(this.db.doc(`diplomas/${id}`).set(saved));
})
);
}
but the getDownloadURL method is getting fired before the upload is complete and hence returning an error storage/object-not-found
. I've tried adding a finalize or filter (on task.state == 'success') before the concatMap(getDownloadURL) but I have failed getting it to work.
Does anyone know how to pipe this operations and return an Observable from them?
I'm using Angular 8.1.2, Angular Fire 5.2.1 and rxjs 6.5.1
回答1:
According to the AngularFire documentation ref.putString(..).snapshotChanges()
Emits the raw UploadTaskSnapshot as the file upload progresses.
So your problem is that .snapshotChanges()
emits before the file upload is complete. concatMap
gets triggered on every emit from the source not just on complete. You should use concat
.
saveDiploma(diploma: { title: any; description: any; picture: any }) {
const id = this.db.createId();
const ref = this.storage.ref(`diplomas/${id}/original.jpg`);
return concat(
ref.putString(diploma.picture, 'data_url').snapshotChanges().pipe(ignoreElements()),
defer(() => ref.getDownloadURL().pipe(
switchMap(url => {
console.log('url', url);
const saved = {
title: diploma.title,
description: diploma.description,
url,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
createdBy: this.auth.auth.currentUser ? this.auth.auth.currentUser.uid : 'anonymous'
};
return this.db.doc(`diplomas/${id}`).set(saved); // you can return a Promise directly
})
))
);
}
Possible alternative:
saveDiploma(diploma: { title: any; description: any; picture: any }) {
const id = this.db.createId();
const ref = this.storage.ref(`diplomas/${id}/original.jpg`);
return ref.putString(diploma.picture, 'data_url').snapshotChanges().pipe(
last(),
switchMap(() => ref.getDownloadURL()),
map(url => ({
title: diploma.title,
description: diploma.description,
url,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
createdBy: this.auth.auth.currentUser ? this.auth.auth.currentUser.uid : 'anonymous'
})),
switchMap(saved => this.db.doc(`diplomas/${id}`).set(saved))
);
}
回答2:
The problem here is that promises are by default eager. I think wrapping the from
operator with the defer
operator (https://rxjs.dev/api/index/function/defer) should solve your problem. So the code would look something like this:
return ref.putString(diploma.picture, 'data_url').snapshotChanges().pipe(
concatMap(task => defer(() => {
console.log('getDownloadURL');
return from(task.ref.getDownloadURL());
})),
concatMap(url => defer(() => {
console.log('url', url);
const saved = {
title: diploma.title,
description: diploma.description,
url,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
createdBy: this.auth.auth.currentUser ? this.auth.auth.currentUser.uid : 'anonymous'
};
return from(this.db.doc(`diplomas/${id}`).set(saved));
}))
The method passed to defer is evaluated as soon as it is subscribed to. ConcatMap will automatically subscribe to the inner observable as soon as there is an incoming notification from the source observable.
来源:https://stackoverflow.com/questions/57660293/how-to-return-an-observable-from-service-in-angular-after-chaining-pipe-operatio