我试图创建一个满足以下要求的可观察流程:
我知道如何执行步骤(3)有很多不同的答案,但是当我试图一起执行这些步骤时,我正在寻找关于解决方案I'我想出的是最简洁的(我怀疑!)。
以下是展示我当前方法的示例:
var cachedRequestToken;
var cachedRequest;
function getOrUpdateValue() {
return loadFromStorage()
.flatMap(data => {
// data doesn't exist, shortcut out
if (!data || !data.refreshtoken)
return Rx.Observable.empty();
// data still valid, return the existing value
if (data.expires > new Date().getTime())
return Rx.Observable.return(data.value);
// if the refresh token is different or the previous request is
// complete, start a new request, otherwise return the cached request
if (!cachedRequest || cachedRequestToken !== data.refreshtoken) {
cachedRequestToken = data.refreshtoken;
var pretendHttpBody = {
value: Math.random(),
refreshToken: Math.random(),
expires: new Date().getTime() + (10 * 60 * 1000) // set by server, expires in ten minutes
};
cachedRequest = Rx.Observable.create(ob => {
// this would really be a http request that exchanges
// the one use refreshtoken for new data, then saves it
// to storage for later use before passing on the value
window.setTimeout(() => { // emulate slow response
saveToStorage(pretendHttpBody);
ob.next(pretendHttpBody.value);
ob.completed();
cachedRequest = null; // clear the request now we're complete
}, 2500);
});
}
return cachedRequest;
});
}
function loadFromStorage() {
return Rx.Observable.create(ob => {
var storedData = { // loading from storage goes here
value: 15, // wrapped in observable to delay loading until subscribed
refreshtoken: 63, // other process may have updated this between requests
expires: new Date().getTime() - (60 * 1000) // pretend to have already expired
};
ob.next(storedData);
ob.completed();
})
}
function saveToStorage(data) {
// save goes here
}
// first request
getOrUpdateValue().subscribe(function(v) { console.log('sub1: ' + v); });
// second request, can occur before or after first request finishes
window.setTimeout(
() => getOrUpdateValue().subscribe(function(v) { console.log('sub2: ' + v); }),
1500);
答案 0 :(得分:5)
首先,看看工作jsbin example。
解决方案与您的初始代码有点不同,我想解释原因。保持返回本地存储,保存,保存标志(缓存和令牌)的需要并不适合我的反应性,功能性方法。我给出的解决方案的核心是:
var data$ = new Rx.BehaviorSubject(storageMock);
var request$ = new Rx.Subject();
request$.flatMapFirst(loadFromServer).share().startWith(storageMock).subscribe(data$);
data$.subscribe(saveToStorage);
function getOrUpdateValue() {
return data$.take(1)
.filter(data => (data && data.refreshtoken))
.switchMap(data => (data.expires > new Date().getTime()
? data$.take(1)
: (console.log('expired ...'), request$.onNext(true) ,data$.skip(1).take(1))));
}
关键是data$
保存您的最新数据并且始终是最新的,通过执行data$.take(1)
可以轻松访问它。 take(1)
对于确保您的订阅获得单个值并终止(因为您尝试以程序性而非功能性方式工作)非常重要。如果没有take(1)
您的订阅将保持活动状态,并且您将拥有多个处理程序,那么您也可以在仅针对当前更新的代码中处理未来的更新。
此外,我持有一个request$
主题,这是您开始从服务器获取新数据的方法。该功能如下:
return Rx.Observable.empty()
。data$.take(1)
,这是您可以订阅的单个元素序列。request$.onNext(true)
并返回data$.skip(1).take(1)
。 skip(1)
是为了避免当前的过时值。为简洁起见,我使用了(console.log('expired ...'), request$.onNext(true) ,data$.skip(1).take(1)))
。这可能看起来有点神秘。它使用js逗号分隔语法,这在minifiers / uglifiers中很常见。它执行所有语句并返回最后一个语句的结果。如果你想要一个更易读的代码,你可以像这样重写它:
.switchMap(data => {
if(data.expires > new Date().getTime()){
return data$.take(1);
} else {
console.log('expired ...');
request$.onNext(true);
return data$.skip(1).take(1);
}
});
最后一部分是flatMapFirst
的用法。这可确保在请求正在进行时,将删除所有后续请求。您可以在控制台打印输出中看到它的工作原理。来自服务器的负载'多次打印,但实际的序列只被调用一次,你从服务器完成了一次加载'打印。对于原始的刷新标记检查,这是一个更具反应性的解决方案。
虽然我不需要保存的数据,但它会被保存,因为您提到您可能希望在将来的会话中阅读它。
关于rxjs的一些提示:
您可以简单地执行setTimeout
,而不是使用导致许多问题的Rx.Observable.timer(time_out_value).subscribe(...)
。
创建一个observable很麻烦(你甚至不得不打电话给next(...)
和complete()
)。使用Rx.Subject
可以更清晰地执行此操作。请注意,您有此类的规范,BehaviorSubject
和ReplaySubject
。这些课程值得了解并且可以提供很多帮助。
最后一点。这是一个非常挑战的问题:-)我不熟悉您的服务器端代码和设计注意事项,但抑制呼叫的需要让我觉得不舒服。除非有与你的后端相关的非常好的理由,否则我的自然方法是使用flatMap并让最后一个请求“赢”,即删除之前未终止的呼叫并设置值。
代码是基于rxjs 4(所以它可以在jsbin中运行),如果你使用angular2(因此rxjs 5),你需要调整它。看看the migration guide。
================回答史蒂夫的其他问题(在下面的评论中)=======
我可以推荐one article。它的标题说明了一切: - )
至于程序与功能方法,我将另外一个变量添加到服务中:
let token$ = data$.pluck('refreshtoken');
然后在需要时使用它。
我的一般方法是首先映射我的数据流和关系,然后像一个好的键盘管道工" (就像我们所有人一样),建造管道。我的服务的顶级草案将是(跳过angular2手续和提供者简洁):
class UserService {
data$: <as above>;
token$: data$.pluck('refreshtoken');
private request$: <as above>;
refresh(){
request.onNext(true);
}
}
您可能需要进行一些检查,以便pluck
不会失败。
然后,需要数据或令牌的每个组件都可以直接访问它。
现在假设您有一项服务需要对数据或令牌进行更改:
class SomeService {
constructor(private userSvc: UserService){
this.userSvc.token$.subscribe(() => this.doMyUpdates());
}
}
如果您需要合成数据,意思是使用数据/令牌和一些本地数据:
Rx.Observable.combineLatest(this.userSvc.data$, this.myRelevantData$)
.subscribe(([data, myData] => this.doMyUpdates(data.someField, myData.someField));
同样,理念是你构建数据流和管道,连接它们,然后你所要做的就是触发东西。
迷你模式&#39;我想出的是,一旦我的触发序列传递给服务并注册到结果。让我们以自动完成为例:
class ACService {
fetch(text: string): Observable<Array<string>> {
return http.get(text).map(response => response.json().data;
}
}
然后,每次文本更改时都必须调用它并将结果分配给组件:
<div class="suggestions" *ngFor="let suggestion; of suggestions | async;">
<div>{{suggestion}}</div>
</div>
并在您的组件中:
onTextChange(text) {
this.suggestions = acSVC.fetch(text);
}
但也可以这样做:
class ACService {
createFetcher(textStream: Observable<string>): Observable<Array<string>> {
return textStream.flatMap(text => http.get(text))
.map(response => response.json().data;
}
}
然后在你的组件中:
textStream: Subject<string> = new Subject<string>();
suggestions: Observable<string>;
constructor(private acSVC: ACService){
this.suggestions = acSVC.createFetcher(textStream);
}
onTextChange(text) {
this.textStream.next(text);
}
模板代码保持不变。
这似乎是一件小事,但是一旦应用程序变得越来越大,数据流越来越复杂,这样做效果会更好。您有一个包含数据的序列,您可以在任何需要它的地方使用它,您甚至可以进一步转换它。例如,假设你需要知道建议的数量,在第一种方法中,一旦得到结果,你需要进一步查询它以获得它,因此:
onTextChange(text) {
this.suggestions = acSVC.fetch(text);
this.suggestionsCount = suggestions.pluck('length'); // in a sequence
// or
this.suggestions.subscribe(suggestions => this.suggestionsCount = suggestions.length); // in a numeric variable.
}
现在在第二种方法中,您只需定义:
constructor(private acSVC: ACService){
this.suggestions = acSVC.createFetcher(textStream);
this.suggestionsCount = this.suggestions.pluck('length');
}
希望这会有所帮助: - )
在写作的过程中,我试图反思我采用这种反应方式的路径。毋庸置疑,在进行实验时,无数的jsbins和奇怪的失败都是其中的重要组成部分。我认为另一件事有助于塑造我的方法(虽然我目前没有使用它)是学习redux和读取/尝试一些ngrx(angular&#39s的redux端口)。哲学和方法不会让你甚至想到程序性,所以你必须调整功能,数据,关系和流程的思维方式。