问题
Hi I'm pretty new to Observables and I'm looking for a way of loading my navigation tree with recursive observable calls.
The Navigation should be build up dynamically base on all the index.json
files in the directory and sub directories.
Only the url of the first call is static:
/public/index.json
This is the directory structure. Each directory may contain a index.json
,
providing information about its content and may references to other index files via the loadChildrenFromUrl
property.
|-public
|- subdir1
|- index.json
|- test.html
|- subdir2
|- index.json
|- test.html
|- subdir2.1
|- index.json
|- . . .
|- index.json
Navigation file index.json
[
// static entry with static children
{
"state": "module1",
"name": "Modul 1",
"type": "sub",
"icon": "dashboard",
"children": [
{"state": "", "name": "Index", "icon": "https" },
{"state": "test1", "name": "Test1", "icon": "live_help"}
]
},
{
// dynamic entry children needs to be load from url
"state": "test",
"name": "Test loaded from url",
"type": "sub",
"icon": "info_outline",
"loadChildrenFromUrl": "subdir2/index.json"
"children": [] // should be loaded via url
},
. . .
]
The result should be one large object describing the whole navigation tree.
So Children may contain children may contain children... .
A Router-Guard (CanActivate returning Observable
) will take care to wait until loading the tree has finished.
My code is working but the function returns before the whole tree is loaded. I know the whole thing is async so this is by design but I've no idea how to solve it right. Looks like I've to use flatMap?
NavigationService.ts
loadNavigation(): Observable<Menu[]> {
if (this.navigationLoaded) {
return Observable.of(this.navigationTree);
} else {
this.navigationTree = new Array();
return this.loadNavigationByUrl('public', this.navigationTree);
}
}
loadNavigationByUrl(url: string, navArray: Menu[]): Observable<Menu[]> {
console.log(`Loading ${url}/index.json`);
const result = this.http.get<Menu[]>(`${url}/index.json`, { responseType: 'json' });
result.catch((err) => this.handleError(err));
result.subscribe(data => {
// console.log(data);
if (data) {
data.forEach((item: Menu, index: number, array: Menu[]) => {
// add to navigationTree
navArray.push(item);
if (item.loadChildrenFromUrl && item.loadChildrenFromUrl !== '') {
item.children = new Array();
this.loadNavigationByUrl(`${url}/${item.loadChildrenFromUrl}`, item.children);
}
// console.log(this.navigationTree);
});
// this.navigationTree = data;
console.log('navigation loaded');
this.navigationLoaded = true;
}
},
err => {
},
() => {
console.log(`Loading ${url}/index.json completed`);
}
);
return result;
}
So how to construct an observable "chain?" to do this?
new info 2017-12-01
At the end I need to use this function in a Route Guard
so navigation structure gets loaded before route gets active.
NavigationGuard.ts
@Injectable()
export class NavigationGuard implements CanActivate, CanActivateChild {
constructor(private svc: NavigationService, private router: Router) { }
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
// console.log('canActivate');
return this.svc.loadNavigation()
.mapTo(true) // I'm not interested in the result
.catch((error: any) => {
console.log(error);
return Observable.of(false);
});
}
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return this.canActivate(route, state);
}
}
回答1:
Leaving aside the"WHY??" because I'm interested in a recursive observable structure... You can't recurse through an observable with nested subscriptions. You need to use higher order observables, and you should really never subscribe at all. the key to this is that the caller needs to subscribe, otherwise it will never work.
loadNavigation(): Observable<Menu[]> {
if (this.navigationLoaded) {
return Observable.of(this.navigationTree);
} else {
let navigationTree = new Array();
return this.loadNavigationByUrl('public', this.navigationTree)
.do(data => {
// console.log(data);
if (data) {
this.navigationTree = data;
console.log('navigation loaded');
this.navigationLoaded = true;
}
}); // could subscribe here instead if you really want.
}
}
loadNavigationByUrl(url: string, navArray: Menu[]): Observable<Menu[]> {
console.log(`Loading ${url}/index.json`);
return this.http.get<Menu[]>(`${url}/index.json`, { responseType: 'json' })
.catch((err) => this.handleError(err))
.switchMap(data => {
if (!data)
return Observable.of(null);
let children$ = [];
data.forEach((item: Menu, index: number, array: Menu[]) => {
// add to navigationTree
navArray.push(item);
if (item.loadChildrenFromUrl) { // FYI empty string is "false" in JS
item.children = new Array();
children$.push(this.loadNavigationByUrl(`${url}/${item.loadChildrenFromUrl}`, item.children));
}
});
return (children$.length) ? Observable.forkJoin(children$) : Observable.of([]);
});
}
回答2:
I now understand why @bryan60 was starting his answer with ...
Leaving aside the"WHY??" . . .
Observables recursion is really complex. I was so fixed on observables that I missed a very nice Typescript feature, . . . async/await
The code is not as elegant but much easier to read. Just in case someone is as stupid as me.
loadNavigation(): Promise<boolean> {
if (this.navigationLoaded) {
return new Promise<boolean>(resolve => {
resolve(true);
});
} else {
const navigationTreeCache = new Array();
return new Promise<boolean>(resolve => {
this.loadNavigationPromise('public', navigationTreeCache).then((value: boolean) => {
this.navigationTree = navigationTreeCache;
this.navigationLoaded = true;
console.log('navigation loaded');
resolve(true);
});
});
}
}
async loadNavigationPromise(url: string, navArray: Menu[]): Promise<boolean> {
console.log(`Loading ${url}/index.json`);
try {
// debug wait a second on each function call
// await new Promise<number>(resolve => { setTimeout(() => { resolve(); }, 1000); });
const data = await this.http.get<Menu[]>(`${url}/index.json`, { responseType: 'json' }).first().toPromise();
if (data) {
for (let i = 0; i < data.length ; i++) {
const item = data[i];
navArray.push(item);
if (item.loadChildrenFromUrl) {
item.children = new Array();
const loadSucessfully = await this.loadNavigationPromise(`${url}/${item.loadChildrenFromUrl}`, item.children);
if (!loadSucessfully) {
return false;
}
}
}
} else {
console.error(`got no data from url ${url}`);
}
return true;
} catch (error) {
console.error(error);
return false;
}
}
来源:https://stackoverflow.com/questions/47582877/angular-4-loading-tree-structure-in-recursive-observable-calls