返回博客首页
← 所有文章

使用 Tab 导航为 NativeScript 应用实现登录

2018 年 12 月 18 日 — 作者:Alexander Djenkov

启用嵌套辅助页面出口使我们能够构建许多新的、更灵活的应用场景。在详细介绍之前,让我们先了解一下 Angular 中的辅助出口是什么。

“Angular 支持辅助路由的概念,它允许您在一个应用程序中设置和导航多个独立的路由。每个组件都有一个主路由和零个或多个辅助出口。辅助出口在组件内必须具有唯一的名称。

在 Web Angular 中,这些辅助出口(也称为命名出口)通常用于在屏幕的一部分实现侧边导航。在 NativeScript Angular 中,我们使用此功能来提供所谓的横向导航。这指的是在导航层次结构的同一级别在屏幕之间导航。NativeScript 的辅助页面路由出口的主要用途是在横向导航组件中,例如TabViewModal View。更多内容 - 稍后介绍。

背景故事

[email protected]之前,框架仅部分支持辅助出口。定义可以拥有自己独立路由的命名出口,仅在应用程序导航的根级别(通常是app.component)才有可能。

const routes: Routes = [
    { path: "", redirectTo: "/(homeTab:home//browseTab:browse//searchTab:search)", pathMatch: "full" },

    { path: "home", component: HomeComponent, outlet: "homeTab" },
    { path: "browse", component: BrowseComponent, outlet: "browseTab" },
    { path: "search", component: SearchComponent, outlet: "searchTab" }
];

上述设置要求您在app.component中拥有TabView,以便在每个选项卡项内进行内部页面导航。到目前为止还不错,但是如果我们想要一些不在TabView(已经是根组件)内的其他页面,例如登录页面呢?

直到现在,还没有办法实现这一点。

我们为什么要改变它

引入嵌套出口功能使我们更接近原生 Angular 辅助出口。我们不再需要 TabView 作为根组件才能进行横向导航。我们现在可以在app.component中设置一个常规的<page-router-outlet>,并为登录选项卡页面定义单独的路由。选项卡页面现在可以拥有自己的子路由,用于各个选项卡。

请参阅下面的图表,以直观了解如何更改当前应用程序设置以处理登录 + 选项卡场景。

没有“嵌套辅助页面出口”功能(旧方法)

without nested aux page outlets - old way

有“嵌套辅助页面出口”功能

with nested aux page outlets

示例应用程序

让我们回到拥有登录页面的应用程序,然后在成功登录或点击按钮后导航到欢迎页面。欢迎页面上有一个按钮,可以链接到TabView页面,每个选项卡中都有主细节导航。

您可以在此 Github 存储库中找到涵盖上述场景并使用新的嵌套出口方法的完整应用程序。我们现在将更详细地介绍此应用程序的一些关键点。首先,让我们阐明我们想要实现的确切工作流程。

login app scenario

请注意,两个主/细节页面将显示在单独的选项卡中,用户可以在每个选项卡中独立地向前导航。

路由

上述导航架构需要以下路由

(1.0) app-routing.module

{ path: "", redirectTo: "/login", pathMatch: "full" },
{ path: "login", component: LoginComponent },
{ path: "welcome", component: WelcomeComponent }
{ path: "tabs", loadChildren: "~/app/tabs/tabs.module#TabsModule" },

为了使此应用程序更接近实际应用程序场景,我们将为选项卡页面(带有TabView的页面)以及两个选项卡内嵌套的主/细节页面使用延迟加载的模块。

(1.1) TabsModule 路由

import { NSEmptyOutletComponent } from "nativescript-angular/router";

...

{
   path: "default", component: TabsComponent, children: [
       {
           path: "players",
           outlet: "playerTab",
           component: NSEmptyOutletComponent,
           loadChildren: "~/app/player/players.module#PlayersModule",
       },
       {
           path: "teams",
           outlet: "teamTab",
           component: NSEmptyOutletComponent,
           loadChildren: "~/app/team/teams.module#TeamsModule"
       }
   ]
}

(1.2) 嵌套的PlayersModule/TeamsModule路由

{ path: "", redirectTo: "players" },
{ path: "players", component: PlayerComponent },
{ path: "player/:id", component: PlayerDetailComponent}

(1.3)

{ path: "", redirectTo: "teams" },
{ path: "teams", component: TeamsComponent },
{ path: "team/:id", component: TeamDetailComponent }

您可能已经注意到 (1.1) 中各个选项卡路径的组件属性值 - NSEmptyOutletComponent。当您需要定义命名延迟加载页面出口路径时,现在必须将您的组件设置为NSEmptyOutletComponent

前向导航

我们将从一些前向导航开始探索应用程序。在登录页面成功登录后,我们希望导航到欢迎页面,而不保留历史记录(我们不希望在点击后退按钮时返回到登录页面),如下所示。

this.routerExtension.navigate(["../welcome"], { clearHistory: true });

现在让我们进一步操作,导航到选项卡页面,但这次我们希望保留欢迎页面的历史记录,以便稍后返回。由于无需为此前向导航传递任何额外参数,因此我们可以直接使用WelcomeComponent模板中的<Button>进行导航。

<Button text="Go To Tabs Page" [nsRouterLink]="['../tabs/default']"></Button>

但是,在我们导航到主页后,发生了一些奇怪的事情。有一个白色的空TabView

empty tabview

我们的应用程序崩溃了吗,还是发生了其他事情?实际上,应用程序运行良好,因为我们只是导航到加载了TabsComponent及其TabView的选项卡页面。如果您返回到TabsModule 路由定义 (1.1),您会看到tabs路径有两个子路径 - playersteams。这两个路径都有出口名称,以及<TabView>模板中具有名称的相应<page-router-outlet>

<TabView androidTabsPosition="bottom">
   <page-router-outlet *tabItem="{title: 'Players Tab'}" name="playerTab">
   </page-router-outlet>

   <page-router-outlet *tabItem="{title: 'Teams Tab'}" name="teamTab">
   </page-router-outlet>
</TabView>

这意味着我们必须手动将这些出口导航到其所需的路由。执行此操作的最佳位置是在TabsComponentngOnInit()内部。

this.routerExtension.navigate(
   [{ outlets: { playerTab: ["players"], teamTab: ["teams"] } }],
   { relativeTo: this.activeRoute }
);

在上面的代码片段中,有两点需要注意。

  1. 始终将精确的<page-router-outlet>名称传递给outlets集合对象。
  2. 相对于当前激活路由进行导航至关重要,因为playersteams是当前路由tabs的子路径。

注意:如果没有传递{ relativeTo: this.activeRoute },则导航将是绝对的。

将两个嵌套出口导航到playersteams路由后,Angular 将分别加载其对应的延迟加载模块PlayersModuleTeamsModule。这两个模块都定义了自己的路径路由。

{ path: "", redirectTo: "players" },
{ path: "players", component: PlayerComponent },
{ path: "player/:id", component: PlayerDetailComponent }

{ path: "", redirectTo: "teams" },
{ path: "teams", component: TeamsComponent },
{ path: "team/:id", component: TeamDetailComponent }

在返回之前,我们将在此应用程序中进行最后一次前向导航。由于当前可见的选项卡项是加载了 player 的选项卡项,因此我们将通过点击其<Label>导航到第一个 player 详细信息

<Label [nsRouterLink]="['../player', item.id]" [text]="item.name"></Label>

后退导航

现在是时候解释 Web 方法与 NativeScript 方法之间的一个重要区别了。在 Web 中,默认情况下,您有一个由您遍历的 URL 序列定义的线性历史记录。点击浏览器上的后退按钮将带您到您访问的上一个 URL。简而言之,这意味着所有出口都使用一个导航控制器。

在这方面,移动导航略有不同。在移动设备上,预期不同的导航容器将具有单独的导航控制器。对于用户而言,这意味着他们可以在每个选项卡中以及根控制器中独立地后退。为了在 NativeScript Angular 中适应这一点,每个页面路由出口本身都包含一个导航控制器。这意味着每个出口都有自己的历史记录,您可以在每个出口中单独后退。

当前应用程序的导航堆栈如下所示。

navigation stack

导航堆栈中有三个出口。

  • primary出口在其堆栈中包含welcometabs路由(请记住,登录页面不再可用,因为我们已使用clearHistory:true从其导航)。
  • playerTab出口包含playersplayer/1路由。
  • teamTab出口包含teams路由。

我们现在位于PlayerDetailsComponent中,并为第一个 player 详细信息激活了路由 - player/1。这里有四个后退导航按钮,接受不同的参数,并在不同的<page-router-outlet>中执行后退导航。

➡️ Back() 将导航回最后导航到的出口。

this.routerExtension.back();

在我们的例子中,这是playersOutlet,因为我们在最后一次前向导航中导航到player/1详细信息。目前,此出口有两个导航状态 - playersplayer/1Back() 将把我们送回players状态(Players 页面)。

➡️ Back(ActivatedRoute) 将导航回与当前激活路由相关的出口,在本例中为playersOutlet

this.routerExtension.back({ relativeTo: this.activeRoute});

在此处使用Back(ActivatedRoute)将导致与使用Back()相同的后退导航。

➡️ Back(OutletName) 将导航回特定出口 - 在app.component.html中定义的<page-router-outlet>

this.routerExtension.back({ outlets: ["primary"] });

仅使用outlets参数进行导航将在根app-routing.module.ts级别搜索给定的出口。所有未命名的<page-router-outlets>都自动使用分配的primary名称。Back(OutletName)按钮将把我们导航回欢迎页面,因为它是选项卡之前的页面。

➡️ Back(ParentRoute) 将使应用程序返回到与Back(OutletName)按钮相同的页面 - 欢迎页面。

this.routerExtension.back({ relativeTo: this.activeRoute.parent });

当前激活路由父级指向在app.component.html中定义的<page-router-outlet>,其上一页是欢迎页面。

请记住,Back(OutletName)Back(ParentRoute)导航都将忽略任何嵌套出口导航(playersOutletteamsOutlet)。

Back() 和 canGoBack() API 的更多示例

以下是一些在处理辅助出口时可能派上用场的其他导航场景。

➡️ Back outlet 相对于特定路由。

this.routerExtension.back({ outlets: ["teamTab"], relativeTo: this.activeRoute });

➡️ Back(..) 同时后退多个出口 相对于根激活路由。

this.routerExtension.back({ outlets: ["teamTab", "playerTab"]});

➡️ Back(..) 同时后退多个出口 相对于特定路由。

this.routerExtension.back({ outlets: ["teamTab", "playerTab"], relativeTo: this.activeRoute.parent });

➡️ CanGoBack(..) 可以与Back()的参数变化一起使用。

this.routerExtension.canGoBack({ outlets: ["teamTab", "playerTab"], relativeTo: this.activeRoute });

请记住,只有当所有提供的出口都可以后退时,CanGoBack()才会返回true

使用辅助页面出口可以使您的应用程序更接近本机移动应用程序模式。有关使用新功能的完整应用程序示例,您可以查看login-tabs 存储库