8.7 发布——WinterCG 合规性第 1 部分
了解更多

本教程将介绍使用 Angular 构建 NativeScript 应用的基础知识,方法是指导您构建具有某些基本功能的示例应用。

本教程将教授您以下内容

  • 使用 NativeScript 组件构建布局
  • 使用手势向应用添加交互性
  • 使用内置的 Angular 指令创建和修改视图
  • 使用路由在不同的视图之间导航

先决条件

要充分利用本教程,你应已基本了解 Angular 框架。如果你完全不了解 Angular,可能需要在尝试本教程前先尝试官方 Angular 教程

示例应用程序概述

组件是 Angular 应用程序的基本构建模块。组件表示用户与之交互的页面和视图。NativeScript Angular 遵循相同理念,不同之处主要在于组件的 HTML 模板层及其样式。

你将构建一个主从应用程序,该应用程序显示音乐剧列表,允许你导航到详细信息页面以查看有关每部音乐剧的更多信息。

Example app preview

你可以在 GitHub 上找到应用程序的完整源代码

设置环境

要设置开发环境,请遵循文档中的 环境设置 部分的说明。

创建新的 NativeScript Angular 应用程序

要创建新的 NativeScript Angular 应用程序,请运行 CLI 命令 ns create 以及应用程序的名称后跟 --ng

cli
ns create example-app --ng

NativeScript CLI 将创建一个新的目录,根文件夹名为 example-app,其中包含一个初始骨架应用程序项目,并安装必要的包和依赖项。这可能需要几分钟,一旦安装完毕,就应该可以运行了。

运行应用程序

转到项目目录,并运行以下命令在其各自平台上运行

cli
cd example-app

// run on iOS
ns run ios

// run on Android
ns run android

ns run 命令构建应用程序,并在连接的 Android 设备或 Android 模拟器(适用于 Android)以及连接的 iOS 设备或 iOS 模拟器(适用于 iOS)上启动该应用程序。默认情况下,它侦听你代码中的更改,同步这些更改,并刷新所有选定的设备。

文件夹结构

基于 Angular 入门应用程序,我们将为我们的应用程序创建以下文件/文件夹结构。

src/app
  |- assets
    |- anastasia.png
    |- beetlejuicemusical.png
    |- bookofmormon.png
  |- core
    |- models
      |- flick.model.ts
    |- services
      |- flick.service.ts
  |- features
    |- home
      |- home.component.ts | html
      |- home.module.ts
      |- home-routing.module.ts
    |- details
      |- details.component.ts | html
      |- details.module.ts
      |- details-routing.module.ts
  ...

创建主页

我们从创建主页功能的文件开始,内容如下

xml
<!-- src/app/features/home/home.component.html -->
typescript
// src/app/features/home/home.component.ts

import { Component } from '@angular/core'

@Component({
  moduleId: module.id,
  selector: 'ns-home',
  templateUrl: 'home.component.html',
})
export class HomeComponent {}
typescript
// src/app/features/home/home-routing.module.ts

import { NgModule } from '@angular/core'
import { Routes } from '@angular/router'
import { NativeScriptRouterModule } from '@nativescript/angular'
import { HomeComponent } from './home.component'

export const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
  },
]

@NgModule({
  imports: [NativeScriptRouterModule.forChild(routes)],
})
export class HomeRoutingModule {}
typescript
// src/app/features/home/home.module.ts

import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core'
import { NativeScriptCommonModule } from '@nativescript/angular'
import { HomeRoutingModule } from './home-routing.module'
import { HomeComponent } from './home.component'

@NgModule({
  imports: [NativeScriptCommonModule, HomeRoutingModule],
  declarations: [HomeComponent],
  schemas: [NO_ERRORS_SCHEMA],
})
export class HomeModule {}

路由设置

我们将把我们的 HomeModule 设置为延迟加载模块和默认路由。打开 app-routing.module.ts 并添加以下代码

typescript
// src/app/app-routing.module.ts

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

const routes: Routes = [
  // Update this 👇
  { path: '', redirectTo: '/home', pathMatch: 'full' },

  // Add this 👇
  {
    path: 'home',
    loadChildren: () => import('./features/home/home.module').then(m => m.HomeModule)
  }
]

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

备注

默认情况下,NgModules 是急切加载的,这意味着它们会在应用程序加载时立即加载。而延迟加载模块在需要时则会加载 NgModules。NativeScript 中的延迟加载模块的处理方式与 Web Angular 应用程序中的处理方式相同。你可以在此处阅读有关延迟加载模块的更多信息。

主页 UI

在我们创建主页 UI 之前,我们先创建我们的 FlickModelFlickService。这将允许我们直接在模板中使用数据。

FlickModel 将包含每个电影对象的形状。在 core 内部创建一个 models 目录,并创建一个名为 flick.model.ts 的新文件。打开新的 flick.model.ts 并添加以下 interface

typescript
// src/app/core/models/flick.model.ts

export interface FlickModel {
  id: number
  genre: string
  title: string
  image: string
  url: string
  description: string
  details: {
    title: string
    body: string
  }[]
}

然后,我们将在 FlickService 中使用 FlickModel 来返回我们的电影数据。在 core 内部创建一个 services 目录,并创建一个名为 flick.service.ts 的新文件。打开新的 flick.service.ts 并添加以下内容

typescript
// src/app/core/services/flick.service.ts

import { Injectable } from '@angular/core'
import { FlickModel } from '~/app/core/models'

@Injectable({
  providedIn: 'root',
})
export class FlickService {
  private flicks: FlickModel[] = [
    {
      id: 1,
      genre: 'Musical',
      title: 'Book of Mormon',
      image: '~/app/assets/bookofmormon.png',
      url: 'https://nativescript.cn/images/ngconf/book-of-mormon.mov',
      description: `A satirical examination of the beliefs and practices of The Church of Jesus Christ of Latter-day Saints.`,
      details: [
        {
          title: 'Music, Lyrics and Book by',
          body: 'Trey Parker, Robert Lopez, and Matt Stone',
        },
        {
          title: 'First showing on Broadway',
          body: 'March 2011 after nearly seven years of development.',
        },
        {
          title: 'Revenue',
          body: 'Grossed over $500 million, making it one of the most successful musicals of all time.',
        },
        {
          title: 'History',
          body: 'The Book of Mormon was conceived by Trey Parker, Matt Stone, and Robert Lopez. Parker and Stone grew up in Colorado and were familiar with The Church of Jesus Christ of Latter-day Saints and its members. They became friends at the University of Colorado Boulder and collaborated on a musical film, Cannibal! The Musical (1993), their first experience with movie musicals. In 1997, they created the TV series South Park for Comedy Central and in 1999, the musical film South Park: Bigger, Longer & Uncut. The two had first thought of a fictionalized Joseph Smith, religious leader and founder of the Latter Day Saint movement while working on an aborted Fox series about historical characters. Their 1997 film, Orgazmo, and a 2003 episode of South Park, "All About Mormons", both gave comic treatment to Mormonism. Smith was also included as one of South Park\'s "Super Best Friends", a Justice League parody team of religious figures like Jesus and Buddha.',
        },
        {
          title: 'Development',
          body: `During the summer of 2003, Parker and Stone flew to New York City to discuss the script of their new film, Team America: World Police, with friend and producer Scott Rudin (who also produced South Park: Bigger, Longer & Uncut). Rudin advised the duo to see the musical Avenue Q on Broadway, finding the cast of marionettes in Team America similar to the puppets of Avenue Q. Parker and Stone went to see the production during that summer and the writer-composers of Avenue Q, Lopez, and Jeff Marx, noticed them in the audience and introduced themselves. Lopez revealed that South Park: Bigger, Longer & Uncut was highly influential in the creation of Avenue Q. The quartet went for drinks afterward and soon found that each camp wanted to write something involving Joseph Smith. The four began working out details nearly immediately, with the idea to create a modern story formulated early on. For research purposes, the quartet took a road trip to Salt Lake City where they "interviewed a bunch of missionaries—or ex-missionaries." They had to work around Parker and Stone\'s South Park schedule. In 2006, Parker and Stone flew to London where they spent three weeks with Lopez, who was working on the West End production of Avenue Q. There, the three wrote "four or five songs" and came up with the basic idea of the story. After an argument between Parker and Marx, who felt he was not getting enough creative control, Marx was separated from the project.[10] For the next few years, the remaining trio met frequently to develop what they initially called The Book of Mormon: The Musical of the Church of Jesus Christ of Latter-day Saints. "There was a lot of hopping back and forth between L.A. and New York," Parker recalled.`,
        },
      ],
    },
    {
      id: 2,
      genre: 'Musical',
      title: 'Beetlejuice',
      image: '~/app/assets/beetlejuicemusical.png',
      url: 'https://nativescript.cn/images/ngconf/beetlejuice.mov',
      description: `A deceased couple looks for help from a devious bio-exorcist to handle their haunted house.`,
      details: [
        {
          title: 'Music and Lyrics',
          body: 'Eddie Perfect',
        },
        {
          title: 'Book by',
          body: 'Scott Brown and Anthony King',
        },
        {
          title: 'Based on',
          body: 'A 1988 film of the same name.',
        },
        {
          title: 'First showing on Broadway',
          body: 'April 25, 2019',
        },
        {
          title: 'Background',
          body: `In 2016, a musical adaptation of the 1988 film Beetlejuice (directed by Tim Burton and starring Geena Davis as Barbara Maitland, Alec Baldwin as Adam Maitland, Winona Ryder as Lydia Deetz and Michael Keaton as Betelgeuse) was reported to be in the works, directed by Alex Timbers and produced by Warner Bros., following a reading with Christopher Fitzgerald in the title role. In March 2017, it was reported that Australian musical comedian Eddie Perfect would be writing the music and lyrics and Scott Brown and Anthony King would be writing the book of the musical and that another reading would take place in May, featuring Kris Kukul as musical director. The musical has had three readings and two laboratory workshops with Alex Brightman in the title role, Sophia Anne Caruso as Lydia Deetz, Kerry Butler and Rob McClure as Barbara and Adam Maitland.`,
        },
      ],
    },
    {
      id: 3,
      genre: 'Musical',
      title: 'Anastasia',
      image: '~/app/assets/anastasia.png',
      url: 'https://nativescript.cn/images/ngconf/anastasia.mov',
      description: `The legend of Grand Duchess Anastasia Nikolaevna of Russia.`,
      details: [
        { title: 'Music and Lyrics', body: 'Lynn Ahrens and Stephen Flaherty' },
        {
          title: 'Book by',
          body: 'Terrence McNally',
        },
        {
          title: 'Based on',
          body: 'A 1997 film of the same name.',
        },
        {
          title: 'Background',
          body: `A reading was held in 2012, featuring Kelli Barret as Anya (Anastasia), Aaron Tveit as Dmitry, Patrick Page as Vladimir, and Angela Lansbury as the Empress Maria. A workshop was held on June 12, 2015, in New York City, and included Elena Shaddow as Anya, Ramin Karimloo as Gleb Vaganov, a new role, and Douglas Sills as Vlad.
        The original stage production of Anastasia premiered at the Hartford Stage in Hartford, Connecticut on May 13, 2016 (previews). The show was directed by Darko Tresnjak and choreography by Peggy Hickey, with Christy Altomare and Derek Klena starring as Anya and Dmitry, respectively.
        Director Tresnjak explained: "We've kept, I think, six songs from the movie, but there are 16 new numbers. We've kept the best parts of the animated movie, but it really is a new musical." The musical also adds characters not in the film. Additionally, Act 1 is set in Russia and Act 2 in Paris, "which was everything modern Soviet Russia was not: free, expressive, creative, no barriers," according to McNally.
        The musical also omits the supernatural elements from the original film, including the character of Rasputin and his musical number "In the Dark of the Night", (although that song’s melody is repurposed in the new number "Stay, I Pray You"), and introduces instead a new villain called Gleb, a general for the Bolsheviks who receives orders to kill Anya.`,
        },
      ],
    },
  ]

  getFlicks(): FlickModel[] {
    return this.flicks
  }

  getFlickById(id: number): FlickModel | undefined {
    return this.flicks.find((flick) => flick.id === id) || undefined
  }
}

向你的项目添加一个 /src/app/assets/ 目录,并从示例项目中复制 3 个静态图像,此处

备注

你可以为你的模型和服务创建桶式导出,以便更灵活地组织你的文件和文件夹。要执行此操作,请在你的 servicesmodels 目录中创建一个 index.ts,并分别导出 flick.service.tsflick.model.ts。你还可以添加另一个 index.ts 到你的 core 文件夹中,并导出你的 servicesmodels 目录。

接下来,我们分解主页的布局和 UI 元素。

Home page layout breakdown

主页可以分为两部分,带有标题的 ActionBar 和带有卡片的可滚动主内容区域(我们将在下一部分讨论卡片)。我们先从创建带有标题的 ActionBar 入手。打开 home.component.html,添加以下代码

xml
<!-- src/app/features/home/home.component.html -->

<ActionBar title="NativeFlix"></ActionBar>

由于我们有一个要显示的电影数组,所以我们可以使用 NativeScript 的 ListView 组件。ListView 是一种 NativeScript UI 组件,它可以在垂直或水平滚动列表中高效地呈现项目。我们先在我们的 HomeComponent 中创建一个变量,我们将使用它作为我们的 ListView 的数据源。打开 home.component.ts,添加以下内容

typescript
// src/app/features/home/home.component.ts

import { Component } from '@angular/core'

// Add this 👇
import { FlickService } from '~/app/core'

@Component({
  moduleId: module.id,
  selector: 'ns-home',
  templateUrl: 'home.component.html'
})
export class HomeComponent {
  // Add this 👇
  flicks = this.flickService.getFlicks()

  constructor(
    // Add this 👇
    private flickService: FlickService
  ) {}
}

接下来,打开你的 home.component.html,添加 ListView 组件

xml
<!-- src/app/features/home/home.component.html -->

<ActionBar title="NativeFlix"></ActionBar>

<!-- Add this 👇 -->
<ListView height="100%" separatorColor="transparent" [items]="flicks">
  <ng-template let-item="item">
    <StackLayout>
      <Label [text]="item.title"></Label>
    </StackLayout>
  </ng-template>
</ListView>

ListView 使用 items 属性作为其数据源。在上面的代码段中,我们将 items 属性绑定到包含一系列电影的 flicks 属性。如果你现在运行该应用程序,你应该会看到一个电影标题列表。

创建电影卡片

在我们开始着手创建下面的卡片之前,让我们先创建一些类来设定背景和文本颜色,供我们在应用程序中使用。因为这些类将在整个应用程序中共享,所以我们将其添加到 app.css。打开 app.css,并添加以下内容

css
/* src/app.css */

/* applied when device is in light mode */
.ns-light .bg-primary {
  background-color: #fdfdfd;
}
.ns-light .bg-secondary {
  background-color: #ffffff;
}
.ns-light .text-primary {
  color: #444;
}
.ns-light .text-secondary {
  color: #777;
}

/* applied when device is in dark mode */
.ns-dark .bg-primary {
  background-color: #212121;
}
.ns-dark .bg-secondary {
  background-color: #383838;
}
.ns-dark .text-primary {
  color: #eee;
}
.ns-dark .text-secondary {
  color: #ccc;
}

Home page cards breakdown

正如您在上方的图像中所看到的,每张卡片由 3 个组件组成,预览图像、标题和说明。我们将使用 GridLayout 作为容器,并使用 ImageLabel 组件作为预览图像和文本。打开 home.component.html,并添加以下内容

xml
<!-- src/app/features/home/home.component.html -->

<ActionBar title="NativeFlix"></ActionBar>

<ListView height="100%" separatorColor="transparent" [items]="flicks">
  <ng-template let-item="item">
    <!-- Add this 👇 -->
    <!-- The item template can only have a single root view container (e.g. GridLayout, StackLayout, etc.)-->
    <GridLayout
      height="280"
      rows="*, auto, auto"
      columns="*"
      class="bg-secondary"
      borderRadius="10"
      margin="5 10"
      padding="0"
    >
      <Image row="0" margin="0" stretch="aspectFill" [src]="item.image"></Image>
      <Label
        row="1"
        margin="10 10 0 10"
        fontWeight="700"
        class="text-primary"
        fontSize="18"
        [text]="item.title"
      ></Label>
      <Label
        row="2"
        margin="0 10 10 10"
        class="text-secondary"
        fontSize="14"
        textWrap="true"
        [text]="item.description"
      ></Label>
    </GridLayout>
  </ng-template>
</ListView>

检查点

如果您已经执行到此处,在任一平台上运行该应用程序都应该会看到类似于此屏幕截图的应用程序,其列表可以垂直滚动。

Home page

创建详情页面

让我们从使用以下内容创建我们的详细信息功能的文件开始

xml
<!-- src/app/features/details/details.component.html -->
typescript
// src/app/features/details/details.component.ts

import { Component } from '@angular/core'

@Component({
  moduleId: module.id,
  selector: 'ns-details',
  templateUrl: 'details.component.html',
})
export class DetailsComponent {}
typescript
// src/app/features/details/details-routing.module.ts

import { NgModule } from '@angular/core'
import { Routes } from '@angular/router'
import { NativeScriptRouterModule } from '@nativescript/angular'
import { DetailsComponent } from './details.component'

export const routes: Routes = [
  {
    path: '',
    component: DetailsComponent,
  },
]

@NgModule({
  imports: [NativeScriptRouterModule.forChild(routes)],
})
export class DetailsRoutingModule {}
typescript
// src/app/features/details/details.module.ts

import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core'
import { NativeScriptCommonModule } from '@nativescript/angular'
import { DetailsRoutingModule } from './details-routing.module'
import { DetailsComponent } from './details.component'

@NgModule({
  imports: [NativeScriptCommonModule, DetailsRoutingModule],
  declarations: [DetailsComponent],
  schemas: [NO_ERRORS_SCHEMA],
})
export class DetailsModule {}

路由设置

我们将设置我们的 DetailsModule 作为延迟加载的模块,类似于我们在上一部分中的 HomeModule。除了路由名称之外,我们还将把影片的 id 作为路由参数传递进来。路由参数是 path 属性中冒号后面的变量。打开 app-routing.module.ts,并添加以下代码

typescript
// src/app/app-routing.module.ts

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

const routes: Routes = [
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  {
    path: 'home',
    loadChildren: () => import('./features/home/home.module').then(m => m.HomeModule)
  },

  // Add this 👇
  {
    path: 'details/:id',
    loadChildren: () =>
      import('./features/details/details.module').then(m => m.DetailsModule)
  }
]

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

设置导航

现在我们已经设置好路由,我们可以使用 NativeScript Angular 的 RouterExtensions 执行导航操作。RouterExtensions 类提供了命令式导航的方法,类似于使用 Angular RouterLocation 类进行导航的方法。要使用该类,请将它注入到组件构造函数中,然后调用其 navigate 函数。打开 home.component.ts,并添加以下内容

typescript
// src/app/features/home/home.component.ts

import { Component } from '@angular/core'
import { FlickService } from '~/app/core'

// Add this 👇
import { ItemEventData } from '@nativescript/core'
import { RouterExtensions } from '@nativescript/angular'

@Component({
  moduleId: module.id,
  selector: 'ns-home',
  templateUrl: 'home.component.html'
})
export class HomeComponent {
  flicks = this.flickService.getFlicks()

  constructor(
    private flickService: FlickService,

    // Add this 👇
    private routerExtensions: RouterExtensions
  ) {}

  // Add this 👇
  onFlickTap(args: ItemEventData): void {
    this.routerExtensions.navigate(['details', this.flicks[args.index].id])
  }
}

接下来,让我们为 ListView 的项目添加点击事件。打开 home.component.html,并添加以下内容

xml
<!-- src/app/features/home/home.component.html -->

<ActionBar title="NativeFlix"></ActionBar>

<!-- Update this 👇 -->
<ListView
  height="100%"
  separatorColor="transparent"
  [items]="flicks"
  (itemTap)="onFlickTap($event)"
>
  <ng-template let-item="item">
    <!-- The item template can only have a single root view container (e.g. GridLayout, StackLayout, etc.)-->
    <GridLayout
      height="280"
      borderRadius="10"
      class="bg-secondary"
      rows="*, auto, auto"
      columns="*"
      margin="5 10"
      padding="0"
    >
      <Image row="0" margin="0" stretch="aspectFill" [src]="item.image"></Image>
      <Label
        row="1"
        margin="10 10 0 10"
        fontWeight="700"
        class="text-primary"
        fontSize="18"
        [text]="item.title"
      ></Label>
      <Label
        row="2"
        margin="0 10 10 10"
        class="text-secondary"
        fontSize="14"
        textWrap="true"
        [text]="item.description"
      ></Label>
    </GridLayout>
  </ng-template>
</ListView>

访问路由参数

在上一部分中,我们在导航到详细信息组件时将用户点击的影片卡片的 id 传递了进来。我们可以使用 Angular 路由器的 ActivatedRoute 在组件刚创建后立即获取路由信息的静态映像。该快照返回一个 params 属性,其中包含一个对象,该对象包含我们在导航中定义的路由参数。然后,我们可以使用 id 来获取所选影片的信息,以便在我们的详细信息组件的模板中显示。打开 details.component.ts,并添加以下内容

typescript
// src/app/features/details/details.component.ts

import { Component } from '@angular/core'

// Add this 👇
import { ActivatedRoute } from '@angular/router'
import { FlickService, FlickModel } from '~/app/core'

@Component({
  moduleId: module.id,
  selector: 'ns-details',
  templateUrl: 'details.component.html'
})
export class DetailsComponent {
  // Add this 👇
  flick: FlickModel | undefined = undefined

  // Add this 👇
  constructor(
    private activatedRoute: ActivatedRoute,
    private flickService: FlickService
  ) {}

  // Add this 👇
  ngOnInit(): void {
    const id = +this.activatedRoute.snapshot.params.id
    if (id) {
      this.flick = this.flickService.getFlickById(id)
    }
  }
}

详细信息用户界面

让我们细分一下详细信息页面的布局和用户界面元素。

Details page layout breakdown

详细信息页面可以分为三个主要部分:带有标题的 ActionBar、内容图片以及带有内容详情的主内容。我们使用来自我们的 `flicks` 对象的 `details` 数组来填充内容详情部分。`details` 数组包含带有 `title` 和 `body`(使用统一样式渲染)的对象。我们可以使用 Angular 的 `*ngFor` 指令在数组中进行循环并为数组中的每个条目创建一个用户界面元素或元素集。打开 `details.component.html` 并添加以下代码

xml
<!-- src/app/features/details/details.component.html -->

<!-- actionbar -->
<ActionBar [title]="flick?.title"></ActionBar>

<ScrollView height="100%">
  <StackLayout>
    <!-- hero image -->
    <Image margin="0" stretch="aspectFill" [src]="flick?.image"></Image>

    <!-- main content -->
    <StackLayout padding="10 20">
      <ng-container *ngFor="let detail of flick?.details">
        <Label
          marginTop="15"
          fontSize="16"
          fontWeight="700"
          class="text-primary"
          textWrap="true"
          [text]="detail.title"
        ></Label>
        <Label
          fontSize="14"
          class="text-secondary"
          textWrap="true"
          [text]="detail.body"
        ></Label>
      </ng-container>
    </StackLayout>
  </StackLayout>
</ScrollView>

检查点

在任一平台上运行该应用程序现在都应该得到一个类似于此屏幕截图中的应用程序,该应用程序能够在主页和详细信息页面之间导航。

Details page

接下来

恭喜!您构建了第一个既可以在 iOS 上运行又可以在 Android 上运行的 NativeScript 应用程序。您可以继续添加更多 NativeScript 用户界面组件(或构建您自己的自定义用户界面组件),或者您可以添加一些 原生功能。可能性无限!