返回博客首页
← 所有文章

如何扩展自定义路由复用策略

2019 年 1 月 10 日 — 作者 Sebastian Witalec

我经常被要求帮忙解决各种 NativeScript 挑战。我想分享一下最近遇到的一个问题,这个问题在过去也出现过几次。问题如下:

挑战

我有一个类似报纸的 Angular 主细节应用程序。主页面显示要阅读的文章列表。当你点击其中一篇文章时,应用程序就会导航(并使用一个漂亮的页面过渡效果),并将 articleID 传递到细节页面。

这里有一个关键点。在我的细节页面,我的应用程序会显示其他相关文章。当用户点击另一篇文章时,应用程序会重新导航到细节页面,并使用新的 articleId。此时我可以检索新的 articleId 并加载新文章。但是,问题在于,由于应用程序导航到同一个组件,我无法使用页面过渡,而且在 iOS 上,**操作栏**中的**返回按钮**仍然指向主页面。

发生了什么?

what-is-happening

在有人开始新的 Twitter 战争之前,我要说这是**预期的行为**。让我解释一下。

创建一个新组件需要付出代价,因为它需要大量的处理周期来创建页面以及每个 Native UI 组件。因此,理想情况下,我们应该尽可能避免这种情况。

在移动应用程序中,当你导航回前一页(例如,从细节页面导航回主页面)时,你想要避免创建新组件。在这种情况下,你希望你的应用程序快速导航,没有任何延迟。

为了实现这一点,我们需要实现一个**自定义路由复用策略**(CRRS)。问题是,Angular 提供的默认 CRRS 会在重新导航到自身时自动复用组件……因此产生了挑战。

你可以在Angular 文档中了解更多信息。

第一直觉 - 补丁

hack

我的第一直觉是做一个快速的补丁。如果从 DetailComponent 导航到 DetailComponent 会导致问题,那么为什么不创建该组件的副本,并将导航从**DetailA**到**DetailB**,再到**DetailA**,以此类推呢?

类似于这样:

export class ItemDetailComponent implements OnInit {
    protected container = 'a';
    private item: Item;

    get detailsPath(): string {
        return (this.container === 'a') ? '/item-b' : '/item-a';
    }

    constructor(protected itemService: ItemService, protected route: ActivatedRoute, protected router: RouterExtensions) { }

    ngOnInit(): void {
        const id = +this.route.snapshot.params['id'];
        this.item = this.itemService.getItem(id);
    }

    navigate(id: number) {
        this.router.navigate([this.detailsPath, id]);
    }
}

@Component({
    selector: 'ns-details-a',
    moduleId: module.id,
    templateUrl: './item-detail.component.html',
})
export class ItemDetailComponentA extends ItemDetailComponent {

    constructor(itemService: ItemService, route: ActivatedRoute, router: RouterExtensions) {
        super(itemService, route, router);
        this.container = 'a';
    }
}

@Component({
    selector: 'ns-details-b',
    moduleId: module.id,
    templateUrl: './item-detail.component.html',
})
export class ItemDetailComponentB extends ItemDetailComponent {

    constructor(itemService: ItemService, route: ActivatedRoute, router: RouterExtensions) {
        super(itemService, route, router);
        this.container = 'b';
    }
}

完整的补丁

Playground中查看完整示例。

补丁的结论

虽然这种方法有效,但它不是一个很好的解决方案,它是一个很好的补丁,但不是一个很好的解决方案。最重要的是:

  • 你的代码变得臃肿,
  • 试图欺骗框架做其他事情,
  • 两周后你甚至不记得为什么这样做,
  • 它可能比它解决的问题造成更多的问题。

bad-solution

如果你有多个这样的细节组件怎么办?我们会开始质疑 Angular 和 NativeScript 是否是适合我们工作需要的框架吗?

nope

“自定义”自定义路由复用策略 - 正确的解决方案

然后我和 NativeScript 核心团队的 Alex Vakrilov 谈了谈……他说:

slap

好吧,回到最初的设计图纸。相反,我们应该**扩展**现有的**自定义路由复用策略**,使其满足我们应用程序的要求。

配置路径 - noReuse 标志

首先,在**路由**配置中,我们需要添加一个标志,帮助我们识别哪些路径不应该被复用。在我们的例子中,就是**DetailComponent**。这可以通过在细节路径中添加data: { noReuse: true }来实现。就像这样:

app-routing.module.ts

const routes: Routes = [
    { path: '', redirectTo: '/items', pathMatch: 'full' },
    { path: 'items', component: ItemsComponent },
    { path: 'item/:id', component: ItemDetailComponent, data: { noReuse: true } },
];

然后,每次应用程序导航时,我们就可以查找noReuse标志。

创建一个新的自定义路由复用策略

接下来,我们需要创建一个新的自定义路由复用策略类。

它应该扩展NSRouteReuseStrategy

export class CustomRouteReuseStrategy extends NSRouteReuseStrategy {

使用依赖注入获取NSLocationStrategy,并将其传递给父构造函数

constructor(location: NSLocationStrategy) {
    super(location);
}

最后,它应该实现shouldReuseRoute函数,该函数应该返回truefalse

shouldReuseRoute(future: ActivatedRouteSnapshot, current: ActivatedRouteSnapshot): boolean {
    // return (shouldIReuse) ? true : false;
}

实现 shouldReuseRoute()

这是我们需要专注于正确解决方案的地方。

我们**必须始终**从父级调用shouldReuseRoute(),因为它用于**标记**页面特定的激活路由,并且结果将让我们知道它何时打算复用当前路由。

let shouldReuse = super.shouldReuseRoute(future, current);

如果shouldReuse为 true,那么我们可能正处于从ItemDetailComponent到自身重新导航的场景中。此时,我们需要检查noReuse标志是否设置为true。如果是,那么应用程序将从ItemDetailComponent重新导航到自身。为了避免复用此路径,只需将shouldReuse更改为false

if (shouldReuse && current.data.noReuse) {
    shouldReuse = false;
}

最后,我们需要返回shouldReuse的值。

return shouldReuse;

以下是完整的 custom-route-reuse-strategy.ts

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { NSLocationStrategy } from 'nativescript-angular/router/ns-location-strategy';
import { NSRouteReuseStrategy } from 'nativescript-angular/router/ns-route-reuse-strategy';

@Injectable()
export class CustomRouteReuseStrategy extends NSRouteReuseStrategy {

    constructor(location: NSLocationStrategy) {
        super(location);
    }

    shouldReuseRoute(future: ActivatedRouteSnapshot, current: ActivatedRouteSnapshot): boolean {
        // first use the global Reuse Strategy evaluation function,
        // which will return true, when we are navigating from the same component to itself
        let shouldReuse = super.shouldReuseRoute(future, current);

        // then check if the noReuse flag is set to true
        if (shouldReuse && current.data.noReuse) {
            // if true, then don't reuse this component
            shouldReuse = false;
        }

        console.log(`Should Reuse: ${shouldReuse}`);
        return shouldReuse;
    }
}

在 AppModule 中提供新的 CRRS

最后一步是让 Angular 知道我们想要使用新的CustomRouteReuseStrategy

这可以通过将新的 CRRS 类作为RouteReuseStrategy添加到AppModule中来实现,就像这样:

app.module.ts

@NgModule({
    ...
    providers: [
        {
            provide: RouteReuseStrategy,
            useClass: CustomRouteReuseStrategy
        }
    ],
    ...
})

完整的解决方案

你可以在Playground中查看完整的解决方案。

custom-route-reuse-strategy

结论

well-done

如你所见,实现自定义路由复用策略不仅是最佳解决方案,而且简单而优雅。你可以非常容易地将noReuse标志添加到任何你不想复用的路径,同时保留其他所有路径的标志。

请在评论中告诉我们你的想法。

你以前遇到过这个挑战吗?

是否有任何这种情况没有涵盖?