65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET Core 和 Angular 2 代码探索 - 第 1 部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (24投票s)

2016 年 9 月 6 日

CPOL

33分钟阅读

viewsIcon

133915

downloadIcon

1271

本文将引导您从零开始,在 ASP.NET Core 1.0 环境下使用 Angular 2 和 Web API 2 构建一个数据驱动的 Web 应用。

引言

如果您偶然发现这篇文章,那么我猜测您是 Angular 2 的新手,并且希望通过实际示例来动手实践。和您一样,我也对 Angular 2 相当陌生,这就是我撰写本系列文章的原因,以供其他可能希望学习 Angular 2 的 ASP.NET 开发人员参考。

幸运的是,我有机会审阅了 Valerio De Sanctis 撰写的关于 ASP.NET Web API 和 Angular 2 的一本书。所以,现在是时候将我从书中学习到的知识融入到本系列文章中了。如果您正在寻找关于 Angular 2 与 ASP.NET Web API 的深入信息,我强烈建议您此处购买一本该书。

在我上一篇文章中,我们已经搭建了 Angular 2 应用的基础骨架。正如承诺的那样,我们将继续探索 ASP.NET Core 中的 Angular 2,通过使用 Web API 来处理服务器端数据。

在本系列中,我们将学习如何在 ASP.NET Core 环境下从零开始构建一个数据驱动的 Angular 2 应用。以下是本系列的输出预览:

你将学到什么

在本系列中,您将学习以下内容:

  • Angular 2 RC 和 Core Web API 2 概述
  • 从 Angular 2 Beta 升级到 RC6
    • 启用静态文件服务和诊断
    • 添加 typings.json 文件
    • 更新 package.json 文件
    • 更新 tsconfig.json 文件
    • 更新 AppComponent 文件
    • 添加 Angular 2 模块文件
    • 更新引导文件
    • 添加 SystemJS 文件
    • 更新 index.html
    • 切换到 Gulp
    • 测试应用
  • Web 请求和响应流概述
  • 创建 Backpacker’s Lounge 应用
  • 重构应用
  • 集成 NewtonSoft.JSON
  • 创建 ViewModels
  • 创建 Web API 控制器
  • 创建客户端 ViewModels
  • 创建客户端服务
  • 创建 Angular 2 组件
  • 启用客户端路由
  • 重写
  • 运行应用程序
  • 技巧

不过,在您继续之前,虽然不是必需的,但我建议您阅读我之前关于ASP.NET Core 中 Angular 2 入门的文章,因为在本系列中我将不再详细介绍如何在 ASP.NET Core 中设置 Angular 2。

概述

在我们继续之前,让我们简要回顾一下每个框架。

Core Web API

ASP.NET Core Web API 是一个基于 .NET Core 构建的框架。它专门用于构建 RESTful 服务,可以服务于包括 Web 浏览器、移动设备等在内的大量客户端。ASP.NET Core 内置了对 MVC 构建 Web API 的支持。统一这两个框架可以简化应用的构建。

Angular 2

Angular 2 是 AngularJS 的第二个主要版本,完全用 TypeScript 编写。对于使用 Angular 1.x 的开发人员来说,Angular 2 可能是一个很大的变化,因为它完全基于组件,并且通过增强的依赖注入 (DI) 能力,面向对象变得更加容易。

我们可以将 Web API 视为我们的数据传输网关,它由一组服务器端接口/端点组成。这组接口/端点处理请求-响应消息,通常以 JSON 或 XML 的形式表示。换句话说,我们的 Web API 将作为我们的中心网关,处理来自客户端的请求,执行服务器端数据操作,然后将响应发送回客户端(调用者)。Angular 可以被描述为一个现代客户端库,它提供了丰富的功能,使浏览器能够将网页的输入/输出部分绑定到一个灵活、可重用且易于测试的 JavaScript 模型。

在本部分中,我们将了解这两个框架的客户端-服务器功能以及它们如何相互交互。换句话说,我们需要了解 Angular 2 如何从 ASP.NET Core Web API 检索和传输数据。我们将使用 Angular 2 RC6 和 ASP.NET Core 1.0 RTM 来构建应用程序。

升级到 Angular2 RC 6

我在上一篇文章中的演示项目使用的是 Angular 2 Beta 版本,因为那是我撰写文章时的当前版本。最近,Angular 2 发布了候选版本 6 (RC6)。为了与最新版本保持一致,我认为升级到最稳定的版本并充分利用 Angular 2 的功能是一个好主意。

您可以在这里找到当前版本的更改日志:CHANGELOG.md

引用

重要提示:当前版本的 Angular 2 至少需要 node v4.x.x 和 npm 3.x.x。通过在终端/控制台窗口中运行 node -v 和 npm -v 来验证您正在运行所述版本。旧版本会产生错误。

让我们开始修改!

在进行任何文件更改之前,请确保您拥有受支持的 Node 和 NPM 版本。如果您使用的是旧版本,请卸载 node,然后从此处下载并安装所需版本。

NPM 已经集成在 Node 安装程序中,因此您无需担心手动升级它。

安装后,请务必重启您的机器,以确保更新生效。在我的情况下,我安装了以下版本:

  • Node v6.3.1(撰写本文时的当前版本)
  • NPM 3.10.3(撰写本文时的最新版本)

请记住,如果您使用的是 Angular 2 RC 4 及更早版本,此升级同样适用。

添加 typings.json 文件

自 RC5 及更高版本发布以来,typings.json 文件可能不再需要。如果我们需要支持向后兼容性并支持以前的 RC 版本,那么我们需要创建一个新文件来创建包含 nodejasminecore-js 的类型定义。为此,只需右键单击项目根目录,然后添加一个新的 .json 文件。在我的情况下,我选择了“NPM 配置文件”并将其重命名为“typings.json”。用以下内容替换默认生成的内容:

{
  "globalDependencies": {
    "core-js": "registry:dt/core-js#0.0.0+20160602141332",
    "jasmine": "registry:dt/jasmine#2.2.0+20160621224255",
    "node": "registry:dt/node#6.0.0+20160621231320"
  }
}

注意typings.json 文件中的 core-js 行是唯一必需的,但我们也借此机会添加了 jasmine 和 node 类型:如果您想使用 Jasmine 测试框架和/或使用引用 nodejs 环境中对象的代码,您可能在不久的将来需要它们。暂时将它们保留在那里,它们不会损害您的项目。

更新 package.json 文件

如果您不支持以前版本的 Angular,我强烈建议您在更新 package.json 文件之前删除现有的 node_modules 文件夹。这将确保我们不会拥有以前 Angular 2 捆绑包和模块的本地副本。

如果您已决定,请打开 package.json 文件,并将其所有内容替换为以下内容:

{
  "version": "1.0.0",
  "name": "Your Application Name",
  "author": "Optional Field",
  "description": "Optional Field",
  "dependencies": {
    "@angular/common": "2.0.0-rc.6",
    "@angular/compiler": "2.0.0-rc.6",
    "@angular/core": "2.0.0-rc.6",
    "@angular/http": "2.0.0-rc.6",
    "@angular/platform-browser": "2.0.0-rc.6",
    "@angular/platform-browser-dynamic": "2.0.0-rc.6",
    "@angular/upgrade": "2.0.0-rc.6",
    "@angular/forms": "2.0.0-rc.6",
    "@angular/router": "3.0.0-rc.2",

    "core-js": "^2.4.1",
    "reflect-metadata": "^0.1.3",
    "rxjs": "5.0.0-beta.6",
    "systemjs": "^0.19.37",
    "typings": "^1.3.2",
    "zone.js": "^0.6.12",
    "moment": "^2.14.1"
  },

  "devDependencies": {
    "gulp": "^3.9.1",
    "gulp-clean": "^0.3.2",
    "gulp-concat": "^2.6.0",
    "gulp-less": "^3.1.0",
    "gulp-sourcemaps": "^1.6.0",
    "gulp-typescript": "^2.13.6",
    "gulp-uglify": "^2.0.0",
    "typescript": "^1.8.10"
  },
  "scripts": {
    "postinstall": "typings install dt~core-js --global"
  }
}
引用

注意:2016 年 9 月 14 日,Angular 2 最终版本发布。如果您想使用最终版本,那么从 RC6 升级到最终版本非常容易;我们只需要从 package.json 引用中删除 -rc.x 后缀即可。以下是使用最终版本的一个快速示例:

"@angular/common": "2.0.0",
"@angular/compiler": "2.0.0",
"@angular/core": "2.0.0",
"@angular/forms": "2.0.0",
"@angular/http": "2.0.0",
"@angular/platform-browser": "2.0.0",
"@angular/platform-browser-dynamic": "2.0.0",
"@angular/router": "3.0.0",
"@angular/upgrade": "2.0.0",

带有 @ 符号的包是新 Angular 2 捆绑包的一部分:其他包用于加载库、辅助工具和旧浏览器支持的 polyfills。撰写本文时,Angular 2 的当前版本是 2.0.0-rc.6。您可以在此处查看更新。您可能还注意到,我们已将对 Grunt 的依赖替换为 Gulp ~ 我将在本文后面解释原因。如果您愿意,可以像我一样删除现有的 gruntfile.js 文件,但如果您更喜欢使用 Grunt,请随意。没有人会因为您使用 Grunt 而解雇您。:)

您可能还注意到,我们添加了 scripts 部分来安装 typings。Typings 管理和安装 TypeScript 定义。

现在,保存文件以恢复应用程序所需的包。

安装完成后,您应该会看到类似以下内容:

图 1:应用程序依赖项

提示:如果 Typings 未能成功保存安装,请尝试右键单击“Dependencies”节点,然后选择“Restore Package”选项。另一种方法是使用命令行显式运行 typings。为此,只需导航到应用程序的根文件夹,然后按 CTRL+SHIFT,选择“Open command window here”。在命令行中,键入以下命令:

npm run typings install

如果成功,您应该会看到类似以下内容:

图 2:命令行

回顾一下,所有 Angular 2 依赖项都将安装在您本地驱动器上的以下位置:

../src/<YourProjectName>/node_modules

更新 tsconfig.json 文件

现在打开您的 tsconfig.json 文件,如果您没有,则需要创建一个。这是更新后的 TypeScript JSON 配置文件:

{
  "compileOnSave": false,
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "module": "system",
    "moduleResolution": "node",
    "noImplicitAny": false,
    "noEmitOnError": false,
    "removeComments": false,
    "target": "es5"
  },
  "exclude": [
    "node_modules",
    "wwwroot"
  ]
}

compileOnSave 指示 IDE 在保存时为给定的 tsconfig.json 生成所有文件。compilerOptions 配置将影响 Intellisense 和我们的外部 TypeScript 编译器的工作方式。通过在配置中 excluding 文件夹,我们告诉 Visual Studio 2015 提供的内置 TypeScript 编译器禁用编译该位置的外部 TypeScript 文件。

更新 AppComponent 文件

由于我们正在升级到 RC 版本,我们还需要更新引用了以前 Angular Beta 版本的组件。在我们的特定示例中,我们需要将 app.component.ts 文件更新为:

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

@Component({
    selector: 'angularjs2demo',
    template: `<h1>AngularJS 2 Demo</h1>
               <div>Hello ASP.NET Core! Greetings from AngularJS 2.</div>`
})

export class AppComponent { } 

关于上述更新,没有什么好多说的。我们只是将 import 引用更改为新名称 @angular/core

添加 Angular 2 模块文件

Angular 模块,也称为 NgModules,自 Angular2 RC5 以来已引入。这提供了一种强大的方式来组织和引导任何 Angular2 应用程序:它们帮助开发人员将自己的一组组件、指令和管道整合到可重用的块中。

自 RC5 以来,每个 Angular2 应用程序都必须至少有一个模块,通常称为根模块,并给定 AppModule 类名。

现在,创建一个新的 TypeScript 文件并将其命名为“app.module.ts”。如果您正在遵循我之前的文章,那么您可以在 Scripts/app 文件夹下创建该文件。app.module.ts 文件应如下所示:

///<reference path="../../typings/index.d.ts"/>
import {NgModule} from "@angular/core";  
import {BrowserModule} from "@angular/platform-browser";  
import {HttpModule} from "@angular/http";  
import "rxjs/Rx";

import {AppComponent} from "./app.component";

@NgModule({
    // directives, components, and pipes
    declarations: [
        AppComponent
    ],
    // modules
    imports: [
        BrowserModule,
        HttpModule
    ],
    // providers
    providers: [
    ],
    bootstrap: [
        AppComponent
    ]
})
export class AppModule { }  

上述配置的第一行添加了对类型定义的引用,以确保我们的 TypeScript 编译器能够找到它。请注意,如果使用 CDN 或预编译的 Angular2 版本,我们绝对可以(也应该)删除此行。然后,我们导入应用程序所需的基本 Angular2 模块。您可以在需要时在此文件中添加更多 Angular 2 模块引用。我们还导入了 rxjs 库定义文件,这将有助于编译一些 Angular2 库。然后,我们导入了我们的组件“AppComponent”。最后,我们声明了我们的根 NgModule:正如我们所看到的,它由一个命名数组的数组组成,每个数组都包含一组服务于共同目的的 Angular2 对象:指令、组件、管道、模块和提供程序。其中最后一个包含我们想要引导的组件,在我们的例子中是 AppComponent 文件。

有关 App Module 的更多信息,请参阅:https://angular.io/docs/ts/latest/cookbook/rc4-to-rc5.html。

更新引导文件

打开 boot.ts 文件,并用以下内容替换现有代码:

import {platformBrowserDynamic} from "@angular/platform-browser-dynamic";  
import {AppModule} from "./app.module";

platformBrowserDynamic().bootstrapModule(AppModule);  

就像在 app.component.ts 文件中一样,我们将引用更改为新的 Angular 捆绑包。请注意,我们引用了我们之前创建的新 AppModule。现在我们只缺少一个用于浏览器加载的入口点:现在就添加它。

添加 SystemJS 文件

现在,让我们添加 systemjs 配置文件。右键单击 wwwroot 文件夹,然后选择“添加”>“新项”。在“客户端模板”下,选择“JavaScript 文件”,如下图所示:

图 3:添加新项对话框

将文件命名为“systemjs.config.js”,然后复制以下代码:

(function (global) {
    System.config({
        paths: {
            // paths serve as alias
            'npm:': 'js/'
        },
        // map tells the System loader where to look for things
        map: {
            // our app is within the app folder
            app: 'app',

            // angular bundles
            '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
            '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
            '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
            '@angular/platform-browser': 
               'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
            '@angular/platform-browser-dynamic': 
               'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
            '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
            '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
            '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',

            // angular testing umd bundles
            '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
            '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
            '@angular/compiler/testing': 
                'npm:@angular/compiler/bundles/compiler-testing.umd.js',
            '@angular/platform-browser/testing': 
                'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
            '@angular/platform-browser-dynamic/testing': 
                'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
            '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
            '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
            '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',

            // other libraries
            'rxjs': 'npm:rxjs',
            'angular2-in-memory-web-api': 'npm:angular2-in-memory-web-api',
        },
        // packages tells the System loader how to load when no filename and/or no extension
        packages: {
            app: {
                main: './boot.js',
                defaultExtension: 'js'
            },
            rxjs: {
                defaultExtension: 'js'
            },
            'angular2-in-memory-web-api': {
                defaultExtension: 'js'
            }
        }
    });
})(this);

SystemJS.config 文件将加载应用程序和库模块。我们需要此加载器来构建我们的 Angular2 应用。有关 SystemJS 配置的更多详细信息,请阅读:SystemJS

如果您想使用 WebPack 作为模块打包器,请参阅:WebPack 简介

更新 index.html

现在,由于我们需要 SystemJS 配置来加载我们的应用程序模块和组件,因此我们需要更新 index.html 以包含新配置。替换您的 index.html 文件,使其看起来像这样:

<html>  
<head>  
    <title>ASP.NET Core with Angular 2 RC Demo</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Step 1. Load libraries -->
    <!-- Polyfill(s) for older browsers -->
    <script src="js/shim.min.js"></script>
    <script src="js/zone.js"></script>
    <script src="js/Reflect.js"></script>
    <script src="js/system.src.js"></script>

    <!-- Angular2 Native Directives -->
    <script src="/js/moment.js"></script>

    <!-- Step 2. Configure SystemJS -->
    <script src="systemjs.config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>
</head>  
<!-- Step 3. Display the application -->  
<body>  
    <!-- Application PlaceHolder -->
    <angularjs2demo>Please wait...</angularjs2demo>
</body>  
</html>  

切换到 Gulp

这次,我们将使用 Gulp 作为 JavaScript 任务运行器来自动化我们的客户端脚本。我想指出,切换到 Gulp 并不意味着 Grunt 是一个糟糕的工具。当然,Grunt 仍然是一个很棒的工具,并且被许多人使用。如果您愿意,您始终可以使用它来自动化任务。我只是更喜欢使用 Gulp,因为它简洁明了,易于编写任务。它还使用 node.js 的流,并且执行速度更快,因为它不会一直打开/关闭文件或创建中间副本。但是话又说回来,每个开发人员都有自己的偏好,所以使用哪个 JS 任务运行器完全取决于您自己 :)。

现在,让我们为 Gulp 配置添加一个新文件。右键单击项目解决方案,然后选择“添加”>“新项”。在“客户端模板”下,选择“Gulp 配置文件”,如下图所示:

图 4:添加新文件

单击“添加”以生成文件,然后将默认生成的配置替换为以下代码:

var gulp = require('gulp'),  
    gp_clean = require('gulp-clean'),
    gp_concat = require('gulp-concat'),
    gp_less = require('gulp-less'),
    gp_sourcemaps = require('gulp-sourcemaps'),
    gp_typescript = require('gulp-typescript'),
    gp_uglify = require('gulp-uglify');

/// Define paths
var srcPaths = {  
    app: ['Scripts/app/main.ts', 'Scripts/app/**/*.ts'],
    js: [
        'Scripts/js/**/*.js',
        'node_modules/core-js/client/shim.min.js',
        'node_modules/zone.js/dist/zone.js',
        'node_modules/reflect-metadata/Reflect.js',
        'node_modules/systemjs/dist/system.src.js',
        'node_modules/typescript/lib/typescript.js',
        'node_modules/ng2-bootstrap/bundles/ng2-bootstrap.min.js',
        'node_modules/moment/moment.js'
    ],
    js_angular: [
        'node_modules/@angular/**'
    ],
    js_rxjs: [
        'node_modules/rxjs/**'
    ]
};

var destPaths = {  
    app: 'wwwroot/app/',
    js: 'wwwroot/js/',
    js_angular: 'wwwroot/js/@angular/',
    js_rxjs: 'wwwroot/js/rxjs/'
};

// Compile, minify and create sourcemaps all TypeScript files 
// and place them to wwwroot/app, together with their js.map files.
gulp.task('app', ['app_clean'], function () {  
    return gulp.src(srcPaths.app)
        .pipe(gp_sourcemaps.init())
        .pipe(gp_typescript(require('./tsconfig.json').compilerOptions))
        .pipe(gp_uglify({ mangle: false }))
        .pipe(gp_sourcemaps.write('/'))
        .pipe(gulp.dest(destPaths.app));
});

// Delete wwwroot/app contents
gulp.task('app_clean', function () {  
    return gulp.src(destPaths.app + "*", { read: false })
    .pipe(gp_clean({ force: true }));
});

// Copy all JS files from external libraries to wwwroot/js
gulp.task('js', function () {  
    gulp.src(srcPaths.js_angular)
        .pipe(gulp.dest(destPaths.js_angular));
    gulp.src(srcPaths.js_rxjs)
        .pipe(gulp.dest(destPaths.js_rxjs));
    return gulp.src(srcPaths.js)
        .pipe(gulp.dest(destPaths.js));
});

// Watch specified files and define what to do upon file changes
gulp.task('watch', function () {  
    gulp.watch([srcPaths.app, srcPaths.js], ['app', 'js']);
});

// Define the default task so it will launch all other tasks
gulp.task('default', ['app', 'js', 'watch']);  

上述代码包含三个 (3) 主要变量:

  • gulp - 初始化运行任务所需的所有 Gulp 插件。
  • srcPaths - 包含我们要复制和转译的源文件数组。
  • destPaths - 包含 wwwroot 中特定位置的数组。这基本上是我们转储在 srcPaths 中定义的脚本的位置。

它还包含五个 (5) 主要任务:

  • app_clean - 此任务从我们定义的目标文件夹中删除现有文件。
  • app - 此任务编译、压缩并为所有 TypeScript 文件创建源映射,并将它们连同 js.map 文件一起放置到 wwwroot/app 文件夹中。
  • js - 此任务将从 node_modules 文件夹中的外部库复制所有 JavaScript 文件,并将它们放置到 wwwroot/js 文件夹中。
  • watch - 此任务监视 app 和 js 任务中已更改的文件。
  • default - 定义默认任务,以便它将启动所有其他任务。

测试应用

清理并构建您的解决方案,确保没有错误。如果构建成功,则右键单击 gulpfile.js 并选择“任务运行器资源管理器”。

请务必点击“刷新”按钮以加载任务,如下图所示:

图 5:任务运行器

现在,右键单击默认任务并点击运行。如果成功,您应该会看到类似以下内容。

图 6:任务运行中

成功运行任务运行器后,您还应该看到“app”和“js”文件夹已在您的“wwwroot”文件夹中生成,如下图所示:

图 7:文件转译

运行应用程序应产生类似以下的结果:

图 8:输出

如果您已经走到这一步,恭喜!您现在拥有一个正在运行的 Angular 2 RC6,并且现在可以使用 Core Web API 处理数据了。

技巧

如果您遇到以下 Typescript 构建错误:

引用

“TS2664 扩充中的模块名称无效,找不到模块 '../../Observable'。”

只需按照此处提供的解决方案操作:https://github.com/Microsoft/TypeScript/issues/8518#issuecomment-229506507

在我的情况下,由于我使用的是 VStudio 2015 Update 3,解决方案是将 typescriptService.js 替换为上述链接中提到的更新版本。换句话说,替换以下内容:

C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\
CommonExtensions\Microsoft\TypeScript\typescriptServices.js

用这个:

https://raw.githubusercontent.com/Microsoft/TypeScript/Fix8518/lib/typescriptServices.js
引用

在更新之前,请务必备份您现有的 typescriptService.js 文件。

如果您在编译过程中遇到类似以下错误或警告:

引用

TS1005 Build:'=' 预期。 TS1005 Build:';' 预期。

请确保您在 ts.config 中排除了 wwwroot。如果不起作用,您可以尝试以下选项进行修复:

  • 选项 1:在 ts.config 文件中过滤掉包含 *.d.ts 文件的位置,使其不参与编译。
  • 选项 2:通过在项目根目录中运行以下命令,升级到 TypeScript 1.9.x 或更高 Beta 版本:
npm install typescript@next

创建应用程序

在动手实践之前,让我们快速了解一下客户端发出请求时会发生什么,以及服务器如何通过查看下图发送 JSON 响应:

图 9:Web 请求和响应流

异步数据请求是任何用户交互,只要它需要从服务器检索数据,一个非常典型的例子包括(但不限于):单击按钮显示数据或编辑/删除某些内容,导航到另一个视图,提交表单等等。为了响应任何客户端发出的异步数据请求,我们需要构建一个服务器端 Web API 控制器来处理客户端请求 (CRUD)。换句话说,API 将处理请求,执行数据库 CRUD 操作,然后将序列化的 ViewModel 作为响应返回给客户端。

为了理解 Angular 和 Web API 在真实场景中如何相互连接,让我们开始构建我们的应用程序。

背包客休息室应用

在本系列中,我们将创建一个涵盖一系列 CRUD 操作的应用程序。我喜欢旅行和探索大自然的奇观,所以为了让这个应用程序对初学者来说既独特又有趣,我决定构建一个简单的背包客休息室应用程序。

更清楚地说,我们将创建类似这样的东西:

图 10:主应用程序布局

在本系列中,我们将专注于创建 Home 内容。这意味着,我们将创建用于显示最新讨论、最新添加地点和最多查看地点的组件。我们还将设置客户端路由,并了解 Angular2 中的导航如何工作。此外,我们将实现一个简单的主-从导航,以了解如何在组件之间传递信息。最后,我们将了解 Angular2 中双向数据绑定的基本原理。

根据上图,我们需要以下几组 API 调用:

  • api/Lounge/GetLatestDiscussion
  • api/Place/GetLatestEntries
  • api/Place/GetMostViewed

GetLatestDiscussion 将返回讨论项列表。此 API 方法将托管在 LoungeController 类中。

GetLatestEntries 将返回新添加地点的项目列表。GetMostViewed 将返回页面浏览量最高的地点项目列表。根据上面定义的 API URI,这两个 API 方法都将托管在 PlaceController 类中。

重构应用

在我们继续之前,让我们改进我们的应用程序结构,使其更易于维护并体现关注点分离的价值。

查看图 10 所示的图片,我们需要在现有的“app”文件夹中添加一些文件夹,然后移动一些现有文件。

让我们从在 Scripts/app 文件夹下添加以下文件夹开始:

  • components
  • services
  • viewmodels

components 文件夹用于存储所有与 Angular 2 相关的组件的 TypeScript 文件。

services 文件夹用于存储我们基于 TypeScript 的服务,用于与 ASP.NET Core Web API 通信。

viewmodels 文件夹用于存储我们基于 TypeScript 的强类型视图模型。

现在,在 components 文件夹下,添加以下子文件夹:

  • 主页
  • lounge
  • about
  • account
  • explore

如果您注意到,上面的文件夹与图 10 中显示的导航菜单相匹配。请注意,文件夹的名称不一定需要与菜单相同。您可以根据自己的喜好命名文件夹,但为了本演示方便参考,我们暂时保持一致。每个文件夹都用于存储网站中特定功能的相应组件。

设置完所有这些文件夹后,将“app.component.ts”移动到 components 文件夹。您的项目结构现在应该看起来像这样:

图 11:组件文件夹

由于我们移动了 app.component.ts 文件的位置,因此我们还需要更新 app.module.ts 文件中对该文件的导入引用:

import {AppComponent} from "./components/app.component";  

集成 NewtonSoft.JSON

如果您正在使用 ASP.NET 并且从未听说过 Newtonsoft 的 Json.NET,那么您肯定错过了可以大大简化工作的东西。我们谈论的是 .NET 开发人员(至少是本文作者)开发过的最好的库之一——也是最有用的工具之一:一个非常高效(因此非常流行)、高性能的 JSON 序列化器、反序列化器和 .NET 的全能框架,它也恰好完全是开源的

要在我们的项目中添加 Newtonsoft.JSON,请右键单击项目根目录,然后选择“管理 NuGet 包”。在“浏览”选项卡下,在搜索栏中输入“newtonsoft”,它应该会显示该包,如下所示:

图 12:管理 Nuget 包

撰写本文时,Newtonsoft.Json 的最新版本是 9.01。单击“安装”将依赖项添加到我们的项目中。成功安装后,您应该能够在项目引用中看到已添加的 Newstonsoft.Json。

创建 ViewModels

快速回顾一下,ViewModel 只是包含视图/UI 中所需某些属性的类。从现在开始,我们将使用 ViewModels 作为数据传输对象:在客户端和服务器之间来回发送数据。

在本部分,我们暂时不使用任何数据库来测试。我们只是一些静态测试数据,以了解如何通过使用结构良好且高度可配置的接口来回传递它们。使用 ASP.NET 和 Angular2 构建原生 Web 应用程序的一大优点是,我们可以开始编写代码,而无需过多担心数据源:它们稍后才会出现,并且只有在我们确定真正需要什么之后。

现在,是时候创建服务器端 ViewModels 了。

在项目根目录创建一个新文件夹,命名为“Data”,并在其下创建一个子文件夹,命名为“ViewModels”。在该文件夹内,创建以下类:

  • LoungeViewModel.cs
  • PlaceViewModel.cs

我们现在应该有以下结构:

图 13:ViewModels 文件夹

以下是每个类的代码。

LoungeViewModel.cs

更新默认生成的代码,使其与以下代码类似:

using System;  
using Newtonsoft.Json;

namespace TheBackPackerLounge.Data.ViewModels  
{
    [JsonObject(MemberSerialization.OptOut)]
    public class LoungeViewModel
    {
        public int ID { get; set; }
        public string Subject { get; set; }
        public string Message { get; set; }
        [JsonIgnore]
        public int ViewCount { get; set; }
        public DateTime CreatedDate { get; set; }
        public DateTime LastModifiedDate { get; set; }
    }
}

The PlaceViewModel.cs

这是 PlaceViewModel 类的代码:

using System;  
using Newtonsoft.Json;

namespace TheBackPackerLounge.Data.ViewModels  
{
    [JsonObject(MemberSerialization.OptOut)]
    public class PlaceViewModel
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public string Location { get; set; }
        [JsonIgnore]
        public int ViewCount { get; set; }
        public DateTime CreatedDate { get; set; }
        public DateTime LastModifiedDate { get; set; }
    }
}

上面两个 ViewModel 都只是包含一些属性的类。每个类都用 [JsonObject(MemberSerialization.OptOut)] 属性进行修饰,除非用显式的 [JsonIgnore] 进行修饰,否则会导致属性序列化为 JSON。我们做出这个选择是因为我们将需要大部分 ViewModel 的属性被序列化,我们很快就会看到。

目前,我们还限制了 ViewModels 中定义的属性。最终,一旦我们集成了授权、身份验证和地点图片,我们将添加更多属性。所以现在,让我们继续。

创建 Web API 控制器

现在,让我们在应用中创建所需的 API 控制器。

休息室控制器 (LoungeController)

在“Controllers”文件夹下创建一个新类,并将其命名为“LoungeController”。为此,只需右键单击“Controllers”文件夹,然后选择“添加”>“新项”。在 ASP.NET 模板下,选择“Web API 控制器类”。将默认生成的代码替换为以下内容:

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Threading.Tasks;  
using Microsoft.AspNetCore.Mvc;  
using TheBackPackerLounge.Data.ViewModels;  
using Newtonsoft.Json;

namespace TheBackPackerLounge.Controllers  
{
    [Route("api/[controller]")]
    public class LoungeController : Controller
    {
        [HttpGet("{id}")]
        public IActionResult Get(int id)
        {
            return new JsonResult(GetTestData().Where(i => i.ID == id), DefaultJsonSettings);
        }

        [HttpGet("GetLatestDiscussion")]
        public IActionResult GetLatestDiscussion()
        {
            return GetLatestDiscussion(DefaultNumberOfItems);
        }

        [HttpGet("GetLatestDiscussion/{n}")]
        public IActionResult GetLatestDiscussion(int n)
        {
            var data = GetTestData().OrderByDescending(i => i.CreatedDate).Take(n);
            return new JsonResult(data, DefaultJsonSettings);
        }

        private List<LoungeViewModel> GetTestData(int num = 99)
        {
            List<LoungeViewModel> list = new List<LoungeViewModel>();
            DateTime date = DateTime.Now.AddDays(-num);
            for (int id = 1; id <= num; id++)
            {
                list.Add(new LoungeViewModel()
                {
                    ID = id,
                    Subject = String.Format("Discussion {0} Subject", id),
                    Message = String.Format("This is a sample message 
                              for Discussion {0} Subject: It's more fun in the Philippines", id),
                    CreatedDate = date.AddDays(id),
                    LastModifiedDate = date.AddDays(id),
                    ViewCount = num - id
                });
            }
            return list;
        }

        private JsonSerializerSettings DefaultJsonSettings
        {
            get
            {
                return new JsonSerializerSettings()
                {
                    Formatting = Formatting.Indented
                };
            }
        }

        private int DefaultNumberOfItems
        {
            get
            {
                return 10;
            }
        }

        private int MaxNumberOfItems
        {
            get
            {
                return 50;
            }
        }
    }
}

上面的类利用特性路由,通过使用属性 [Route("api/[controller]")] 来确定该类是一个 API。

上述类包含以下三个 (3) 主要 API 动作方法:

  • Get() – 此方法根据给定的 ID 返回单个数据集。此方法可以通过以下方式调用:/api/lounge/
  • GetLatestDiscussion() – 此方法表示一个 RESTFUL API 方法,它只是调用其重载方法来实际处理请求。此方法可以通过以下方式调用:/api/lounge/getlatestdiscussion
  • GetLatestDiscussion(int n) – 此方法是 GetLatestDiscussion() 的重载,它接受一个 int 参数。它使用 LINQ 的 OrderByDescending()Take 运算符从我们的测试数据中获取最新记录。此方法可以通过以下方式调用:/api/lounge/getlatestdiscussion/<n>,其中 n 是一个变量,代表一个数字。

它还包含以下 private 方法:

  • GetTestData(int num = 99) - 一个测试方法,返回静态项目列表。我们将使用此方法生成 LoungViewModel 列表,以便快速测试。
  • DefaultJsonSettings - 获取默认 JSON 格式设置的属性。
  • DefaultNumberOfItems - 返回 UI 中要返回的默认项目数的属性。
  • MaxNumberOfItems - 返回 UI 中要返回的最大项目数的属性。

我们将使用上述 private 成员在我们的应用中显示测试数据。我们肯定会在本系列的下一部分中将数据访问从 Controller 中分离和解耦,但目前,让我们继续前进。

地点控制器 (PlaceController)

创建另一个 Web API Controller 类,并将其命名为“PlaceController”。再次,用以下内容替换默认代码:

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Threading.Tasks;  
using Microsoft.AspNetCore.Mvc;  
using TheBackPackerLounge.Data.ViewModels;  
using Newtonsoft.Json;

namespace TheBackPackerLounge.Controllers  
{
    [Route("api/[controller]")]
    public class PlaceController : Controller
    {

        [HttpGet("{id}")]
        public IActionResult Get(int id)
        {
            return new JsonResult(GetTestData().Where(i => i.ID == id), DefaultJsonSettings);
        }

        [HttpGet("GetLatestEntries")]
        public IActionResult GetLatestEntries()
        {
            return GetLatestEntries(DefaultNumberOfItems);
        }

        [HttpGet("GetLatestEntries/{n}")]
        public IActionResult GetLatestEntries(int n)
        {
            var data = GetTestData().OrderByDescending(i => i.CreatedDate).Take(n);
            return new JsonResult(data, DefaultJsonSettings);
        }

        [HttpGet("GetMostViewed")]
        public IActionResult GetMostViewed()
        {
            return GetMostViewed(DefaultNumberOfItems);
        }

        [HttpGet("GetMostViewed/{n}")]
        public IActionResult GetMostViewed(int n)
        {
            if (n > MaxNumberOfItems) n = MaxNumberOfItems;
            var data = GetTestData().OrderByDescending(i => i.ViewCount).Take(n);
            return new JsonResult(data, DefaultJsonSettings);
        }

        private List<PlaceViewModel> GetTestData(int num = 999)
        {
            List<PlaceViewModel> list = new List<PlaceViewModel>();
            DateTime date = DateTime.Now.AddDays(-num);
            for (int id = 1; id <= num; id++)
            {
                list.Add(new PlaceViewModel()
                {
                    ID = id,
                    Name = String.Format("Place {0} Name", id),
                    Location = String.Format("Place {0} Location", id),
                    CreatedDate = date.AddDays(id),
                    LastModifiedDate = date.AddDays(id),
                    ViewCount = num - id
                });
            }
            return list;
        }

        private JsonSerializerSettings DefaultJsonSettings
        {
            get
            {
                return new JsonSerializerSettings()
                {
                    Formatting = Formatting.Indented
                };
            }
        }

        private int DefaultNumberOfItems
        {
            get
            {
                return 5;
            }
        }

        private int MaxNumberOfItems
        {
            get
            {
                return 100;
            }
        }
    }
}

您可能已经注意到,这两个类的实现非常相似。我们或许可以将其合并到一个 Controller 中,但我决定将其拆分为不同的类,以体现关注点分离的价值。

您可能还注意到,这两个类具有相同的 private 成员。我们可以通过创建一个单独的类/基类并在其中实现代码来最小化这种情况。但目前,为了便于参考,我们将其保留在单独的类中。

创建客户端 ViewModels

我们将使用 TypeScript 来定义一组类,以便处理类型定义。换句话说,我们不会处理原始 JSON 数据和匿名对象;相反,我们将使用类型化对象:类的实际实例。

Scripts/app/viewmodels 文件夹下,添加一个名为“lounge.ts”的新 TypeScript 文件。然后复制以下代码:

export class Lounge {  
    constructor(
        public ID: number,
        public Subject: string,
        public Message: string
    ) { }
}

再添加一个名为“place.ts”的 TypeScript 文件。复制以下代码:

export class Place {  
    constructor(
        public ID: number,
        public Name: string,
        public Location: string
    ) { }
}

请注意,我们没有添加服务器端 ViewModel 类中存在的所有属性:作为一般经验法则,我们将尽可能保持这些类轻量化,仅定义我们在 UI 中需要的内容:我们随时可以在需要时添加更多属性。

这些 ViewModels 将用作客户端 TypeScript 类,以正确映射从 Web API 控制器返回的 JSON 序列化服务器端 ViewModels

引用

注意:属性名称应与您在服务器端 ViewModels 中定义的属性名称(包括大小写)匹配。

创建客户端服务

现在,我们需要设置一个客户端服务来从 Web API 获取所需的数据:我们将通过向我们之前构建的 API 控制器发出请求来完成此操作。我们将使用 Angular Http 客户端通过 XMLHttpRequest (XHR) 进行通信,这是一个相当复杂的基于 HTTP 的 API,它提供了客户端功能,用于在客户端和服务器之间传输数据。

AppService 类

在“Scripts/app/services”文件夹下创建另一个 TypeScript 文件,并将其命名为“app.service.ts”。复制以下代码:

import {Injectable} from "@angular/core";  
import {Http, Response} from "@angular/http";  
import {Lounge} from "../viewmodels/lounge";  
import {Observable} from "rxjs/Observable";

@Injectable()
export class AppService {  
    constructor(private http: Http) { }

     // URL to web api
    private loungeBaseUrl = 'api/lounge/';
    private placeBaseUrl = 'api/place/'; 

    getLatestDiscussion(num?: number) {
        var url = this.loungeBaseUrl + "GetLatestDiscussion/";
        if (num != null) url += num;
        return this.http.get(url)
            .map(response => response.json())
            .catch(this.handleError);
    }

    getDiscussion(id: number) {
        if (id == null) throw new Error("id is required.");
        var url = this.loungeBaseUrl + id;
        return this.http.get(url)
            .map(response => <Lounge>response.json())
            .catch(this.handleError);
    }

    getLatestEntries(num?: number) {
        var url = this.placeBaseUrl + "GetLatestEntries/";
        if (num != null) url += num;
        return this.http.get(url)
            .map(response => response.json())
            .catch(this.handleError);
    }

    getMostViewed(num?: number) {
        var url = this.placeBaseUrl + "GetMostViewed/";
        if (num != null) url += num;
        return this.http.get(url)
            .map(response => response.json())
            .catch(this.handleError);
    }

    getPlace(id: number) {
        if (id == null) throw new Error("id is required.");
        var url = this.placeBaseUrl + id;
        return this.http.get(url)
            .map(response => <Lounge>response.json())
            .catch(this.handleError);
    }

    private handleError(error: Response) {
        console.error(error);
        return Observable.throw(error.json().error || "Server error");
    }
}

请注意,上面的代码与我们的 Web API Controllers 方法非常相似,只不过我们已将所有对 API 的调用合并到其中。这将是我们的客户端用于从 WebAPI Controller 本身获取数据的类。

请记住,我们使用了 Injectable 装饰器,声明该服务是一个可注入类:这样做会将一组元数据附加到我们的类,这些元数据将在实例化时由 DI 系统使用。基本上,我们在这里所做的是告诉 DI 注入器,构造函数参数应该使用其声明的类型进行实例化。TypeScript 代码允许使用非常流畅的语法在构造函数级别实现此结果,如以下行所示:

    constructor(private http: Http) { }

创建 Angular 2 组件

接下来我们要做的就是为不同内容创建专用组件。让我们从实现“最新讨论”主从组件开始。

休息室列表组件 (LoungeList Component)

在“Scripts/app/components/lounge”下创建一个新的 TypeScript 文件,并将文件命名为“lounge-list.component.ts”。将以下代码复制到文件中:

import {Component, OnInit} from "@angular/core";  
import {Router} from "@angular/router";  
import {Lounge} from "../../viewmodels/lounge";  
import {AppService} from "../../services/app.service";

@Component({
    selector: "lounge-list",
    template: `
            <h2>{{title}}</h2>
            <ul class="items">
                <li *ngFor="let item of items" 
                    [class.selected]="item === selectedItem"
                    (click)="onSelect(item)">
                    <span>{{item.Subject}}</span>
                </li>
            </ul>
    `,
    styles: [`
        ul.items li { 
            cursor: pointer;
        }
        ul.items li:hover { 
            background-color: #E8FAEC; 
        }
    `]
})

export class LoungeListComponent implements OnInit {  
    title: string;
    selectedItem: Lounge;
    items: Lounge[];
    errorMessage: string;

    constructor(private AppService: AppService, private router: Router) { }

    ngOnInit() {
        this.title = "The Lounge";
        var service = this.AppService.getLatestDiscussion();

        service.subscribe(
            items => this.items = items,
            error => this.errorMessage = <any>error
        );
    }

    onSelect(item: Lounge) {
        this.selectedItem = item;
        var link = ['/lounge', this.selectedItem.ID];
        this.router.navigate(link);
    }
}

LoungeListComponent 将显示从 Web API 调用返回的模拟数据中的最新讨论列表。让我们来谈谈我们做了什么:

在文件的顶部,我们导入了所需的 Angular 类:由于我们正在创建一个 Component,我们需要通过引用 @angular/core 来获取 Component 基类,并且我们还需要实现 OnInit 接口,因为我们的组件需要在初始化时执行一些操作。我们引用了 Angular2 Router 以利用客户端导航,因为我们需要它来导航到详细信息组件。我们还引用了我们之前创建的服务,以便与服务器通信以获取一些数据。最后,我们导入了 lounge viewmodel 来存储值。

@component 块是我们为 Component 设置 UI 的地方,包括选择器、模板和样式。请注意,我们使用了一些 Angular2 模板语法来完成工作。具体来说,我们使用了主模板、ngFor 指令、属性绑定和事件绑定。请注意,我们还可以通过定义 templateUrlstyleUrls 将模板和样式分离到单独的文件中。

LoungeListComponent 是用 TypeScript 编写的类。该类包含一些属性,一个使用 DI 实例化 AppServiceRouter 对象的构造函数。它还由将在组件本身中使用的方法组成,特别是 Angular2 模板操作和客户端路由。ngOnInit() 方法是在组件初始化时从服务中获取数据的地方。onSelect() 方法接受一个 Lounge 类型对象作为参数。在这里我们获取选定的项目,并通过使用 Angular2 路由将其传递给详细信息组件。有关 Angular 2 模板的信息,请阅读模板语法

休息室详情组件 (LoungeDetail Component)

在“Scripts/app/components/lounge”中创建另一个 TypeScript 组件,并将文件命名为“lounge-detail.component.ts”。现在将以下内容复制到该文件中:

import {Component, OnInit, OnDestroy} from "@angular/core";  
import {Router, ActivatedRoute} from "@angular/router";  
import {AppService} from "../../services/app.service";  
import {Lounge} from "../../viewmodels/lounge";

@Component({
    selector: "lounge-detail",
    template: `
        <div *ngIf="item" class="item-details">
          <h2>{{item.Subject}} - Detail View</h2>
          <ul>
              <li>
                  <label>Subject:</label>
                  <input [(ngModel)]="item.Subject" placeholder="Insert the title..."/>
              </li>
              <li>
                  <label>Message:</label>
                  <textarea [(ngModel)]="item.Message" placeholder="Insert a message..."></textarea>
              </li>
          </ul>
        </div>

        <div>
               <button (click)='onBack()'>Back to Home</button>
        </div>
    `,
    styles: [`
        .item-details {
            margin: 5px;
            padding: 5px 10px;
            border: 1px solid 9BCCE0;
            background-color: #DDF0D5;
            width: 500px;
        }
        .item-details * {
            vertical-align: middle;
        }
        .item-details ul li {
            padding: 5px 0;
        }
    `]
})

export class LoungeDetailComponent implements OnInit {  
    item: Lounge;
    sub: any;

    constructor(private AppService: AppService, private router: Router, 
                private route: ActivatedRoute) { }

    ngOnInit() {
        this.sub = this.route.params.subscribe(params => {
            var id = +params['id'];
            console.log("selected id " + id);
            this.AppService.getDiscussion(id).subscribe(item => this.item = item[0]);
        });
    }

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

    onBack(): void {
        this.router.navigate(['/home']);
    }
}

就像我们在 lounge-list.component.ts 文件中做的那样,我们正在导入此特定组件所需的内容。在模板中,注意到我们使用了 Angular2 ngIf 指令来隐藏当属性项为 null 或没有与之关联的数据时的 DOM 元素。我们还使用了 Angular2 ngModel 指令来实现文本区域元素的双向数据绑定语法。这简单地意味着,绑定元素中进行的任何更改都将自动更新模型本身,反之亦然。

我们还通过定义 @componentstyles 属性,为我们的 HTML 应用了一些简单的样式,只是为了稍微美化一下我们渲染的标记。

LoungeDetailComponent 类实现了此特定组件的逻辑。我们定义了一些属性,并利用构造函数注入 (DI) 来初始化类实现所需的对象。该类从 ActivatedRoute 服务中的 params observable 获取 id 参数,并使用 AppService 根据 id 获取数据。您应该能够在 ngOnInit() 方法下看到我们是如何做到的。ngOnDestroy() 方法负责清理 params 订阅。最后,我们实现了一个 onBack() 方法,让用户导航回 Home 组件视图。此事件通过 Angular2 事件绑定附加到 HTML 模板中定义的 Button 元素。

引用

请记住,我们将在 app.module.ts 文件中定义组件所需的指令和提供程序,我们很快就会看到。

至此,我们已经完成了基本的 Lounge 主从导航和数据绑定。现在,让我们也通过类似的方式实现“最新消息?”和“最受欢迎旅游地点”主从组件。

地点列表组件 (PlaceList Component)

在“Scripts/app/components/explore”文件夹下创建一个新的 TypeScript 文件,并将文件命名为“place-list.component.ts”。将以下代码复制到文件中:

import {Component, Input, OnInit} from "@angular/core";  
import {Router} from "@angular/router";  
import {Place} from "../../viewmodels/place";  
import {AppService} from "../../services/app.service";

@Component({
    selector: "place-list",
    template: `
            <h2>{{title}}</h2>
            <ul class="items">
                <li *ngFor="let item of items" 
                    (click)="onSelect(item)">
                    <span>{{item.Name}}</span>
                </li>
            </ul>
    `,
    styles: [`
        ul.items li { 
            cursor: pointer;
        }
        ul.items li:hover { 
            background-color: #E8FAEC; 
        }
    `]
})

export class PlaceListComponent implements OnInit {  
    @Input() class: string;
    title: string;
    items: Place[];
    errorMessage: string;

    constructor(private AppService: AppService, private router: Router) { }

    ngOnInit() {

        var service = null;
        switch (this.class) {
            case "latest":
            default:
                this.title = "What's New?";
                service = this.AppService.getLatestEntries();
                break;
            case "most-viewed":
                this.title = "Top Places to Visit";
                service = this.AppService.getMostViewed();
                break;
        }

        service.subscribe(
            items => this.items = items,
            error => this.errorMessage = <any>error
        );
    }

    onSelect(item: Place) {
        var link = ['/explore', item.ID];
        this.router.navigate(link);
    }
}

上述代码的实现与我们的 LoungeListComponent 类似,只是我们在 ngOnInit() 方法中做了一些不同的事情。我们实现了一个 switch 语句,通过传递 class 变量作为参数来确定要在组件中加载哪些数据。class 变量是使用 @Input() 装饰器函数 (@Input() class: string;) 定义的,它将添加所需的元数据,使此属性可用于属性绑定。我们需要这样做是因为我们期望此属性由父组件(Home Component)中的绑定表达式填充。我们这样做是为了使此组件更具可重用性和可维护性,因为“最新消息?”和“最受欢迎旅游地点”组件的数据来自相同的数据源。

地点详情组件 (PlaceDetail Component)

“最新消息?”和“最受欢迎旅游地点”组件的详细信息也将共享相同的详情视图组件。让我们继续创建它。在同一个文件夹中添加一个新的 TypeScript 文件,并将其命名为“place-detail.component.ts”。现在复制以下代码:

import {Component, OnInit, OnDestroy} from "@angular/core";  
import {Router, ActivatedRoute} from "@angular/router";  
import {AppService} from "../../services/app.service";  
import {Place} from "../../viewmodels/place";

@Component({
    selector: "place-detail",
    template: `
        <div *ngIf="item" class="item-details">
          <h2>{{item.Name}} - Detail View</h2>
          <ul>
              <li>
                  <label>Subject:</label>
                  <input [(ngModel)]="item.Name" placeholder="Insert the name..."/>
              </li>
              <li>
                  <label>Message:</label>
                  <textarea [(ngModel)]="item.Location" placeholder="Insert a location..."></textarea>
              </li>
          </ul>
        </div>

        <div>
               <button (click)='onBack()'>Back to Home</button>
        </div>
    `,
    styles: [`
        .item-details {
            margin: 5px;
            padding: 5px 10px;
            border: 1px solid 9BCCE0;
            background-color: #DDF0D5;
            width: 500px;
        }
        .item-details * {
            vertical-align: middle;
        }
        .item-details ul li {
            padding: 5px 0;
        }
    `]
})

export class PlaceDetailComponent implements OnInit {  
    item: Place;
    sub: any;

    constructor(private AppService: AppService, 
                private router: Router, 
                private route: ActivatedRoute) { }

    ngOnInit() {
        this.sub = this.route.params.subscribe(params => {
            var id = +params['id'];
            this.AppService.getPlace(id).subscribe(item => this.item = item[0]);
        });
    }

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

    onBack(): void {
        this.router.navigate(['/home']);
    }
}

上面的代码与我们实现 LoungeDetailComponent 的代码非常相似,因此没有太多可说的。

首页组件 (Home Component)

在“Scripts/app/components/home”文件夹下创建一个新的 TypeScript 文件,并将其命名为“home.component.ts”。将以下代码复制到文件中:

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

@Component({
    selector: "home",
    template: `
        <div class="div-wrapper">
            <div class="div-lounge">
                   <div>
                        <lounge-list class="discussion"></lounge-list>
                   </div>
            </div>
            <div class="div-explore">
                  <div class="top">
                        <place-list class="latest"></place-list>
                  </div>
                  <div class="bot">
                        <place-list class="most-viewed"></place-list>
                  </div>
            </div>
            <div class="div-clear"></div>
        </div>
    `,
    styles: [`
        .div-wrapper {
          margin-right: 300px;
        }
        .div-lounge {
          float: left;
          width: 100%;

        }
        .div-lounge div{
           margin:0 0 10px 0;
           border: 1px solid #9BCCE0;
           background-color: #DDF0D5;
        }
        .div-explore {
          float: right;
          width: 300px;
          margin-right: -300px;
        }
       .div-explore .top{
           margin:0 10px 10px 10px;
           border: 1px solid #9BCCE0;
           background-color: #DDF0D5;
        }
        .div-explore .bot{
           margin:10px 10px 10px 10px;
           border: 1px solid #9BCCE0;
           background-color: #DDF0D5;
        }
        .div-clear {
          clear: both;
        }

    `]
})

export class HomeComponent { }

HomeComponent 将作为我们的主页,用于显示来自各种组件的数据列表。我们添加了元素来显示“最新讨论”。此外,我们添加了元素来显示“最新消息?”和“最受欢迎旅游地点”列表,并使用标准 class 属性来唯一标识它们。元素中定义的 class 属性将用作属性绑定的目标:我们指的是在 PlaceListComponent 中定义的 @Input() 装饰器属性。

我们还使用 class 属性为每个元素唯一地定义了一组样式:正如我们从上面的 Angular2 模板样式部分所看到的。

引用

同样,我们将在 app.module.ts 中定义组件所需的指令和提供程序。

启用客户端路由

目前,我们已经完成了 Angular2 应用程序所需组件的实现,但它存在一些主要问题。我们在主从组件中设置的导航将无法工作,因为我们组件中定义的 URL 路由尚不存在。此外,我们需要配置我们的应用程序以启用客户端路由。现在是时候将图片中的点连接起来,以实现我们期望的结果了。

创建 App.Routes

在“/Scripts/app”文件夹下创建一个新的 TypeScript 文件,并将其命名为“app.routing.ts”。复制以下代码:

import {ModuleWithProviders} from "@angular/core";  
import {Routes, RouterModule} from "@angular/router";

import { HomeComponent } from "./components/home/home.component";  
import { LoungeDetailComponent } from "./components/lounge/lounge-detail.component";  
import { PlaceDetailComponent } from "./components/explore/place-detail.component";

const routes: Routes = [  
    {
        path: '',
        redirectTo: '/home',
        pathMatch: 'full'
    },
    {
        path: 'home',
        component: HomeComponent
    },
    {
        path: 'lounge/:id',
        component: LoungeDetailComponent
    },
    {
        path: 'explore/:id',
        component: PlaceDetailComponent
    }
];

export const AppRoutingProviders: any[] = [  
];

export const AppRouting: ModuleWithProviders = RouterModule.forRoot(routes);  

让我们看看我们刚才做了什么。

就像其他常规组件一样,我们需要导入组件所需的模块/指令。在这种情况下,我们导入了“@angular/router”以使用 Angular 2 路由器接口:具体来说,我们需要通过定义 Routes 并定义一个导出以将路由器添加到我们的引导程序中来使用和声明路由。

Routes 是一个路由定义数组。路由通常由两个主要部分组成:路径和组件。从上面的代码中,我们定义了本系列所需的某些路由。第一条路由指示了我们指向 Home 组件的默认组件。其余路由指向我们之前创建的组件。

修改 AppModule 文件

让我们修改 app.module.ts 文件,使路由可用于需要它的任何组件。我们还将添加我们之前创建的组件所期望的所需指令和提供程序。

这是更新后的 app.module.ts

///<reference path="../../typings/index.d.ts"/>
import {NgModule} from "@angular/core";  
import {BrowserModule} from "@angular/platform-browser";  
import {HttpModule} from "@angular/http";  
import {RouterModule}  from "@angular/router";  
import {FormsModule} from "@angular/forms";  
import "rxjs/Rx";

import {AppComponent} from "./components/app.component";  
import {HomeComponent} from "./components/home/home.component";  
import {LoungeListComponent} from "./components/lounge/lounge-list.component";  
import {LoungeDetailComponent} from "./components/lounge/lounge-detail.component";  
import {PlaceListComponent} from "./components/explore/place-list.component";  
import {PlaceDetailComponent} from "./components/explore/place-detail.component";

import {AppRouting} from "./app.routing";  
import {AppService} from "./services/app.service";

@NgModule({
    // directives, components, and pipes
    declarations: [
        AppComponent,
        HomeComponent,
        LoungeListComponent,
        LoungeDetailComponent,
        PlaceListComponent,
        PlaceDetailComponent
    ],
    // modules
    imports: [
        BrowserModule,
        HttpModule,
        FormsModule,
        RouterModule,
        AppRouting
    ],
    // providers
    providers: [
        AppService
    ],
    bootstrap: [
        AppComponent
    ]
})
export class AppModule { }  

修改应用组件

现在,我们需要向模板添加一个锚标签,当点击时,它会触发导航到组件。为此,打开“app.component.ts”文件并更新代码,使其看起来像这样:

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


@Component({
    selector: "angularjs2demo",
    template: `
        <h1>{{title}}</h1>
        <h3>{{subTitle}}</h3>
            <div class="menu">
                <a class="home" [routerLink]="['/home']">Home</a> |
            </div>
        <router-outlet></router-outlet>
    `
})


export class AppComponent {  
    title = "The Backpackers' Lounge";
    subTitle = "For geeks who want to explore nature beyond limits.";
}

锚标签中的 [routerLink] 绑定告诉路由器当用户点击特定链接时导航到哪里。

添加 base 标签

打开“wwwroot/index.html”并在 <head> 部分的最顶部添加 <base href="/">。我们更新后的 index.html 文件现在应该看起来像这样:

<html>  
<head>  
    <base href="/">
    <title>ASP.NET Core with Angular 2 RC Demo</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Step 1. Load libraries -->
    <!-- Polyfill(s) for older browsers -->
    <script src="js/shim.min.js"></script>
    <script src="js/zone.js"></script>
    <script src="js/Reflect.js"></script>
    <script src="js/system.src.js"></script>

    <!-- Angular2 Native Directives -->
    <script src="/js/moment.js"></script>

    <!-- Step 2. Configure SystemJS -->
    <script src="systemjs.config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>
</head>  
<!-- Step 3. Display the application -->  
<body>  
    <!-- Application PlaceHolder -->
    <angularjs2demo>Please wait...</angularjs2demo>
</body>  
</html>  

我们需要设置 base 标签,因为它将告诉路由引擎如何组合我们的应用程序最终将拥有的所有后续导航 URL。有关详细信息,请参阅:路由器基本 HREF

重写

最后一步是处理 web.config 文件中的重写。我们需要通过在 web.config<system.webServer> 部分添加以下行,告诉 Web 服务器将所有路由 URL(包括根 URL)重写为 index.html 文件:

<rewrite>  
      <rules>
        <rule name="Angular2 pushState routing" stopProcessing="true">
          <match url=".*" />
          <conditions logicalGrouping="MatchAll">
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
            <add input="{REQUEST_FILENAME}" pattern=".*\.[\d\w]+$" negate="true" />
            <add input="{REQUEST_URI}" pattern="^/(api)" negate="true" />
          </conditions>
          <action type="Rewrite" url="/index.html" />
        </rule>
      </rules>
</rewrite>  

通过实现上述规则,我们基本上是要求我们的 Web 服务器将任何传入请求重新寻址到 /index.html 文件,但以下情况除外:

  • 任何现有文件(以保留对实际 .js.css.pdf、图像文件等的引用)
  • 任何现有文件夹(以保留对实际、可能可浏览和/或与 Angular 无关的子文件夹的引用)
  • /api/ 文件夹中的任何内容(以保留对我们的 Web API 控制器的任何调用)。

运行应用程序

现在尝试编译并重新构建您的项目,并确保您的 gulp 任务正在运行。按 F5,您应该能够看到类似以下的输出:

请注意,点击列表中的项目后,URL 会发生变化,并路由到相应的详细信息视图。您还可以看到数据会随着您的输入而自动更改:这就是 Angular2 中双向数据绑定的工作方式。

技巧

如果您的更改未在浏览器中反映,则可能是缓存问题。要解决此问题,请在 web.config 文件中的 <system.webServer> 元素下添加以下配置:

<caching enabled="false"/>  

对于 VStudio 2015 中的 ASP.NET Core 应用程序,配置 TypeScript 有点挑战。tsconfig.json 将不被遵守,因为项目文件 (.xproj) 具有优先权。因此,如果您收到以下 TypeScript 错误或警告:

  • TS1219 对装饰器的实验性支持是未来版本可能会更改的功能。设置“experimentalDecorators”选项以消除此警告。
  • TS2307 找不到模块“@angular/core”。

在 TypeScript 工具改进之前,您可以手动配置 TypeScript:

步骤 1:右键单击项目,然后“卸载项目”。

步骤 2:右键单击卸载的项目,然后编辑 .xproj 文件。

步骤 3:在 Project 节点下添加一个 PropertyGroup 节点。

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">  
    <TypeScriptTarget>ES5</TypeScriptTarget>  
    <TypeScriptJSXEmit>None</TypeScriptJSXEmit>  
    <TypeScriptCompileOnSaveEnabled>True</TypeScriptCompileOnSaveEnabled>  
    <TypeScriptNoImplicitAny>False</TypeScriptNoImplicitAny>  
    <TypeScriptModuleKind>System</TypeScriptModuleKind>  
    <TypeScriptRemoveComments>False</TypeScriptRemoveComments>  
    <TypeScriptOutFile />  
    <TypeScriptOutDir />  
    <TypeScriptGeneratesDeclarations>False</TypeScriptGeneratesDeclarations>  
    <TypeScriptNoEmitOnError>True</TypeScriptNoEmitOnError>  
    <TypeScriptSourceMap>True</TypeScriptSourceMap>  
    <TypeScriptMapRoot />  
    <TypeScriptSourceRoot />  
    <TypeScriptExperimentalDecorators>True</TypeScriptExperimentalDecorators>  
    <TypeScriptEmitDecoratorMetadata>True</TypeScriptEmitDecoratorMetadata>  
</PropertyGroup> 

步骤 4:右键单击卸载的项目并“重新加载项目”。

步骤 5:重新构建项目。

摘要

在本系列的这一部分中,我们学到了很多东西,从升级到 Angular2 RC6 到在 ASP.NET Core 环境中从头开始创建数据驱动的 Angular2 应用程序。我们学习了如何在 Angular2 应用程序中创建和与 Web API 通信。我们还学习了 Angular2 主从实现、双向数据绑定和路由的基础知识。

您可以在我的 github 仓库中查看源代码:https://github.com/proudmonkey/TheBackpackerLounge

敬请期待我的下一篇文章。 :)

© . All rights reserved.