我们在NativeScript 4.0
中引入的最重大变化之一是我们与应用程序主框架交互的方式。使其更加灵活(因此有此标题),并且更加直观。
TL;DR:旧版 NativeScript 无法在页面之间共享内容 - 需要更多内存,更多 CPU 使用率和更慢的性能
在 NativeScript 的早期版本中,我们总是在应用程序的根目录中使用单个框架(注意:框架是负责导航的元素。它在导航到这些页面时托管页面)。由于此框架是应用程序的根目录,因此它始终为全屏,因此在其中加载的所有页面也是如此。
起初,这似乎没什么大不了。我在页面 A 上,因此主框架包含 A,然后我导航到页面 B,现在主框架包含 B。当您需要添加侧边抽屉(或选项卡视图)进行导航时,问题就出现了。由于每次应用程序导航时,其所有内容都会被替换,这意味着我们必须为每个页面添加一个侧边抽屉。结果,应用程序每次导航时都会创建一个新的侧边抽屉实例,这意味着使用了更多 RAM,花费了更多 CPU 周期,并且性能下降。
TL;DR:NativeScript 4.0 可以在页面之间共享内容 - 不浪费 RAM 或 CPU 周期,性能更佳
好的,这过于简化,并不完全正确。在 NativeScript 4.0 中,您可以控制哪个元素应该是应用程序的根目录,以及在何处(以及是否)放置一个将托管页面的框架。因此,现在您可以将侧边抽屉设置为应用程序根目录,并将框架放在其主要内容内部。这实际上意味着框架将在同一个侧边抽屉实例内部交换不同的页面(实际上是重复使用它)。
乐趣不止于此。让我们关注选项卡视图示例。以前,如果您想在一个选项卡内深入导航 - 您必须创建一个类似的页面(其中包含整个选项卡视图定义),然后导航到该页面(这是因为每次导航都会交换整个屏幕内容)。现在,您可以使用更简单的方法 - 将选项卡视图作为根目录,并将框架放置在选项卡内容内部。当您在框架内导航页面时,框架之外的所有内容将保持不变。
因此,我们可以轻松地将页面的内容拆分为单独的框架
通过这种方式,侧边抽屉(或选项卡视图)只需要创建一次。结果是什么?一个资源消耗更少的应用程序,具有更愉快的用户体验。
在本文中,我们将介绍如何使用以下方法创建应用程序
使用NativeScript 4+创建的所有项目tns create
都已准备好使用灵活的框架组合。
引入的更改之一是我们引导应用程序的方式。之前在app.ts
中,我们用来调用app.start()
(从 {N} 4 开始,它被标记为已弃用),现在我们需要调用app.run()
。
API 中还有另一个细微的差异。之前app.start()
方法接受一个moduleName ,它提供了我们在应用程序加载时要加载的页面。
app.start({ moduleName: 'home/home-page' });
现在,新的 app.run()
方法接受一个moduleName ,它指向我们称之为 app-root
页面的内容,该页面需要包含一个导航框架
app.run({ moduleName: "app-root/app-root" });
正是这个框架提供了加载初始页面的路径。参见defaultPage
。
<!-- other content -->
<Frame defaultPage="home/home-page"></Frame>
<!-- other content -->
如果您正在升级现有项目,请记住还要将
nativescript
和tns-core-modules
更新到"^4.0.0"。
现在我们已经了解了基础知识,让我们看看如何将它与侧边抽屉一起使用。
在app-root.xml
中,我们只需要添加一个RadSideDrawer,其中包含通常的部分
<nsDrawer:RadSideDrawer id="sideDrawer" xmlns:nsDrawer="nativescript-ui-sidedrawer" loaded="onLoaded">
<nsDrawer:RadSideDrawer.drawerTransition>
<nsDrawer:SlideInOnTopTransition/>
</nsDrawer:RadSideDrawer.drawerTransition>
<nsDrawer:RadSideDrawer.drawerContent>
<!-- Here go the side drawer items -->
<Button text="Home" tap="goHome" />
<Button text="Browse" tap="goBrowse" />
<Button text="Search" tap="goSearch" />
</nsDrawer:RadSideDrawer.drawerContent>
<nsDrawer:RadSideDrawer.mainContent>
<!-- This is the navigation frame -->
<Frame defaultPage="home/home-page"></Frame>
</nsDrawer:RadSideDrawer.mainContent>
</nsDrawer:RadSideDrawer>
这是结构化app-root
的第一步。接下来,我们需要一种方法从主页导航到另一个页面。
这实际上非常简单。您只需要调用topmost()
来获取主框架,然后调用navigate()
并提供组件的路径作为modulePath。
之后,我们需要获取drawerComponent并使用closeDrawer()
将其隐藏。
import * as app from "application";
import { RadSideDrawer } from "nativescript-ui-sidedrawer";
import { topmost } from "ui/frame";
export function goBrowse() {
topmost().navigate({
moduleName: "browse/browse-page"
});
const drawerComponent = <RadSideDrawer>app.getRootView();
drawerComponent.closeDrawer();
}
您可以在 Playground 中看到它的实际效果
为了方便起见,已经有一个项目模板,其中已配置了侧边抽屉。您可以在 市场中找到它。
要使用侧边抽屉创建一个新项目,请调用tns create my-drawer-ts --template tns-template-drawer-navigation-ts
。
如果选项卡视图是您首选的应用程序导航方式,则过程略有不同。
在app-root.xml
中,我们需要一个TabView组件,其androidTabsPosition
属性设置为bottom
<TabView androidTabsPosition="bottom">
androidTabsPosition
不仅更改了选项卡视图的位置(从 Android 的顶部到底部),还更改了其行为。因此,当选项卡视图加载时,只有当前可见选项卡的内容被加载到内存中。
然后,对于每个选项卡,我们需要一个TabViewItem,其中包含一个Frame组件 - 请注意,每个框架都有自己的默认页面。
<TabView androidTabsPosition="bottom">
<TabViewItem title="Home">
<Frame defaultPage="home/home-page"></Frame>
</TabViewItem>
<TabViewItem title="Browse">
<Frame defaultPage="browse/browse-page"></Frame>
</TabViewItem>
<TabViewItem title="Search">
<Frame defaultPage="search/search-page"></Frame>
</TabViewItem>
</TabView>
这将负责在不同选项卡之间导航,并且每个框架将在需要时加载其页面。
您还可以独立于所有其他框架在每个框架中导航。
我们需要做的就是获取当前页面对象,然后获取其框架。然后,一旦我们有了框架,就可以像这样调用navigate
<Button text="Navigate" tap="onItemTap"></Button>
export function onItemTap(args: ItemEventData) {
const frame = args.view.page.frame;
frame.navigate({
moduleName: "home/another-page",
})
}
现在,如果您运行包含此代码的应用程序,您将能够导航到another-page,当您移动到Browse或Search选项卡并返回Home选项卡时,Home选项卡仍然会显示another-page。
在 iOS 上,点击当前打开的选项卡会自动导航到其defaultPage。因此,如果您在Home选项卡中,并且您再次点击
Home
选项卡,则该选项卡将返回到home/home-page
。
iOS 会自动将一个后退按钮添加到操作栏。但是,对于 Android,您需要自己处理它。
这可以通过将一个导航按钮添加到您的操作栏来轻松解决
<ActionBar class="action-bar">
<NavigationButton tap="onBackButtonTap" android.systemIcon="ic_menu_back" />
<Label class="action-bar-title" text="{{ name }}"></Label>
</ActionBar>
然后,在onBackButtonTap
上,您需要获取框架并调用goBack()
。就像这样
export function onBackButtonTap(args: EventData) {
const view = args.object as View;
const frame = view.page.frame;
frame.goBack();
}
您可以在 Playground 中看到它的实际效果。
或者,您可以使用 来自市场上的现成模板
要使用选项卡视图创建一个新项目,请调用tns create my-tab-ts --template tns-template-tab-navigation-ts
。
在 4.0 之前,所有 NativeScript 应用程序都有一个由application.start()
方法隐式创建的顶级框架。frameModule.topmost()
方法返回此框架。在 4.0 中,我们现在允许您的应用程序具有多个框架实例。这需要不同的框架检索 API。以下是在 4.0 中获取所需框架的方法
frameModule.topmost()
- 旧方法仍然存在,不仅是为了向后兼容。它现在返回最后导航的框架,或者如果您在选项卡视图中,则返回当前选定选项卡项的框架。这意味着,在简单的情况下,您应该仍然能够依赖此方法。但是,对于更复杂的情况或仅仅是为了获得更多控制,您应该使用以下方法。
frameModule.topmost()
非常适合选项卡视图导航
eventArgs.object.page.frame
- 这可以在事件处理程序中使用。所有事件参数对象都应该包含对其页面的引用,该页面反过来又包含对其框架实例的引用。这使您能够检索显示事件对象的精确框架。
eventArgs.object.page.frame
非常适合侧边抽屉导航
frameModule.getFrameById(id)
- 这是一个新方法。它允许您通过您在元素上指定的 ID 获取对框架的引用。请注意,这将搜索已导航的框架,而不会找到尚未显示的框架,例如在模态视图中。
frameModule.getFrameById(id)
在页面上有多个框架时非常有用,例如在拆分屏幕情况下。
为了将灵活的框架组合与 Angular 一起使用,您需要使用
nativescript-angular: 5.3.0
或nativescript-angular: next
或更新版本(6.0.0
即将推出)@angular/x: 6.0.0
与在 NativeScript 的早期版本中使用侧边抽屉相比,我们需要做的唯一更改是将所有侧边抽屉实例移动到您的主组件(默认情况下是AppComponent
),该组件应该包含
tkDrawerContent
指令标记的布局容器 - 它包含抽屉内容使用tkMainContent
指令标记的page-router-outlet - 它包含应用程序主内容
请注意,page-router-outlet等同于 NativeScript Core 的Frame 组件。
就像这样
<RadSideDrawer [drawerTransition]="sideDrawerTransition">
<GridLayout tkDrawerContent rows="auto, *" class="sidedrawer sidedrawer-left">
<StackLayout row="0" class="sidedrawer-header">
<Label class="sidedrawer-header-image fa" text=""></Label>
<Label class="sidedrawer-header-brand" text="User Name"></Label>
<Label class="footnote" text="[email protected]"></Label>
</StackLayout>
<ScrollView row="1">
<StackLayout class="sidedrawer-content">
<Button text="home" [nsRouterLink]="['/home']" (tap)="hideDrawer()" class="btn btn-primary"></Button>
<Button text="browse" [nsRouterLink]="['/browse']" (tap)="hideDrawer()" class="btn btn-primary"></Button>
<Button text="search" (tap)="goSearch()" class="btn btn-primary"></Button>
</StackLayout>
</ScrollView>
</GridLayout>
<page-router-outlet tkMainContent class="page page-content"></page-router-outlet>
</RadSideDrawer>
现在导航是相当直观的 Angular 体验。
您可以从以下位置进行
TypeScript
通过将RouterExtensions
注入到AppComponent,然后调用this.routerExtensions.navigate()
。最后,为了整理,我们需要获取抽屉并将其关闭。就像这样
constructor(private routerExtensions: RouterExtensions) {}
goSearch() {
// navigate
this.routerExtensions.navigate(['/search'], {
transition: { name: "flip" }
});
// hide drawer
const sideDrawer = <RadSideDrawer>app.getRootView();
sideDrawer.closeDrawer();
}
通过使用nsRouterLink
提供导航路径,以及(可选)pageTransition
提供过渡类型。最后,为了整理,我们需要关闭抽屉。
<Button
text="browse"
[nsRouterLink]="['/browse']"
(tap)="hideDrawer()"
pageTransition="flip">
</Button>
hideDrawer() {
const sideDrawer = <RadSideDrawer>app.getRootView();
sideDrawer.closeDrawer();
}
您可以在 Playground 中看到它的实际效果
参见AppComponent以了解所有神奇之处 😉
或者,您可以使用 来自市场上的现成模板
要使用侧边抽屉创建一个新项目,请调用tns create my-drawer-ng --template tns-template-drawer-navigation-ng
。
在侧边抽屉示例中,我们只需要一个页面路由出口。但是,对于选项卡视图导航,我们需要多个命名页面路由出口,此功能不是nativescript-angular: 5.3
的一部分,该版本是撰写本文时该 npm 模块的最新官方版本。这将在nativescript-angular: 6.0
中正式提供,因此,目前我们将使用nativescript-angular@next
。
命名页面路由出口只是一个带有name
属性的page-router-outlet
,就像这样
<page-router-outlet name="myTab"></page-router-outlet>
名称允许我们将每个页面路由出口链接到我们**路由配置**中的一个outlet
。(有关详细信息,请参阅app.routing.ts
部分)
例如,如果我们设想一个使用两个标签的应用程序
那么我们需要在app.component.html
中添加一个带有两个**命名**的page-router-outlet组件的TabView。
<TabView androidTabsPosition="bottom">
<page-router-outlet
*tabItem="{title: 'Players'}"
name="playerTab">
</page-router-outlet>
<page-router-outlet
*tabItem="{title: 'Teams'}"
name="teamTab">
</page-router-outlet>
</TabView>
现在我们需要配置我们的路由。这与常规的**路由配置**非常相似。
以下是我们的路径,没有**多个**page-router-outlet
const routes: Routes = [
{ path: '', redirectTo: '/players', pathMatch: 'full' },
{ path: 'players', component: PlayerComponent },
{ path: 'player/:id', component: PlayerDetailComponent },
{ path: 'teams', component: TeamsComponent},
{ path: 'team/:id', component: TeamDetailComponent },
];
首先,我们需要为每个路径添加**outlet**属性,将其分配给特定的路由出口。例如,对于**players**路径,我们需要将其分配给**playerTab**,就像这样
{ path: 'players', component: PlayerComponent, outlet: 'playerTab' },
然后我们需要更新默认的(**redirectTo**)路径。像这样
{ path: '', redirectTo: '/(playerTab:players//teamTab:teams)', pathMatch: 'full' },
注意,
'/'
,这是AppComponent的路径,( )
中,我们定义tabname:path
//
分隔以下是示例的完整配置
const routes: Routes = [
{ path: '', redirectTo: '/(playerTab:players//teamTab:teams)', pathMatch: 'full' },
{ path: 'players', component: PlayerComponent, outlet: 'playerTab' },
{ path: 'player/:id', component: PlayerDetailComponent, outlet: 'playerTab' },
{ path: 'teams', component: TeamsComponent, outlet: 'teamTab' },
{ path: 'team/:id', component: TeamDetailComponent, outlet: 'teamTab' },
];
现在,我们只需要创建所有四个组件,我们就可以开始运行了。
如果我们有一个带有**登录页面**和**标签页面**的应用程序,那么我们的配置看起来像这样
const routes: Routes = [
{ path: '', redirectTo: '/login', pathMatch: 'full' },
//{ path: '', redirectTo: '/tabs/(playerTab:players//teamTab:teams)', pathMatch: 'full' },
{ path: 'login', component: LoginComponent },
{ path: 'tabs', component: TabsComponent, children: [
{ path: 'players', component: PlayerComponent, outlet: 'playerTab' },
{ path: 'player/:id', component: PlayerDetailComponent, outlet: 'playerTab' },
{ path: 'teams', component: TeamsComponent, outlet: 'teamTab' },
{ path: 'team/:id', component: TeamDetailComponent, outlet: 'teamTab' },
]}
];
拼图的最后一块是导航。
要从组件类导航,我们需要注入RouterExtensions
和ActivateRoute
,
import { RouterExtensions } from 'nativescript-angular/router';
import { ActivatedRoute } from '@angular/router';
...
export class PlayerComponent {
...
constructor(
private router: RouterExtensions,
private route: ActivatedRoute
) { }
...
}
players
到player/:id
当我们导航时,我们可以提供{ relativeTo: this.route }
,这会告诉路由器它应该导航到哪个page-router-outlet
。因此,我们可以简单地通过调用以下内容来导航
navigateToPlayer(id: number) {
this.router.navigate(['../player', id], { relativeTo: this.route });
}
注意:
../<sibling>
是一种引用兄弟路径的方法。这正是您从home/a
到home/b
导航的方式,方法是提供../b
路径。
player/x
到players
每次您导航时,**导航堆栈**都会更新。因此,iOS 在 ActionBar 中为您提供了一个后退按钮,而 Android 允许您使用系统后退按钮后退。
当您只是导航回主页或默认页面时,这没有用。幸运的是,您可以通过添加clearHistory: true
属性来避免这种情况。 navigateToPlayers() { this.router.navigate(['../../players'], { relativeTo: this.route, clearHistory: true }); }
注意#1:
clearHistory
只会清除被导航的**page-router-outlets**的历史记录。因此,当您在**playersTab outlet**中导航时,**teamsTab outlet**保持不变。
注意#2:当您尝试从
pageA/param
导航到../pageB
时,将会带您到pageA/pageB
并失败。由于我们当前路径中有一个参数,因此我们还需要通过导航到../../pageB
来转义它。
我们也可以使用绝对路径进行导航。这使用**outlets**属性以及我们需要的出口名称以及我们希望它导航到的路径来完成,就像这样
this.router.navigate([{
outlets: {
outletName: ['path']
}
}]);
players
到player/x
navigatePlayerOutlet(id: number) {
this.router.navigate([{
outlets: {
playerTab: ['player', id]
}
}]);
}
注意:路径和参数必须分别提供
好的:playerTab: ['player', id]
不好:
playerTab: ['player/id']
players
到team/x
您也可以在另一个出口中进行导航,就像这样
navigateTeamOutlet(id: number) {
this.router.navigate([{
outlets: {
teamTab: ['team', id]
}
}]);
}
注意:这不会切换标签到**Team Tab**,但是您导航到那里后,**Team Tab**将显示
team/id
。
players
到player/x
和team/x
navigateOutlets(playerId: number, teamId: number) {
this.router.navigate([{
outlets: {
playerTab: ['player', playerId],
teamTab: ['team', teamId],
}
}]);
}
players
到team/x
这种方法通常用于更新所有出口。语法与我们在app.routing.ts
中为redirectTo
使用的语法相同。
在这种情况下,它允许我们在teamTab中导航到team/id
,同时将**playerTab**保持在players
。
navigateTeamUrl(id: number) {
this.router.navigateByUrl(`/(playerTab:players//teamTab:team/${id})`);
}
player/x
到player/y
要做到这一点,我们只需要后退一步../
,这将带我们到/player
,然后提供新的 id。像这样
navigateToPlayer(id: number) {
this.router.navigate(['../', id], { relativeTo: this.route });
}
player/x
到player/x+1
当使用绝对路径导航时,我们无法告诉路由器保持在相同的路径上。因此,我们必须每次都提供完整的路径。
navigateNext() {
this.router.navigate([{
outlets: {
playerTab: ['player', this.player.id + 1]
}
}]);
}
我们也可以直接从 html 导航。这是通过nsRouterLink
属性的支持来实现的。
注意:**nsRouterLink**可以用于任何组件:**Button**、**Label**甚至**StackLayout**。
teams
到team/1
当我们使用相对路径时,路由器将自动假设您希望在相同的**page-router-outlet**中导航。
以下是我们从teams
到team/1
导航的方式。首先,我们需要../
后退一步,然后添加team/1
,就像这样
<Button
text="Open Team One"
nsRouterLink="../team/1">
</Button>
teams
到team/2
<Button
text="Open Team Two with Animation"
nsRouterLink="../team/2"
pageTransition="flip"
pageTransitionDuration="3000">
</Button>
teams
到team/x
当您想要添加一个应该从变量中提取的参数时,我们需要在nsRouterLink
周围使用[ ]
,然后将导航作为项目数组提供。
要从teams
到team/id
导航,首先我们需要['../team'
路径到 team,然后是参数值team.id]
,就像这样
<ListView [items]="items" class="list-group">
<ng-template let-team="item">
<Label
[text]="team.name"
[nsRouterLink]="['../team', team.id]">
</Label>
</ng-template>
</ListView>
teams
到team/5
要使用相对路径导航,我们需要提供一个带有以下内容的数组:
'/'
outlets
配置的对象,其中包含我们需要导航到的出口名称<Button
text="Navigate using Outlet"
[nsRouterLink]="['/', { outlets: { teamTab: ['team', 5] } }]">
</Button>
teams
到player/5
当我们希望在另一个标签中的**page-router-outlet**中导航时,这也有效,就像这样
<Button
text="Navigate in Players Tab"
[nsRouterLink]="['/', { outlets: { playerTab: ['player', 5] } }]">
</Button>
注意:这将更改**playerTab**的内容,但不会将当前标签更改为**Players Tab**。
teams
到team/6
和player/6
为了使它更好,您甚至可以一次性导航多个出口。像这样
<Button
text="Navigate in Two Outlets"
[nsRouterLink]="[
'/',
{ outlets:
{
teamTab: ['team', 6],
playerTab: ['player', 6]
}
}]">
</Button>
team/x
到team/x+1
当您需要重新导航到同一页面,但具有不同的参数时,您只需要使用../
后退一步,然后提供新的参数即可。像这样
<Button
text="prev"
[nsRouterLink]="['../', team.id - 1]">
</Button>
team/x
到team/x+1
当您需要使用绝对路径重新导航到同一页面时,您需要每次都提供完整的路径。像这样
<Button
text="next"
[nsRouterLink]="['/', { outlets: { teamTab: ['team', team.id + 1] } }]">
</Button>
team/x
到teams
最后,当您需要导航回主页组件时,您可能还想清除导航堆栈,以便 iOS 会删除后退按钮。这是通过添加clearHistory="true"
来完成的,就像这样
<Button
text="Back to Teams"
[nsRouterLink]="['../../teams']"
clearHistory="true">
</Button>
在撰写本文时,Playground 使用的是nativescript-angular: 5.3
,它不支持Named Page Router Outlets
。因此没有 Playground 演示。
您可以在GitHub - frame-tabview-example中查看我的示例项目。
NativeScript 团队正在努力更新当前的tab-navigation-ng
模板,您应该能够从市场中获取它。
要检查模板是否已更新,只需检查GitHub 中的 app.component.html即可。它应该很快准备好。
要使用 TabView 创建新项目,请调用tns create my-tab-ng --template tns-template-tab-navigation-ng
。
您还可以使用其他场景
frameModule.getFrameById(id)
用于 TS,以及希望您觉得它有用,并且我成功涵盖了您可能需要的多数场景。
如果您有任何未涵盖的场景,或在 GitHub 或 Playground 中共享您使用灵活框架组合的项目,请告诉我。
或在评论中分享您的反馈。