Skip to content

Latest commit

 

History

History
1247 lines (989 loc) · 47.9 KB

File metadata and controls

1247 lines (989 loc) · 47.9 KB

五、路由和延迟加载

路由对于任何应用的可靠可用性流程都至关重要。让我们了解一下移动应用路由配置的关键要素,该应用利用了 Angular 路由提供的所有灵活性。

在本章中,我们将介绍以下主题:

  • 使用 NativeScript 应用配置角度路由
  • 通过路由延迟加载模块
  • 为 Angular 的 NgModuleFactoryLoader 提供 NSModuleFactoryLoader
  • 了解如何将路由出口与页面路由出口结合使用
  • 了解如何跨多个延迟加载模块共享单例服务
  • 使用身份验证保护来保护需要有效身份验证的视图
  • 了解NavigationButton定制后台移动导航
  • 通过引入后期功能需求,利用我们灵活的路由设置

在 66 号公路上尽情享受吧

当我们沿着这条充满冒险的公路开始我们的旅程时,让我们先在我们当地的服务店停车,以确保我们的车辆处于最佳状态。进入app的根目录,为我们的车辆引擎构建一个新的附加组件:路由模块。

新建路由模块app/app.routing.ts,包含以下内容:

import { NgModule } from '@angular/core';
import { NativeScriptRouterModule } 
  from 'nativescript-angular/router';
import { Routes } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    redirectTo: '/mixer/home',
    pathMatch: 'full'
  },
  {
    path: 'mixer',
    loadChildren: () => require('./modules/mixer/mixer.module')['MixerModule']
  },
  {
    path: 'record',
    loadChildren: () => require('./modules/recorder/recorder.module')['RecorderModule']
  }
];

@NgModule({
  imports: [
    NativeScriptRouterModule.forRoot(routes)
  ],
  exports: [
    NativeScriptRouterModule
  ]
})
export class AppRoutingModule { }

定义根''路径以重定向到延迟加载的模块提供了非常灵活的路由配置,您将在本章中看到这一点。您将看到一个新模块,MixerModule,我们将立即创建它。事实上,它将在很大程度上成为AppComponent现在的样子。下面列出了使用类似于此的路由配置所获得的一些优势:

  • 通过急切地只加载最小根模块配置,然后缓慢地快速加载第一个路由的模块,保持应用启动时间的快速
  • 使我们能够结合使用page-router-outletrouter-outlet进行主/详细导航以及clearHistory交换页面导航
  • 将路由配置责任隔离到它所关注的模块,这些模块可以随时间扩展
  • 如果我们决定更改用户看到的初始页面,我们可以在将来轻松地针对不同的起始页面

这使用NativeScriptRoutingModule.forRoot(routes),因为这应该被视为我们应用路由配置的根。

我们还将输出NativeScriptRoutingModule,因为我们将在稍后将此AppRoutingModule导入我们的根AppModule。这使得路由指令可用于根模块的根组件。

为 NgModuleFactoryLoader 提供 NSModuleFactoryLoader

默认情况下,Angular 的内置模块加载器使用 SystemJS;但是,NativeScript 提供了一个名为NSModuleFactoryLoader的增强模块加载器。让我们在主路由模块中提供它,以确保所有模块都加载了它,而不是 Angular 的默认模块加载程序。

app/app.routing.ts进行以下修改:

import { NgModule, NgModuleFactoryLoader } from '@angular/core';
import { NativeScriptRouterModule, NSModuleFactoryLoader } from 'nativescript-angular/router';

const routes: Routes = [
  {
    path: '',
    redirectTo: '/mixer/home',
    pathMatch: 'full'
  },
  {
    path: 'mixer',
    loadChildren: './modules/mixer/mixer.module#MixerModule'
  },
  {
    path: 'record',
    loadChildren: './modules/recorder/recorder.module#RecorderModule',
    canLoad: [AuthGuard]
  }
];

@NgModule({
  imports: [
    NativeScriptRouterModule.forRoot(routes)
  ],
  providers: [
    AuthGuard,
    {
 provide: NgModuleFactoryLoader,
 useClass: NSModuleFactoryLoader
 }
  ],
  exports: [
    NativeScriptRouterModule
  ]
})
export class AppRoutingModule { }

现在,我们可以通过loadChildren指定默认的NgModuleFactoryLoader来使用标准的角度延迟加载语法,但应该使用 NativeScript 的增强型NSModuleFactoryLoader。我们不会详细讨论NSModuleFactoryLoader提供的内容,因为这里对进行了很好的解释 https://www.nativescript.org/blog/optimizing-app-loading-time-with-angular-2-lazy-loading ,我们想在本书中介绍更多内容。

杰出的有了这些升级,我们就可以离开维修车间,继续沿着公路前行。让我们继续实施新的路由设置。

打开app/app.component.html;将其内容剪切到剪贴板并替换为以下内容:

<page-router-outlet></page-router-outlet>

这将是我们视图级实现的基础。page-router-outlet允许任何组件在其位置插入自身,无论是单个平面布线还是具有自身子视图的布线。它还允许将其他组件视图推送到移动导航堆栈上,从而允许主/详细移动导航和历史记录。

为了使这个page-router-outlet指令生效,我们需要根目录AppModule来导入新的AppRoutingModule。我们也将借此机会删除之前在这里导入的PlayerModule。打开app/app.module.ts并进行以下修改:

// angular
import { NgModule } from '@angular/core';

// app
import { CoreModule } from './modules/core/core.module';
import { AppRoutingModule } from './app.routing';
import { AppComponent } from './app.component';

@NgModule({
 imports: [
   CoreModule,
   AppRoutingModule
 ],
 declarations: [AppComponent],
 bootstrap: [AppComponent]
})
export class AppModule { }

创建 MixerModule

这个模块实际上不会是什么新东西,因为它将重新定位以前根组件的视图。然而,它将引入一个额外的细节:定义自己内部路径的能力。

创建app/modules/mixer/components/mixer.component.html并粘贴app.component.html中我们剪切的内容:

<ActionBar title="TNSStudio" class="action-bar"></ActionBar><GridLayout rows="*, 100" columns="*" class="page">  
  <track-list row="0" col="0"></track-list>  
  <player-controls row="1" col="0"></player-controls></GridLayout>

然后创建一个匹配的app/modules/mixer/components/mixer.component.ts

import { Component } from '@angular/core';

@Component({ 
  moduleId: module.id, 
  selector: 'mixer', 
  templateUrl: 'mixer.component.html'
})
export class MixerComponent {}

现在,我们将创建BaseComponent,它将不仅作为前面的MixerComponent的占位符,而且作为我们可能希望在其位置显示的任何其他子视图组件的占位符。例如,我们的混音器可能希望允许用户弹出混音器中的单个曲目,并进入一个孤立的视图,以处理音频效果。

使用以下内容创建app/modules/mixer/components/base.component.ts

// angular
import { Component } from '@angular/core';

@Component({
 moduleId: module.id,
 selector: 'mixer-base',
 template: `<router-outlet></router-outlet>`
})
export class BaseComponent { }

这提供了一个插槽来插入我们混音器配置的任何子路由,其中一个是MixerComponent本身。由于视图只是一个简单的router-outlet,因此实际上不需要创建单独的templateUrl,因此我们将其内联在这里。

现在我们准备实施MixerModule;使用以下内容创建app/modules/mixer/mixer.module.ts

import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { NativeScriptRouterModule } from 
  'nativescript-angular/router';
import { Routes } from '@angular/router';

import { PlayerModule } from '../player/player.module';
import { BaseComponent } from './components/base.component';
import { MixerComponent } from 
  './components/mixer.component';

const COMPONENTS: any[] = [
  BaseComponent,
  MixerComponent
]

const routes: Routes = [
  {
    path: '',
    component: BaseComponent,
    children: [
      {
        path: 'home',
        component: MixerComponent
      }
    ]
  }
];

@NgModule({
  imports: [
    PlayerModule,
    NativeScriptRouterModule.forChild(routes)
  ],
  declarations: [
    ...COMPONENTS
  ],
  schemas: [
    NO_ERRORS_SCHEMA
  ]
})
export class MixerModule { }

我们已经导入了PlayerModule,因为混音器使用了其中定义的组件/小部件(即track-listplayer-controls。我们还利用NativeScriptRouterModule.forChild(routes)方法指出这些是特定的子路线。我们的路由配置在根' '路径上设置 BaseComponent,它将'home'定义为MixerComponent。如果您还记得,我们的应用的AppRoutingModule配置了我们应用的根路径,如下所示:

...
{
  path: '',
  redirectTo: '/mixer/home',
  pathMatch: 'full'
},
...

这将直接路由到此处的MixerComponent,定义为'home'。如果需要的话,我们可以将起始页指向混合器的不同子视图,从而轻松地指向不同的视图。由于BaseComponent只是一个router-outlet,因此在我们的混音器路由根' '下定义的任何子项(我们的整个应用路由视为'/mixer')都将直接插入该视图槽中。如果您现在就运行这个,您应该会看到与我们之前相同的起始页。

恭喜!你的应用的启动时间现在很快,你已经惰性地加载了你的第一个模块!

然而,有两件事值得注意:

  • 在开始页面出现之前,您可能会注意到一个快速的白光闪烁(至少在 iOS 上)
  • 您可能会注意到控制台日志打印了两次Current user:

我们将分别讨论这些问题。

  1. 在显示起始页之前,在启动屏幕后移除白色闪光灯。

这是正常的,是默认页面背景颜色为白色的结果。要提供无缝的发布体验,请打开app/common.css文件并删除此全局Page类定义,以便将背景色着色为与我们的ActionBar背景色相同的颜色:

Page {
  background-color:#101B2E;
}

现在,将不再有白色闪光灯,应用的发布将看起来天衣无缝。

  1. 控制台日志打印Current user:两次

Angular 的依赖项注入器由于延迟加载而导致此问题。

这来自app/modules/core/services/auth.service.ts,我们在app/modules/core/services/auth.service.ts中有一个私有init方法,该方法是从服务的构造函数调用的:

...
@Injectable()
export class AuthService {
   ...
   constructor(
     private databaseService: DatabaseService,
     private logService: LogService
   ) {
     this._init();
   } 
  ...
  private _init() {
    AuthService.CURRENT_USER = this.databaseService.getItem(
      DatabaseService.KEYS.currentUser);
    this.logService.debug(`Current user: `,
 AuthService.CURRENT_USER);
    this._notifyState(!!AuthService.CURRENT_USER);
  }
  ...
}

等待什么这是否意味着AuthService将建造两次??!!

对是的(

我能听到车轮的吱吱声,当你驶离这条公路,驶入一条沟渠时

这无疑是一个巨大的问题,因为我们绝对希望AuthService成为一个全球共享的单例,可以在任何地方注入并共享,以提供我们应用的当前认证状态。

我们现在必须解决这个问题,但在研究固溶体之前,让我们先绕一小段路,了解为什么会发生这种情况。

了解 Angular 在延迟加载模块时的依赖关系注入器

我们将直接从 Angular 的官方文件(https://angular.io/guide/ngmodule-faq#!#q-why-child-injector中进行解释,而不是重述细节,这完美地解释了这一点:

Angular adds @NgModule.providers to the application root injector unless the module is lazy loaded. For a lazy-loaded module, Angular creates a child injector and adds the module's providers to the child injector.

This means that a module behaves differently depending on whether it's loaded during application start or lazily loaded later. Neglecting that difference can lead to adverse consequences.

Why doesn't Angular add lazy-loaded providers to the app root injector as it does for eagerly loaded modules?

The answer is grounded in a fundamental characteristic of the Angular dependency-injection system. An injector can add providers until it's first used. Once an injector starts creating and delivering services, its provider list is frozen; no new providers are allowed.

When an application starts, Angular first configures the root injector with the providers of all eagerly loaded modules before creating its first component and injecting any of the provided services. Once the application begins, the app root injector is closed to new providers.

Time passes and application logic triggers lazy loading of a module. Angular must add the lazy-loaded module's providers to an injector somewhere. It can't add them to the app root injector because that injector is closed to new providers. So Angular creates a new child injector for the lazy-loaded module context.

如果我们看我们的根AppModule,我们可以看到它导入了CoreModule,它提供了AuthService

...
@NgModule({
  imports: [
    CoreModule,
    AppRoutingModule
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
  schemas: [NO_ERRORS_SCHEMA]
})
export class AppModule { }

如果我们再看一下PlayerModule,我们可以看到它也导入了CoreModule,因为PlayerModule的组件使用了它声明的OrderByPipe以及它提供的几个服务(即AuthServiceLogServiceDialogService

...
@NgModule({
  imports: [
    CoreModule
  ],
  providers: [...PROVIDERS],
  declarations: [...COMPONENTS],
  exports: [...COMPONENTS],
  schemas: [ NO_ERRORS_SCHEMA ]
})
export class PlayerModule { }

由于我们新颖的路由配置,PlayerModule现在与MixerModule一起延迟加载。这导致 Angular 的 dependency injector 为我们的延迟加载的MixerModule注册了一个新的子注入器,它带来了PlayerModule,它还带来了CoreModule的导入,它定义了这些提供者,包括AuthServiceLogService等等。当 Angular 注册MixerModule时,它将注册整个新模块中定义的所有提供者,包括新子注入器中导入的模块,从而产生正在构建的这些服务的新实例。

Angular 的文档还为模块提供了一个建议设置,以纠正这种情况,因此让我们再次解释一下https://angular.io/guide/ngmodule-faq#!#q-module-recommendations

SharedModule Create a SharedModule with the components, directives, and pipes that you use everywhere in your app. This module should consist entirely of declarations, most of them exported. The SharedModule may re-export other widget modules, such as CommonModule, FormsModule, and modules with the UI controls that you use most widely.The SharedModule should not have providers for reasons explained previously. Nor should any of its imported or re-exported modules have providers. If you deviate from this guideline, know what you're doing and why. Import the SharedModule in your feature modules, both those loaded when the app starts and those you lazily load later. Create a CoreModule with providers for the singleton services you load when the application starts. Import CoreModule in the root AppModule only. Never import CoreModule in any other module. Consider making CoreModule a pure service module with no declarations.

好的,哇!这是一个很好的建议。特别值得注意的是最后一行:

Consider making CoreModule a pure service module with no declarations.

所以,我们已经有了CoreModule,这是一个好消息,但我们希望它成为一个纯服务模块,没有声明。我们还将仅在根 AppModule 中导入 CoreModule。切勿在任何其他模块中导入 CoreModel。然后,我们可以创建一个新的SharedModule来提供而已。。。**我们在【我们的】应用中随处使用的组件、指令和管道。

我们创建app/modules/shared/shared.module.ts,如下所示:

// nativescript
import { NativeScriptModule } from 'nativescript-angular/nativescript.module'; 

// angular
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';

// app
import { PIPES } from './pipes';

@NgModule({
  imports: [
    NativeScriptModule
  ],
  declarations: [
    ...PIPES
  ],
  exports: [
    NativeScriptModule,
    ...PIPES
  ],
  schemas: [ NO_ERRORS_SCHEMA ]
})
export class SharedModule {}

对于PIPES,我们只是将管道目录从app/modules/core移动到app/modules/shared文件夹。现在,SharedModule是一个我们可以自由导入多个不同模块的模块,这些模块需要它可能提供的任何管道或未来共享组件/指令。它不会定义本建议中提到的任何服务提供商:

SharedModule should not have providers for reasons explained previously, nor should any of its imported or re-exported modules have providers.

然后我们可以将CoreModule(位于app/modules/core/core.module.ts中)调整为无声明的纯服务模块:

// nativescript
import { NativeScriptModule } from 'nativescript-angular/nativescript.module'; 
import { NativeScriptFormsModule } from 'nativescript-angular/forms'; 
import {NativeScriptHttpModule } from 'nativescript-angular/http';
// angular
import { NgModule, Optional, SkipSelf } from '@angular/core';

// app
import { PROVIDERS } from './services';

const MODULES: any[] = [
  NativeScriptModule,
  NativeScriptFormsModule,
  NativeScriptHttpModule
];

@NgModule({
  imports: [
    ...MODULES
  ],
  providers: [
    ...PROVIDERS
  ],
  exports: [
    ...MODULES
  ]
})
export class CoreModule {
  constructor (
    @Optional() @SkipSelf() parentModule: CoreModule) {
    if (parentModule) {
      throw new Error(
        'CoreModule is already loaded. Import it in the AppModule only');
    }
  }
}

这个模块现在只将提供者定义为包含AuthServiceDatabaseServiceDialogServiceLogService的集合,所有这些都是我们在本书前面创建的,我们希望确保它们是我们应用中使用的真正的单例,无论它们是否用于延迟加载的模块。

Why do we use the ...PROVIDERS spread notation instead of just assigning the collection directly? For scalability reasons. In the future, if we need to add an additional provider or override a provider, we can do so simply by just adding to the collection right in the module. The same goes for imports and exports.

我们还借此机会导入了一些额外的模块,以确保这些模块在整个应用中也在全球范围内使用。NativeScriptModuleNativeScriptFormsModuleNativeScriptHttpModule都是重要的模块,它们可以覆盖 Angular 的各种供应商提供的某些 web API,从而使用本机 API 增强我们的应用。例如,该应用将使用 iOS 和 Android 上提供的本机 HTTP API,而不是使用XMLHttpRequest(这是一种 web API),以实现最终的网络性能。我们确保我们也导出它们,这样我们的根模块就不再需要导入它们,而只需导入这个CoreModule

最后,我们定义了一个构造函数,它将帮助我们在将来避免意外地将这个CoreModule导入到其他延迟加载的模块中。

我们还不知道PlayerModule提供的PlayerService是否会被RecorderModule需要,它也会被延迟加载。如果将来出现这种情况,我们还可以将PlayerService重构为CoreModule,以确保它是在整个应用中共享的真正的单例。现在,我们将把它作为PlayerModule的一部分放在原处。

现在,让我们根据我们所做的工作,对其他模块进行最后的调整。

app/modules/player/player.module.ts文件现在应该如下所示:

// angular
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';

// app
import { SharedModule } from '../shared/shared.module';
import { COMPONENTS } from './components';
import { PROVIDERS } from './services';

@NgModule({
  imports: [ SharedModule ],
  providers: [ ...PROVIDERS ],
  declarations: [ ...COMPONENTS ],
  exports: [
    SharedModule,
    ...COMPONENTS
  ],
  schemas: [ NO_ERRORS_SCHEMA ]
})
export class PlayerModule { }

app/modules/recorder/recorder.module.ts文件现在应该如下所示:

// angular
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';

// app
import { SharedModule } from '../shared/shared.module';
import { PROVIDERS } from './services';

@NgModule({
 imports: [ SharedModule ],
 providers: [ ...PROVIDERS ],
 schemas: [ NO_ERRORS_SCHEMA ]
})
export class RecorderModule { }

请注意,我们现在导入的是SharedModule而不是CoreModule。这使我们能够通过导入SharedModule,在整个应用中共享指令、组件和管道(基本上是模块声明部分中的任何内容)。

我们在app/app.module.ts的根AppModule保持不变:

// angular
import { NgModule } from '@angular/core';

// app
import { CoreModule } from './modules/core/core.module';
import { AppRoutingModule } from './app.routing';
import { AppComponent } from './app.component';

@NgModule({
  imports: [
    CoreModule,
    AppRoutingModule
  ],
  declarations: [ AppComponent ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

任何模块(无论是否延迟加载)仍然可以注入CoreModule提供的任何服务,因为根AppModule现在导入了CoreModule。这允许 Angular 的根注入器构造CoreModule所提供的服务一次。然后,每当这些服务被注入到任何地方(在延迟加载模块中或不在中),Angular 将首先向父注入器(在延迟加载模块的情况下,它将是子注入器)请求该服务,如果在那里找不到,它将请求下一个父注入器到达根注入器,最终,提供单身人士的地方。

我们在这个沙漠小镇度过了一段美妙的时光。让我们沿着高速公路巡游到超级安全的 51 区,在那里,除非得到适当的授权,否则模块可能会被锁定数年。

为 RecorderModule 创建 AuthGuard

我们的应用的一个要求是,在用户通过身份验证之前,录音功能应该被锁定并且不可访问。这为我们提供了一个拥有用户基础的能力,如果我们愿意,还可以在未来引入付费功能。

Angular 提供了在我们的路线上插入防护装置的能力,这只能在特定条件下激活。这正是我们实现此功能需求所需要的,因为我们已经隔离了'/record'路由以延迟加载RecorderModule,该路由将包含所有录制功能。我们只希望在用户经过身份验证的情况下才允许访问该'/record'路由。

让我们在一个新文件夹中创建app/guards/auth-guard.service.ts以实现可伸缩性,因为我们可以根据需要在此处扩展并创建其他防护:

import { Injectable } from '@angular/core';
import { Route, CanActivate, CanLoad } from '@angular/router';
import { AuthService } from '../modules/core/services/auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanLoad {

  constructor(private authService: AuthService) { }

  canActivate(): Promise<boolean> {
    return new Promise((resolve, reject) => {
      if (this._isAuth()) {
        resolve(true);
      } else {
        // login sequence to continue prompting
        let promptSequence = (usernameAttempt?: string) => {
          this.authService.promptLogin(
            'Authenticate to record.',
            usernameAttempt
          ).then(() => {
            resolve(true); 
          }, (usernameAttempt) => {
            if (usernameAttempt === false) {
              // user canceled prompt
              resolve(false);
            } else {
              // initiate sequence again
              promptSequence(usernameAttempt);
            }
          });
        };
        // start login prompt sequence
        // require auth before activating
        promptSequence();
      }
    });
  }

  canLoad(route: Route): Promise<boolean> {
    // reuse same logic to activate
    return this.canActivate();
  }

  private _isAuth(): boolean {
    // just get the latest value from our BehaviorSubject
    return this.authService.authenticated$.getValue();
  }
}

我们可以利用AuthServiceBehaviorSubject来获取最新的值,使用this.authService.authenticated$.getValue()来确定身份验证状态。如果用户通过canLoad验证,则立即通过canActivate激活该模块。否则,我们将通过服务的方法显示登录提示,但这次我们将其包装为一个重新提示序列,该序列将继续提示失败的尝试,直到成功验证,或者在用户取消提示时忽略它。

For the book, we aren't wiring up to any backend service to do any real authentication with a service provider. We will leave that part up to you in your own app. We will just be persisting the e-mail and password you enter into the login prompt as a valid user after doing very simple validation on the input.

请注意,AuthGuard与其他服务一样,是一个可注入服务,因此我们希望确保将其添加到AppRoutingModule的提供者元数据中。我们现在可以通过对app/app.routing.ts进行以下突出显示的修改来保护我们的路线:

...
import { AuthGuard } from './guards/auth-guard.service';

const routes: Routes = [
  ...
  {
    path: 'record',
    loadChildren: 
      './modules/recorder/recorder.module#RecorderModule',
    canLoad: [AuthGuard]
  }
];

@NgModule({
  ...
  providers: [
    AuthGuard,
    ...
  ],
  ...
})
export class AppRoutingModule { }

为了尝试这一点,我们需要在RecorderModule中添加子路由,因为我们还没有这样做。打开app/modules/recorder/recorder.module.ts并添加以下突出显示的部分:

// nativescript
import { NativeScriptModule } from 'nativescript-angular/nativescript.module';
import { NativeScriptRouterModule } from 'nativescript-angular/router';

// angular
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { Routes } from '@angular/router';

// app
import { SharedModule } from '../shared/shared.module';
import { PROVIDERS } from './services';
import { RecordComponent } from './components/record.component';

const COMPONENTS: any[] = [
 RecordComponent
]

const routes: Routes = [
 {
 path: '',
 component: RecordComponent
 }
];

@NgModule({
  imports: [
    SharedModule,
    NativeScriptRouterModule.forChild(routes)
  ],
  declarations: [ ...COMPONENTS ],
  providers: [ ...PROVIDERS ],
  schemas: [ NO_ERRORS_SCHEMA ]
})
export class RecorderModule { }

我们现在有一个正确的子路由配置,当用户导航到'/record'路径时,它将显示单个RecordComponent。我们将不显示RecordComponent的详细信息,因为您可以参考回购协议上的第 5 章路由和延迟加载分支。然而,在app/modules/recorder/components/record.component.html中的这一点上,它只是一个断头的组件,只是显示了一个简单的标签,所以我们可以尝试一下。

最后,我们需要一个按钮,它将路由到我们的'/record'路径。如果我们回顾一下我们的原始草图,我们希望在ActionBar的右上角显示一个记录按钮,现在让我们实现它。

打开app/modules/mixer/components/mixer.component.html并添加以下内容:

<ActionBar title="TNSStudio" class="action-bar">
  <ActionItem nsRouterLink="/record" ios.position="right">
 <Button text="Record" class="action-item"></Button>
 </ActionItem>
</ActionBar>
<GridLayout rows="*, 100" columns="*" class="page">
  <track-list row="0" col="0"></track-list>
  <player-controls row="1" col="0"></player-controls>
</GridLayout>

现在,如果我们在 iOS 模拟器中运行它,我们会注意到ActionBar中的录制按钮没有任何作用!这是因为MixerModule只导入以下内容:

@NgModule({
  imports: [
    PlayerModule,
    NativeScriptRouterModule.forChild(routes)
  ],
  ...
})
export class MixerModule { }

NativeScriptRouterModule.forChild(routes)方法仅配置路由,但不使各种路由指令(如nsRouterLink)可用于我们的组件。

因为您在前面了解到,SharedModule应该用于声明您希望在整个模块中共享的各种指令、组件和管道(无论是否延迟加载),所以这是一个利用这一点的绝佳机会。

打开app/modules/shared/shared.module.ts并进行以下突出显示的修改:

...
import { NativeScriptRouterModule } from 'nativescript-angular/router'; 
...

@NgModule({
  imports: [
    NativeScriptModule, 
    NativeScriptRouterModule
  ],
  declarations: [
    ...PIPES
  ],
  exports: [
    NativeScriptModule,
    NativeScriptRouterModule,
    ...PIPES
  ],
  schemas: [NO_ERRORS_SCHEMA]
})
export class SharedModule { }

现在回到MixerModule中,我们可以调整进口使用SharedModule

...
import { SharedModule } from '../shared/shared.module'; 
@NgModule({
  imports: [
    PlayerModule,
    SharedModule,
    NativeScriptRouterModule.forChild(routes)
  ],
  ...
})
export class MixerModule { }

这确保通过NativeScriptRouterModule公开的所有指令现在都包括在内,并可通过利用我们的应用范围SharedModuleMixerModule中使用。

再次运行我们的应用,当我们点击ActionBar中的记录按钮时,我们会看到登录提示。如果我们输入格式正确的电子邮件地址和密码,它将保留详细信息,登录我们,并在 iOS 上显示RecordComponent,如下所示:

你可能会注意到一些相当有趣的事情。ActionBar更改了我们通过 CSS 指定的背景颜色,按钮颜色现在显示默认的蓝色。这是因为RecordComponent没有定义ActionBar;因此,它将恢复为默认样式ActionBar,并带有默认的后退按钮,该按钮将显示刚刚导航的页面的标题。'/record'路线还利用page-router-outlet的功能将组件推到移动导航堆栈上。RecordComponent在视图中设置动画,同时允许用户选择左上角按钮向后导航(将导航历史弹出一个)。

为了修复ActionBar,我们只需在RecordComponent视图中添加ActionBar和一个自定义NavigationButton(一个NativeScript视图组件,模拟移动设备的默认后退导航按钮)。我们可以对app/modules/record/components/record.component.html进行调整:

<ActionBar title="Record" class="action-bar">
  <NavigationButton text="Back"
    android.systemIcon="ic_menu_back">
  </NavigationButton>
</ActionBar>
<StackLayout class="p-20">
  <Label text="TODO: Record" class="h1 text-center"></Label>
</StackLayout>

现在,这看起来好多了:

如果我们在 Android 上运行此操作,并使用任何电子邮件/密码组合登录以持久化用户,它将显示相同的RecordComponent视图;但是,您会注意到另一个有趣的细节。我们已经将 Android 设置为显示一个标准的后向箭头系统图标NavigationButton,但当点击该箭头时,它什么也不做。Android 的默认行为依赖于设备主按钮旁边的物理硬件后退按钮。然而,我们可以通过在NavigationButton中添加点击事件来提供一致的体验,因此 iOS 和 Android 对点击后退按钮的反应是相同的。对模板进行以下修改:

<ActionBar title="Record" icon="" class="action-bar">
  <NavigationButton (tap)="back()" text="Back" 
    android.systemIcon="ic_menu_back">
  </NavigationButton>
</ActionBar>
<StackLayout class="p-20">
  <Label text="TODO: Record" class="h1 text-center"></Label>
</StackLayout>

然后,我们可以在 Angular 的RouterExtensions服务中使用NativeScript实现app/modules/recorder/components/record.component.ts中的back()方法:

// angular
import { Component } from '@angular/core';
import { RouterExtensions } from 'nativescript-angular/router';

@Component({
 moduleId: module.id,
 selector: 'record',
 templateUrl: 'record.component.html'
})
export class RecordComponent { 

  constructor(private router: RouterExtensions) { }

  public back() {
    this.router.back();
  }
}

现在,除了硬件后退按钮外,还可以点击 Android 的后退按钮进行导航。iOS 只是忽略 tap 事件处理程序,因为它使用NavigationButton的默认本机行为。很好。以下是RecordComponent在 Android 上的外观:

在接下来的章节中,我们将实现一个很好的录制视图。

我们现在肯定在 66 号公路上巡航!

我们已经实现了延迟加载的路由,提供了AuthGuard来保护未经授权使用我们应用的录制功能,并在这个过程中学到了很多。然而,我们刚刚意识到我们在游戏后期缺少了一个非常重要的功能。随着时间的推移,我们需要一种方法来处理几种不同的混音。默认情况下,我们的应用可能会推出最后一次打开的混合,但我们想创建新的混合(让我们考虑他们的 T3 T3 组成作文,T4),并记录全新的混合作为单独的组成单独轨道。我们需要一个新的路径来显示这些成分,我们可以适当地命名,这样我们就可以在不同的材料上来回跳跃。

处理后期功能需求–管理合成

是时候处理 66 号公路上的意外交通了。我们遇到了后期功能需求,意识到我们需要一种方法来管理任意数量的不同混音,以便随着时间的推移可以处理不同的材料。我们可以将每个混音称为音频曲目的组合。

好消息是,我们已经花了相当多的时间设计一个可伸缩的体系结构,我们即将收获我们的劳动成果。对后期功能需求的响应现在变成了一个相当愉快的周日在附近闲逛。让我们花点时间来开发这个新功能,展示一下我们应用架构的优势。

让我们首先为我们将创建的新MixListComponent定义一条新路线。打开app/modules/mixer/mixer.module.ts并进行以下突出显示的修改:

...
import { MixListComponent } from './components/mix-list.component';
import { PROVIDERS } from './services';

const COMPONENTS: any[] = [
  BaseComponent,
  MixerComponent,
  MixListComponent
]

const routes: Routes = [
  {
    path: '',
    component: BaseComponent,
    children: [
      {
 path: 'home',
 component: MixListComponent
 },
 {
 path: ':id',
 component: MixerComponent
 }
    ]
  }
];

@NgModule({
   ...
   providers: [
 ...PROVIDERS
 ]
})
export class MixerModule { }

我们正在改变最初的策略,将MixerComponent作为首页,但我们将在一瞬间创建一个新的MixListComponent来表示'home'首页,这将是我们正在处理的所有作文的列表。为了方便以后使用,我们仍然可以在应用启动时使用MixListComponent自动选择最后选择的合成。我们现在将MixerComponent定义为一个参数化路由,因为它将始终代表由':id'参数路由标识的一个工作组件,例如,它将解析为类似'/mixer/1'的路由。我们还引入了PROVIDERS,我们将在稍后创建。

让我们修改CoreModule提供的DatabaseService,以帮助为我们的新数据需求提供一个恒定的持久性密钥。我们希望通过这个常量键名保存用户创建的组合。打开app/modules/core/services/database.service.ts并进行以下突出显示的修改:

...
interface IKeys {
  currentUser: string;
  compositions: string;
}

@Injectable()
export class DatabaseService {

  public static KEYS: IKeys = {
    currentUser: 'current-user',
    compositions: 'compositions'
  };
...

让我们也创建一个新的数据模型来表示我们的组成。创建app/modules/shared/models/composition.model.ts

import { ITrack } from './track.model';

export interface IComposition {
  id: number;
  name: string;
  created: number;
  tracks: Array<ITrack>;
  order: number;
}
export class CompositionModel implements IComposition {
  public id: number;
  public name: string;
  public created: number;
  public tracks: Array<ITrack> = [];
  public order: number;

  constructor(model?: any) {
    if (model) {
      for (let key in model) {
        this[key] = model[key];
      }
    }
    if (!this.created) this.created = Date.now();
    // if not assigned, just assign a random id
    if (!this.id)
      this.id = Math.floor(Math.random() * 100000);
  }
}

然后,坚持我们的惯例,开放app/modules/shared/models/index.ts并重新出口这种新模式:

export * from './composition.model';
export * from './track.model';

现在,我们可以在一个新的数据服务中使用这个新模型和数据库密钥来构建这个新特性。创建app/modules/mixer/services/mixer.service.ts

// angular
import { Injectable } from '@angular/core';

// app
import { ITrack, IComposition, CompositionModel } from '../../shared/models';
import { DatabaseService } from '../../core/services/database.service';
import { DialogService } from '../../core/services/dialog.service';

@Injectable()
export class MixerService {

  public list: Array<IComposition>;

  constructor(
    private databaseService: DatabaseService,
    private dialogService: DialogService
  ) {
    // restore with saved compositions or demo list
    this.list = this._savedCompositions() || 
      this._demoComposition();
  } 

  public add() {
    this.dialogService.prompt('Composition name:')
      .then((value) => {
        if (value.result) {
          let composition = new CompositionModel({
            id: this.list.length + 1,
            name: value.text,
            order: this.list.length // next one in line
          });
          this.list.push(composition);
          // persist changes
          this._saveList();
        }
      });
  }

  public edit(composition: IComposition) {
    this.dialogService.prompt('Edit name:', composition.name)
      .then((value) => {
        if (value.result) {
          for (let comp of this.list) {
            if (comp.id === composition.id) {
              comp.name = value.text;
              break;
            }
          }
          // re-assignment triggers view binding change
          // only needed with default change detection
          // when object prop changes in collection
          // NOTE: we will use Observables in ngrx chapter
          this.list = [...this.list];
          // persist changes
          this._saveList();
        }
      });
  }

  private _savedCompositions(): any {
    return this.databaseService
      .getItem(DatabaseService.KEYS.compositions);
  }

  private _saveList() {
    this.databaseService
      .setItem(DatabaseService.KEYS.compositions, this.list);
  }

  private _demoComposition(): Array<IComposition> {
    // Starter composition to demo on first launch
    return [
      {
        id: 1,
        name: 'Demo',
        created: Date.now(),
        order: 0,
        tracks: [
          {
            id: 1,
            name: 'Guitar',
            order: 0
          },
          {
            id: 2,
            name: 'Vocals',
            order: 1
          }
        ]
      }
    ]
  }
}

我们现在有了一个服务,它将提供一个列表来绑定视图以显示用户保存的合成。它还提供了一种添加和编辑合成的方法,并为第一次应用发布添加演示合成,以获得良好的首次用户体验(我们将在稍后的演示中添加实际曲目)。

根据我们的惯例,我们还添加了app/modules/mixer/services/index.ts,如下所示,我们刚才在MixerModule中说明了导入的内容:

import { MixerService } from './mixer.service';

export const PROVIDERS: any[] = [
  MixerService
];

export * from './mixer.service';

现在让我们创建app/modules/mixer/components/mix-list.component.ts来使用和规划我们的新数据服务:

// angular
import { Component } from '@angular/core';

// app
import { MixerService } from '../services/mixer.service';

@Component({
  moduleId: module.id,
  selector: 'mix-list',
  templateUrl: 'mix-list.component.html'
})
export class MixListComponent {

  constructor(public mixerService: MixerService) { } 
}

对于视图模板,app/modules/mixer/components/mix-list.component.html

<ActionBar title="Compositions" class="action-bar">
  <ActionItem (tap)="mixerService.add()" 
    ios.position="right">
    <Button text="New" class="action-item"></Button>
  </ActionItem>
</ActionBar>
<ListView [items]="mixerService.list | orderBy: 'order'" 
  class="list-group">
  <ng-template let-composition="item">
    <GridLayout rows="auto" columns="100,*,auto" 
      class="list-group-item">
      <Button text="Edit" row="0" col="0" 
        (tap)="mixerService.edit(composition)"></Button>
      <Label [text]="composition.name"
        [nsRouterLink]="['/mixer', composition.id]"
        class="h2" row="0" col="1"></Label>
      <Label [text]="composition.tracks.length" 
        class="text-right" row="0" col="2"></Label>
    </GridLayout>
  </ng-template>
</ListView>

这将把我们的MixerService用户保存的作品列表呈现到视图中,当我们第一次启动应用时,它将被植入一个样本演示作品,预装了两段录音,因此用户可以四处播放。以下是 iOS 首次发布时的情况:

我们可以创建新的组合,并编辑现有组合的名称。我们也可以点击作文名称查看MixerComponent;但是,我们需要调整组件以获取路由':id'参数,并将其视图连接到所选的构图中。打开app/modules/mixer/components/mixer.component.ts并添加突出显示的部分:

// angular
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';

// app
import { MixerService } from '../services/mixer.service';
import { CompositionModel } from '../../shared/models';

@Component({
 moduleId: module.id,
 selector: 'mixer',
 templateUrl: 'mixer.component.html'
})
export class MixerComponent implements OnInit, OnDestroy {

  public composition: CompositionModel; 
 private _sub: Subscription;

 constructor(
 private route: ActivatedRoute,
 private mixerService: MixerService
 ) { } 

 ngOnInit() {
 this._sub = this.route.params.subscribe(params => {
 for (let comp of this.mixerService.list) {
 if (comp.id === +params['id']) {
 this.composition = comp;
 break;
 }
 }
 });
 } 

 ngOnDestroy() {
 this._sub.unsubscribe();
 }
}

我们可以注入 Angular 的ActivatedRoute来订阅路由的参数,这样我们就可以访问id。因为默认情况下它将以字符串的形式出现,所以当我们在服务列表中找到组合时,我们使用+params['id']将其转换为数字。我们为所选的composition分配一个本地引用,现在允许我们在视图中绑定到它。在这期间,我们还将在ActionBar中添加一个标记为List的按钮,以导航回我们的作文(稍后,我们将实现字体图标,以显示在它们的位置。打开app/modules/mixer/components/mixer.component.html并进行以下突出显示的修改:

<ActionBar [title]="composition.name" class="action-bar">
  <ActionItem nsRouterLink="/mixer/home">
 <Button text="List" class="action-item"></Button>
 </ActionItem>
  <ActionItem nsRouterLink="/record" ios.position="right">
    <Button text="Record" class="action-item"></Button>
  </ActionItem>
</ActionBar>
<GridLayout rows="*, 100" columns="*" class="page">
  <track-list [tracks]="composition.tracks" row="0" col="0"></track-list>
  <player-controls row="1" col="0"></player-controls>
</GridLayout>

这允许我们在标题ActionBar中显示所选作品的名称,并将其曲目传递给track-list。我们需要将Input添加到track-list,这样它就可以呈现构图的轨迹,而不是它现在绑定到的虚拟数据。让我们打开app/modules/player/components/track-list/track-list.component.ts并添加一个Input

...
export class TrackListComponent {

 @Input() tracks: Array<ITrack>;

 ...
}

之前,TrackListComponent视图绑定到playerService.tracks,所以我们调整app/modules/player/components/track-list/track-list.component.html组件的视图模板,绑定到我们新的Input,现在将代表用户实际选择的构图**:**中的曲目

<ListView [items]="tracks | orderBy: 'order'" class="list-group">
  <template let-track="item">
    <GridLayout rows="auto" columns="100,*,100" class="list-group-item">
      <Button text="Record" (tap)="record(track)" row="0" col="0" class="c-ruby"></Button>
      <Label [text]="track.name" row="0" col="1" class="h2"></Label>
      <Switch [checked]="track.solo" row="0" col="2" class="switch"></Switch>
    </GridLayout>
  </template>
</ListView>

现在,我们的应用中有以下序列,以满足这一最新功能需求,我们在这里仅用了几页材料:

它在 Android 上的工作原理完全相同,同时保留其独特的本地特性。

然而,您可能会注意到,Android 上的ActionBar默认为右侧的所有ActionItem。我们想快速向您展示的最后一个技巧是平台特定视图模板的功能。哦,别担心那些丑陋的安卓按钮;稍后我们将为这些应用集成字体图标。

Create platform-specific view templates wherever you see fit. Doing so will help you dial views for each platform where necessary and make them highly maintainable.

让我们创建app/modules/mixer/components/action-bar/action-bar.component.ts

// angular
import { Component, Input } from '@angular/core';

@Component({
  moduleId: module.id,
  selector: 'action-bar',
  templateUrl: 'action-bar.component.html'
})
export class ActionBarComponent {

  @Input() title: string;
}

然后,您可以创建特定于 iOS 的视图模板:app/modules/mixer/components/action-bar/action-bar.component.ios.html

<ActionBar [title]="title" class="action-bar">
  <ActionItem nsRouterLink="/mixer/home">
    <Button text="List" class="action-item"></Button>
  </ActionItem>
  <ActionItem nsRouterLink="/record" ios.position="right">
    <Button text="Record" class="action-item"></Button>
  </ActionItem>
</ActionBar>

以及 Android 特定的视图模板:app/modules/mixer/components/action-bar/action-bar.component.android.html

<ActionBar class="action-bar">
  <GridLayout rows="auto" columns="auto,*,auto" class="action-bar">
    <Button text="List" nsRouterLink="/mixer/home" class="action-item" row="0" col="0"></Button>
    <Label [text]="title" class="action-bar-title text-center" row="0" col="1"></Label>
    <Button text="Record" nsRouterLink="/record" class="action-item" row="0" col="2"></Button>
  </GridLayout>
</ActionBar>

那么我们可以在app/modules/mixer/components/mixer.component.html中使用它:

<action-bar [title]="composition.name"></action-bar>
<GridLayout rows="*, 100" columns="*" class="page">
  <track-list [tracks]="composition.tracks" row="0" col="0"></track-list>
  <player-controls row="1" col="0"></player-controls>
</GridLayout>

只需确保将其添加到app/modules/mixer/mixer.module.tsMixerModuleCOMPONENTS

...
import { ActionBarComponent } from './components/action-bar/action-bar.component';
...

const COMPONENTS: any[] = [
  ActionBarComponent,
  BaseComponent,
  MixerComponent,
  MixListComponent
];
...

瞧!

总结

我们已经到达 66 号公路这段令人惊叹的旅程的终点,希望您能像我们一样感到兴奋。本章介绍了一些有趣的角度概念,包括延迟加载模块的路由配置,以保持应用启动时间的快速;使用本机文件处理 API 构建自定义模块加载器;将router-outlet的灵活性与 NativeScript 的page-router-outlet相结合;通过延迟加载模块获得对单例服务的控制和理解;依赖授权访问的防护路线;并致力于后期功能需求,展示我们出色的可扩展应用设计。

本章详细介绍了我们应用的一般可用性流程,现在,我们准备进入我们应用的核心竞争力:通过 iOS 和 Android 丰富的本机 API进行音频处理。

在深入研究大量内容之前,在下一章中,我们将花一点时间检查 NativeScript 的各种tns命令行参数,以运行我们的应用,锁定我们现在可以使用的工具带的全面教育。