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

Angular 1.4.8 TypeScript 强类型

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2017 年 1 月 2 日

CPOL

19分钟阅读

viewsIcon

23528

downloadIcon

199

本文介绍如何在 AngularJs 框架中使用 TypeScript 编写强类型客户端脚本。

引言

Angular JS 是目前最流行的客户端框架,用于构建响应式、快速且可测试的 Web 应用程序。它允许按照 MVC(模型-视图-控制器)模式编写复杂的代码。已经有许多 Angular Js 的插件和库,并且 DI(依赖注入)模式允许快速集成这些库,因此应用程序的开发非常便捷。

TypeScript 是微软开发的一种语言,用于编写可以编译成 JavaScript (EcmaScript 5) 代码的强类型代码。它借鉴了 Java 或 C# 等现代面向对象语言的许多思想。通过编译过程,生成的结果代码可以在所有现代浏览器中成功使用。

背景

最近,我不得不编写一个客户端 Angular Js 应用程序。
由于我主要从事后端开发,习惯于编写强类型代码,并且不喜欢使用纯 JavaScript 编写代码。

我知道 TypeScript 的存在。我之前使用过它,但那是与 Knockout Js 框架一起使用的。

主要问题是:如何集成 Angular Js 和 TypeScript,以编写我习惯于在后端编写的那种优秀且高质量的代码?

有很多关于如何使用 Angular Js 的教程。也有很多关于 TypeScript 的文档。但很难找到如何将它们混合使用的方法。

最终,我做到了,并且我认为结果还不错,所以我想简要分享我的经验。

Using the Code

环境设置

首先,要开始这段旅程,需要安装这些应用程序

有很多关于如何正确配置 Node 环境的教程,所以我在这里不再赘述。希望你能顺利完成,并且命令行中可以使用“npm”命令,因为稍后会用到它。

安装完成后,在项目文件夹(例如 *C:/Projects/NewsApp*)中,运行 npm init 命令并按照说明进行操作(设置应用程序名称、版本、描述、作者等)

npm init

命令执行后,会创建一个 package.json 文件。它定义了应用程序及其依赖项。实际上,它有点简短,但随着项目的复杂性增加,该文件也会变大。

现在是时候安装 bower (https://bower.io) 了,这是一个 JavaScript 库的包管理器

npm install bower -g

良好实践

目前,npm 也允许安装客户端库,如 JQuery 或 Angular JS。许多人更喜欢只使用 npm,但我认为将客户端库和服务器端库分开,并相应地使用 bower 和 npm 是一个好习惯。不过,是否使用 Bower 完全取决于开发人员。:)

现在,可以通过在命令行中输入以下命令来初始化 bower 包

bower init

指定项目属性后,应该会创建一个 bower.json 文件。

借助 package.jsonbower.json 文件,应用程序所需的所有包都不必存储在远程仓库中。恢复这些包就像在命令行中执行以下命令一样简单。

npm install
bower install

执行这些操作将恢复 package.jsonbower.json 中定义的所有包(**请注意**,全局安装的包,如 bower,将不会恢复)。

为了拥有准备好的开发环境,必须安装 Angular。

所以,在命令行中执行

bower install angular@1.4.8 --save

--save 属性会将 angular 依赖项保存在 bower.json 文件中。

另一个必要的操作是安装 Typings 管理器 (https://github.com/typings/typings)。在命令行中执行

npm install typings --global

现在我们就可以访问 typings CLI 了。要检查 typings 是否存在,请执行此命令

typings search angular

由于有很多与 Angular 相关的库,因此可以通过精确名称过滤结果。

typings search --name angular

最后,还有一个用于 Angular 的 typings 包。安装这个包应该非常简单,只需执行 **typings install angular**?不幸的是,不行。:(

因为 Angular 的 typings 在旧的仓库中,所以需要将源添加到路径中,那么它会是这样的:typings install dt~angular?不幸的是,不行。:(

在本例中,我们使用 Angular 版本 1.4.8。所以我们必须修改命令,typings install dt~angular@1.4 会起作用吗?仍然不行。该版本已弃用,所以我们必须添加 --global 才能使其正常工作。

最后,有效的命令是

typings install dt~angular@1.4 --global

现在,在 NewsApp 文件夹中,会创建一个“typings”文件夹,其中有一个 Angular 文件夹,里面有一个 index.d.t.s 文件。所有必需的定义都在这个文件中。

**就这样!**所有(当然不是全部,但目前为止)必需的组件都已安装,让我们开始吧!:)

Angular 应用程序的 Hello World

现在,让我们开始编码。

首先,让我们添加一个 index.html 文件,其中包含一些基本的 HTML 标签。

<!DOCTYPE html>
<html>
    <head>
        <meta name="description" content="News portal example with Angular JS.">
        <title>
            News Portal greets you!
        </title>
    </head>
    <body>
        <header>
            <h1>
                Hello world!
            </h1>
        </header>
    </body>
</html>

网站正在运行,但没有什么特别之处。所以让我们添加 JavaScript 部分,从 app.js 文件开始。

var app = angular.module("NewsApp", []);

app.controller("NewsController", ["$scope", function($scope){
    $scope.helloWorld = "Hello from AngularJS world!";
}]);

并更新 index.html 文件

<!DOCTYPE html>
<html>
    <head>
        <meta name="description" content="News portal example with Angular JS.">
        <title>
            News Portal greets you!
        </title>
    </head>
    <body ng-app="NewsApp">
        <section ng-controller="NewsController">
            <header>
                <h1 ng-bind="helloWorld"></h1>
            </header>
        </section>
    </body>
    <script type="text/javascript" src="app.js"></script>
</html>

好吧,什么都没发生,Hello World 没有显示。控制台中有一个错误:“angular is not defined”。是的,我们还没有引用 angular 源。在 app.js 脚本之前,添加对 angular 的引用

<script type="text/javascript" src="bower_components/angular/angular.js"></script>

耶,它奏效了!:)

但这开始看起来像无数的 AngularJs 教程。

让我们做得更… 吧。

Definitely Typed AngularJs

首先,让我们将 app.js 重命名为 app.ts 现在 IDE(至少是 Visual Studio Code)会给 angular 关键字下划线。要解决这个问题,让我们添加对 angular 定义的引用。我过去是这样做的...

/// <reference path="typings/index.d.ts" />

...作为文件中的第一行(就像在 C# 和 Java 等后端语言中一样)。

现在代码是有效的,没有下划线。将鼠标悬停在 angular 或 controller 关键字上会显示组件的类型。我们甚至可以通过在特定组件上按 F12 来导航到这些组件(就像在 Visual Studio 中处理后端应用程序时一样)。

所以让我们做一些清理。在项目文件夹(例如 NewsApp)中,让我们创建 scriptsdist 目录。
scripts 目录中,让我们创建一个 controllers 文件夹。

现在让我们创建 newsController.ts 文件,并将 NewsController 实现移到这里

function newsController($scope){
    $scope.helloWorld = "Hello from AngularJS world!";
};

app.ts 文件移动到 scripts 目录,只保留控制器的注册

/// <reference path="typings/index.d.ts" />

var app = angular.module("NewsApp", []);
app.controller("NewsController", ["$scope", newsController]);

很好,但还不够好。

让我们使 NewsController 更加强类型,并使 app.ts 不知道 NewsController 的任何依赖项(SOLID 原则中的开放-封闭原则)。

更改后,app.ts 看起来很简单

/// <reference path="../typings/index.d.ts" />
/// <reference path="controllers/newsController.ts" />

var app = angular.module("NewsApp", []);
app.controller("NewsController", NewsApp.NewsController);

NewsController 的实现看起来更像每个后端开发人员都熟悉的强类型代码

module NewsApp
{
    export class NewsController
    {
        public static $inject = ["$scope"];

        constructor($scope: any){
            $scope.helloWorld = "Hello from AngularJS world!";
        }
    }
}

唯一有点奇怪的是 public static field $inject。不幸的是,Angular 需要它来解析依赖项并通过控制器的构造函数注册控制器。在 $inject 字段中,指定了注入到控制器的所有依赖项。
$inject 字段还有一些其他替代实现(例如,在注释中),但我习惯于这个。:)

在上面的示例中,还添加了 module 关键字。将类声明在特定模块中是一个好习惯,可以使代码更清晰。

一切正常,代码没有下划线,所以它可能工作。它可能工作,但现在应用程序无法运行,因为代码是用 typescript 语言编写的,浏览器无法编译它。

现在是时候安装可以编译它的工具了。在本例中,我们使用 gulp,因为它非常简单且高度可配置。还有其他工具,如 grunt 或 webpack,但对于本例,gulp 就足够了。:)

在命令行中执行

npm install gulp --save

这会将 gulp 工具作为项目依赖项安装。要运行 gulp,请在项目目录中添加 gulpfile.js,并附带一个示例以检查它是否正常工作

var gulp = require('gulp');

gulp.task('default', function(){
    console.log('Works!');
});

在命令行中输入简单的

gulp

如果控制台中显示“works!”,那么它就工作了。:)

现在为了让它真正做些事情,让我们添加其他依赖项

npm install gulp-typescript --save
npm install gulp-concat --save
npm install gulp-sourcemaps --save
npm install typescript --save

让我们更改 gulp 文件来编译 typescript 文件并创建一个输出文件 sources.js

var gulp = require('gulp');
var ts = require('gulp-typescript');

gulp.task('typescript', function(){
    return gulp.src('scripts/**/*.ts')
        .pipe(ts({
            declaration: false,
            out: 'sources.js',
            target: 'ES5'
        }))
        .pipe(gulp.dest('dist'));
})

gulp.task('watch', function(){
    gulp.watch('scripts/**/*.ts', ['typescript']);
})

gulp.task('default', ['typescript', 'watch']);

现在运行...

gulp

...在命令行中执行此命令,应该会编译 typescript 文件并在 dist 文件夹中创建 JavaScript 文件结构。此外,还有一个 sources.js 文件,其中包含了所有文件的连接。

使用 typescript 和文件中的“references”的另一个好处是正确的文件结构。这意味着所有依赖的文件都会在使用它们类型的类型之前进行编译和声明。很神奇,不是吗?:)

现在,让我们更新 index.html 文件

<script type="text/javascript" src="bower_components/angular/angular.js"></script>
<script type="text/javascript" src="dist/sources.js"></script>

应用程序就可以工作了!:)

良好实践

gulp 中尽可能自动化是一项好习惯。因此,在这种情况下,可以更新 gulpfile 为所有源文件(typescript 文件)添加 sourcemaps,并且所有库都可以连接成一个文件,这样 index.html 文件就不必更新了(将所有源文件包含在一个文件中也有性能优势)。

更改后,gulpfile 看起来像这样

var gulp = require('gulp');
var ts = require('gulp-typescript');
var sourcemaps = require('gulp-sourcemaps');
var concat = require('gulp-concat');
var lib = require('bower-files')();

gulp.task('typescript', function(){
    return gulp.src('scripts/**/*.ts')
        .pipe(sourcemaps.init())
        .pipe(ts({
            declaration: false,
            out: 'sources.js',
            target: 'ES5'
        }))
        .pipe(sourcemaps.write())
        .pipe(gulp.dest('dist'));
});

gulp.task('libs', function(){
    return gulp.src(lib.ext('js').files)
        .pipe(concat('libs.js'))
        .pipe(gulp.dest('dist'));
})

gulp.task('watch', function(){
    gulp.watch('scripts/**/*.ts', ['typescript']);
})

gulp.task('default', ['libs', 'typescript', 'watch']);

现在,index.html 文件中最后一个小改动

<script type="text/javascript" src="dist/libs.js"></script>
<script type="text/javascript" src="dist/sources.js"></script>

服务、指令和其他

好的,环境已经准备就绪。让我们通过添加 NewsService 来改进应用程序,该服务从 WebApi 获取新闻。

为此,请在 scripts 目录中创建一个名为“services”的目录。在 services 目录中,添加 newsService.ts 文件

/// <reference path="../../typings/index.d.ts" />

module NewsModule{
    export class NewsService{
        public static $inject = ['$http'];

        constructor(private $http: ng.IHttpService) {
        }

        public GetAllNewses(): ng.IHttpPromise<string>{
            return this.$http.get("https://jsonplaceholder.typicode.com/posts");
        }
    }
}

NewsService 会将 $http 服务注入构造函数,并且与控制器一样,它有一个 public static $inject 字段,其中定义了依赖项。在本例中,我将使用 jsonplaceholder 网站获取一些存根的帖子并将它们显示为新闻。
构造函数参数 $http 中的 private 关键字会将该变量定义为类字段,因此无需显式初始化。

为了完全强类型,$http 服务被定义为 Angular typings 中的 ng.IHttpService(引用 typings/index.d.ts)。

app.ts 文件中,必须注册 NewsService

app.service("NewsService", NewsModule.NewsService);

如果 IDE 无法识别 NewsService 类型,则必须在上方添加对该文件的引用。

好的,gulp 工作正常,一切编译。但是 GET 请求的结果是一个 JSON,而 GetAllNewses 方法的结果是一个 string 的 Promise。这看起来不太好。让我们添加 NewsModel 并从服务返回强类型结果。

scripts 目录中,添加 models 目录,并在其中添加 newsModel.ts 文件。

module NewsModule{
    export class NewsModel{
        public id: number;
        public title: string;
        public body: string;
    }
}

目前,这个模型还可以。

现在让我们更改 GetAllNewses 方法以使用定义的模型

public GetAllNewses(): ng.IHttpPromise<NewsModel[]>{
    return this.$http.get<NewsModel[]>("https://jsonplaceholder.typicode.com/posts");
}

有了这样的实现,我们就可以在 NewsController 中使用 NewsService 了。

module NewsModule {
    export class NewsController {
        public static $inject = ["$scope", "NewsService"];

        constructor(private $scope: any, private newsService: NewsService){
            $scope.helloWorld = "Hello from AngularJS world!";
            this.LoadNewses();
        }

        private LoadNewses(){
            this.newsService.GetAllNewses()
                .then(success => {
                    this.$scope.newsList = success.data;
                }, error => {
                    console.log(error);
                });
        }
    }
}

为了在应用程序中看到结果,index.html 需要稍作修改

<body ng-app="NewsApp">
    <section ng-controller="NewsController">
        <header>
            <h1 ng-bind="helloWorld"></h1>
        </header>
        <ul>
            <li ng-repeat="news in newsList">
                <span ng-bind="news.title"></span>
            </li>
        </ul>
    </section>
</body>

耶,这么多结果… :) 代码中的小修改,我们就显示了最后二十条新闻。:)

<li ng-repeat="news in newsList | orderBy: '-id' | limitTo: 20">
    <div ng-if="$index === 0">
        <h3 ng-bind="news.title"></h3>
        <p ng-bind="news.body"></p>
    </div>
    <span ng-if="$index > 0" ng-bind="news.title"></span>
</li>

好的,但是 NewsController 有点丑陋。它使用了类型为 any 的 $scope。让我们修复它。

Angular 允许在视图中使用 Controller AS nameController 语法来注册控制器。

因此,Controller 就是 $scope,并且将任何内容注册为控制器属性/字段或方法都会使其在视图中可见。

让我们先更改视图

<section ng-controller="NewsController as News">
    <header>
        <h1 ng-bind="News.helloWorld"></h1>
    </header>
    <ul>
        <li ng-repeat="news in News.newsList | orderBy: '-id' | limitTo: 20">
            <div ng-if="$index === 0">
                <h3 ng-bind="news.title"></h3>
                <p ng-bind="news.body"></p>
            </div>
            <span ng-if="$index > 0" ng-bind="news.title"></span>
        </li>
    </ul>
</section>

现在是 NewsController

module NewsModule {
    export class NewsController {
        public static $inject = ["NewsService"];

        public helloWorld: string;
        public newsList: NewsModel[];

        constructor(private newsService: NewsService){
            this.helloWorld = "Hello from AngularJS world!";
            this.LoadNewses();
        }

        private LoadNewses(){
            this.newsService.GetAllNewses()
                .then(success => {
                    this.newsList = success.data;
                }, error => {
                    console.log(error);
                });
        }
    }
}

如您所见,$scope 已完全删除。取而代之的是,所有属性都在 NewsController 类中定义,并且可以通过 this 关键字访问。

现在让我们创建一个指令。例如,我们可以创建一个指令,为给定的 UserId 返回完整的用户名。我知道在现实生活中,应该有一个 UserService 来获取给定 UserName 的用户详细信息,但在这个示例中,我们可以只创建用户名的存根。

将指令视为可重用组件。不是“我有一个 ng-repeat,让我们创建一个指令”,而是“我需要在几个不同的视图中使用它”。我最喜欢的指令用法之一是将 Enum 类型转换为显示的 stringFullUserName 指令是一个类似的示例,但它基于 userId 而不是 Enum 类型。

scripts 目录中,创建一个 directives 目录,并在其中添加文件 fullUserNameDirective.ts

/// <reference path="../../typings/index.d.ts" />

module NewsModule{
    interface IFullUserNameScope extends ng.IScope{
        userId: number;
        firstName: string;
        lastName: string;
    }

    export class FullUserNameDirective implements ng.IDirective{
        public restrict = "E";
        public replace = true;
        public scope = {
            userId: "="
        };

        public template = "<span>{{ firstName }} {{ lastName }}</span>";

        constructor() {
        }

        public link = (scope: IFullUserNameScope, 
                       element: ng.IAugmentedJQuery, attrs: ng.IAttributes) => {
            var userDetails = this.GetUserDetails(scope.userId);
            scope.firstName = userDetails[0];
            scope.lastName = userDetails[1];
        };

        private GetUserDetails(userId: number): string[]{
            switch(userId){
                case 0:
                    return ["Gidget", "Thomson"];
                case 1:
                    return ["Emilia", "Cornell"];
                case 2:
                    return ["Cyril", "Wallen"];
                case 3:
                    return ["Andre", "Gunderson"];
                case 4:
                    return ["Joi", "Kruse"];
                case 5:
                    return ["Alexander", "Leon"];
                case 6:
                    return ["Emile", "Decker"];
                case 7:
                    return ["Idell", "Rosenberg"];
                case 8:
                    return ["William", "Bower"];
                case 9:
                    return ["Deb", "Royal"];
                case 10:
                    return ["Trista", "Grubb"];
            }
        }

        public static Factory(): FullUserNameDirective{
            return new FullUserNameDirective();
        }
    }
}

好的,这需要一点解释。

FullUserNameDirective 实现 IDirective 接口。如果您查看此接口,您会发现所有属性都是可选的,因此实际上不必实现此接口。在本例中,我添加了它,以便可以轻松地看到允许的指令属性。

然后有一些 Angular 特定的属性(由 IDirective 接口实现)

  • Restrict 是指令的用法 - “E” 代表 Element
  • Replace 意味着原始指令将被模板字段中的元素替换
  • Scope 定义了传递给指令的值,在本例中,它是必需的 userId 属性
  • Template 是将在视图中渲染的结果 Html

在这种情况下,我们不需要将任何特定的服务注入指令,因此构造函数是空的。

真正重要(也是我长期以来一直为此苦恼)的是 link 方法。

当指令被渲染时,这个方法就会被执行。它的工作方式类似于构造函数,但在方法参数中,有通过作用域传递的值和其他有用的属性。

scope 元素可以是 any 类型,但在 TypeScript 中这并非最佳实践,因此我们定义了一个专用的接口 IFullUserNameScope,它扩展了 IScope。扩展 IScope 不是必需的,但在某些情况下,访问基本作用域属性或方法可能很有用。

最后,有一个 public static Factory 方法,它返回 FullUserNameDirective。指令的关键事实是,同一指令可以在一个视图中显示多次(例如,在 ng-repeat 中)。每个指令都应该有隔离的作用域,因为它是一个独立的元素。因此,Angular 执行 Factory 方法来初始化每个指令,而不是像控制器或其他服务中的构造函数那样。

Factory 方法还允许将初始化封装在指令中,因此在初始化时无需知道指令中使用了哪些依赖项。初始化非常简单,如下所示:

app.directive("fullUserName", NewsModule.FullUserNameDirective.Factory);

请注意,在这种情况下,指令的名称是用 PascalCase(“fullUserName”)写的。Angular 会自动将 PascalCase 转换为用连字符分隔的单词(kebab-case?),这在 HTML 中是常见的命名约定。因此,指令名称的首字母是小写的,以便符合约定。

最后,在视图中使用该指令

<li ng-repeat="news in News.newsList | orderBy: '-id' | limitTo: 20">
    <full-user-name user-id="news.userId"></full-user-name>
    <div ng-if="$index === 0">
        <h3 ng-bind="news.title"></h3>
        <p ng-bind="news.body"></p>
    </div>
    <span ng-if="$index > 0" ng-bind="news.title"></span>
</li>

请注意,userId 参数也被转换为符合 kebab-case 约定,并引用为 user-id

最后,让我们创建 UserModel 并将 GetUserDetails 方法移到 UserService,这样指令就会更简洁一些。

module NewsModule{
    export class UserModel{
        public id: number;
        public firstName: string;
        public lastName: string;

        constructor(id: number, firstName: string, lastName: string) {
            this.id = id;
            this.firstName = firstName;
            this.lastName = lastName;
        }
    }
}
/// <reference path="../models/userModel.ts" />

module NewsModule{
    export class UserService{
        public GetUserDetails(userId: number): UserModel{
            switch(userId){
                case 0:
                    return new UserModel(userId, "Gidget", "Thomson");
                case 1:
                    return new UserModel(userId, "Emilia", "Cornell");
                case 2:
                    return new UserModel(userId, "Cyril", "Wallen");
                case 3:
                    return new UserModel(userId, "Andre", "Gunderson");
                case 4:
                    return new UserModel(userId, "Joi", "Kruse");
                case 5:
                    return new UserModel(userId, "Alexander", "Leon");
                case 6:
                    return new UserModel(userId, "Emile", "Decker");
                case 7:
                    return new UserModel(userId, "Idell", "Rosenberg");
                case 8:
                    return new UserModel(userId, "William", "Bower");
                case 9:
                    return new UserModel(userId, "Deb", "Royal");
                case 10:
                    return new UserModel(userId, "Trista", "Grubb");
            }
        }
    }
}
/// <reference path="../../typings/index.d.ts" />
/// <reference path="../services/userService.ts" />
/// <reference path="../models/userModel.ts" />

module NewsModule{
    interface IFullUserNameScope{
        userId: number;
        firstName: string;
        lastName: string;
    }

    export class FullUserNameDirective implements ng.IDirective{
        public restrict = "E";
        public replace = true;
        public scope = {
            userId: "="
        };

        public template = "<span>{{ firstName }} {{ lastName }}</span>";

        constructor(private userService: UserService) {
        }

        public link = (scope: IFullUserNameScope, 
                       element: ng.IAugmentedJQuery, attrs: ng.IAttributes) => {
            var userDetails = this.userService.GetUserDetails(scope.userId);
            scope.firstName = userDetails.firstName;
            scope.lastName = userDetails.lastName;
        };

        public static GetFactory(): (userService: UserService) => FullUserNameDirective{
            var factory = (userService: UserService) => new FullUserNameDirective(userService);
            factory.$inject = ["UserService"];

            return factory;
        }
    }
}

上例中唯一需要解释的是 FullUserNameDirective 的变化以及从 Factory 方法演变而来的 GetFactory 方法。

现在指令需要 UserService,Angular 必须将其注入指令。必须指定所有依赖项,以便 Angular 可以解析它们并在创建 FullUserNameDirectives 的新对象时注入它们。因此,而不是 Factory 方法,有一个 lambda 表达式定义了 FullUserNameDirective 的创建。然后,将 $inject 属性分配给 lambda 表达式,其中包含所有必需的依赖项。因此,lambda 表达式实际上是由 GetFactory 方法返回的 factory 方法。

因此,所有依赖项信息都封装在指令中,因此指令实现的任何更改都不会导致指令外部的任何更改。

app.ts 文件中的组件注册仍然非常简单

app.service("UserService", NewsModule.UserService);

app.directive("fullUserName", NewsModule.FullUserNameDirective.GetFactory());

路由和视图

现在客户端应用程序看起来不错,让我们改进视图。

目前只有一个视图文件 - index.html,它负责许多事情并且非常复杂。

是时候引入路由了。

要安装 ui-router,请在命令行中执行

bower install angular-ui-router --save

要获取类型定义,请在命令行中执行

typings install github:DefinitelyTyped/DefinitelyTyped/
angular-ui-router/angular-ui-router.d.ts#47310a628aa6d2d7e58ac0463a6a2e0918954d9e

不幸的是,该应用程序基于有点旧版本的库(angularui-router),所以要避免编译错误,我建议使用这个版本。

如果 gulp 在重启后工作正常,让我们引入一些视图和路由。

在项目目录中,添加 views 目录,然后添加 news 目录。在 news 目录中,创建 newsList.html

<section>
    <header>
        <h1 ng-bind="News.helloWorld"></h1>
    </header>
    <ul>
        <li ng-repeat="news in News.newsList | orderBy: '-id' | limitTo: 20">
            <full-user-name user-id="news.userId"></full-user-name>
            <div ng-if="$index === 0">
                <h3 ng-bind="news.title"></h3>
                <p ng-bind="news.body"></p>
            </div>
            <span ng-if="$index > 0" ng-bind="news.title"></span>
            <input type="button" text="Show details" ui-sref="news.details({id: news.Id})"/>
        </li>
    </ul>
</section>

基本上,NewsController 已从 index.html 移到 newsList.html,但没有声明 ng-controller。这将在路由文件中设置。

还有一个“显示详细信息”按钮,其中包含 ui-sref 指令。此指令负责重定向到另一个状态。

如上所述,ui-router 使用状态在应用程序内的视图之间导航。State 是一种视图,但具有定义好的路由和参数。

让我们在 scripts 目录中创建一个 routes.ts 文件

/// <reference path="../typings/index.d.ts" />

module NewsModule{
    export class RouteConfig{
        public static $inject = ['$stateProvider', '$urlRouterProvider'];

        constructor($stateProvider: ng.ui.IStateProvider, $urlRouterProvider: ng.ui.IUrlRouterProvider) {
            $stateProvider.state('news', {
                url: "/news",
                templateUrl: "views/news/newsList.html",
                controller: "NewsController as News"
            });

            $urlRouterProvider.otherwise("/news");
        }
    }
}

这个类负责路由定义。$stateProvider$urlRouterProviderui-router 的组件,允许在应用程序中注册状态和路由。

我们已声明 news 状态,包括其 url、模板 html 的路径(在 templateUrl 中)和控制器。

urlRouterProviderotherwise 方法会在路由不正确且状态未识别时重定向到默认状态。

app.ts 中,有必要将 RouteConfig 注册为 Angular 配置。

app.config(NewsModule.RouteConfig);

最后,为了让我们的路由工作,index.html 文件中需要 <ui-view/> 指令

<body ng-app="NewsApp">
    <ui-view/>
</body>

Ui-router 会用当前状态的模板替换 <ui-view/> 元素。它还会解析模板指定的控制器。

可以在模板中嵌套 <ui-view/>,并且可以将某些状态设为“abstract”,因为它无法导航到(例如父视图)。

跨域请求错误

当应用程序使用 ui-router 在视图之间导航时,可能会出现跨域请求错误,因为 ui-router 通过 xhr 请求获取模板。如果您将应用程序作为文件查看(例如 file:///C:/Projects/NewsApp/index.html#/),就会发生错误。要处理此错误,有必要在服务器上托管该应用程序。您可以使用 IIS、tomcat 或 NodeJs 的轻量级库,如 http-server。要安装 http-server,请在命令行中执行

npm install http-server -g

安装后,在特定端口上运行服务器(不是必需的,但我建议指定端口)

http-server -p 666

现在应用程序可在 https://:666/#/news URL 访问。

请注意,当输入错误的地址(例如 https://:666 或 https://:666/#/test)时,它会立即重定向到默认状态 - news。

好的,如果应用程序现在正在运行,让我们添加新的状态 - news 详细信息。

views/news/ 目录中创建 newsDetails.html 文件

<section>
    <header>
        <h1 ng-bind="NewsDetails.details.title"></h1>
    </header>
    <p ng-bind="NewsDetails.details.body"></p>
    <span>This post was created by <full-user-name user-id="NewsDetails.details.userId">
    </full-user-name></span>
    <input type="button" value="Close" ui-sref="news"/>
</section>

现在在 newsDetailsController.ts 文件中添加新的控制器 NewsDetailsController

module NewsModule {
    export class NewsDetailsController {
        public static $inject = ["NewsService"];

        public details: NewsModel;

        constructor(private newsService: NewsService){
            this.LoadNewsDetails();
        }

        private LoadNewsDetails(){
            this.newsService.GetNewsById(1)
                .then(success => {
                    this.details = success.data;
                }, error => {
                    console.log(error);
                });
        }
    }
}

app.ts 中注册 Controller

app.controller("NewsDetailsController", NewsModule.NewsDetailsController);

routes.ts 中注册 state

.state('news.details', {
    url: "/details/{id}",
    templateUrl: "views/news/newsDetails.html",
    controller: "NewsDetailsController as NewsDetails"
});

请注意,state 名称是绝对的,它表示 details statenews state 的子级。另一方面,url 是相对的,只是在路径后面添加了“/details/{id}”,但最终,url 将结合所有父级,因此它将是“#/news/details/{id}”。

另一件事是 {id} 参数。这是 news 详细信息的 id。这样给定,它可以在 NewsDetailsController 中检索。

为了使其完全工作,newList.html 的末尾需要添加一行代码

<ui-view></ui-view>

现在点击“显示详细信息”按钮时,新闻详细信息将显示在新闻列表下方。父视图中的 ui-view 元素是子视图的容器。

但是 FullUserNameDirective 有一个问题,因为它在 NewsDetails 中不显示任何内容。

问题在于,获取用户详细信息是在 link 方法中完成的。link 方法在指令绘制时执行。但是用户 id 是通过 Ajax 从 WebApi 获取的,需要一些延迟才能检索。

要解决这个问题,userId 参数必须是“可观察的”,并且当它改变时在指令中触发一个事件。

FullUserNameDirective 的 link 方法的这个实现为 userId 参数添加了一个观察者

public link = (scope: IFullUserNameScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes) => {
    scope.$watch(() => scope.userId, 
           (newValue: number, oldValue: number, scope: IFullUserNameScope) => {
        var user = this.userService.GetUserDetails(newValue);
        scope.firstName = user.firstName;
        scope.lastName = user.lastName;
    });
};

userId 参数值改变时,$watch 方法会被执行。新值、旧值和作用域会传递给回调方法,因此我们可以轻松检测更改并更新作用域参数。

当指令初始化时,newValue 可能为 undefined。让我们更新 UserService 以使其对这种情况具有鲁棒性,方法是在 switch 中添加一个 default

default:
    return new UserModel();

UserModel 的构造函数也需要一个小小的改变

constructor(id?: number, firstName = "", lastName = "") {
    this.id = id;
    this.firstName = firstName;
    this.lastName = lastName;
}

现在 User 的全名在 NewsListNewsDetails 中正确显示。

但是这个应用程序还有另一个 bug。NewsDetails 始终是从 WebApi 获取 id=1。让我们从路由器获取 id 参数并将其传递给 NewsServiceNewsDetailsController 中的更改

export class NewsDetailsController {
    public static $inject = ["NewsService", "$stateParams"];

    public details: NewsModel;

    private id: number;

    constructor(private newsService: NewsService, $stateParams: ng.ui.IStateParamsService){
        this.id = $stateParams["id"];
        this.LoadNewsDetails();
    }

    private LoadNewsDetails(){
        this.newsService.GetNewsById(this.id)
            .then(success => {
                this.details = success.data;
            }, error => {
                console.log(error);
            });
    }
}

stateParams 服务包含状态参数作为键值字典。我们可以通过其名称(在 routes.ts 文件中定义)获取 id 参数,然后将其传递给 NewsServiceLoadNewsDetails

newsList.htmlNewsList 视图也需要在末尾添加一行代码

<input type="button" value="Show details" ui-sref="news.details({id: news.id})"/>

现在所选新闻的正确详细信息显示在新闻详细信息部分。

但说实话,我不太喜欢这样。我希望新闻详细信息能像一个新页面一样显示,而不是列表下方的一个部分。

为了解决这个问题,新闻状态将是父级(现在是主状态),news.list 状态将是 news 的子级,而 news.details 将是 news 状态的另一个子级。这需要在 routes.ts 文件中进行一些小改动

$stateProvider.state('news', {
    url: "/news",
    template: "<ui-view/>",
    abstract: true
})
.state('news.list', {
    url: "",
    templateUrl: "views/news/newsList.html",
    controller: "NewsController as News"
})
.state('news.details', {
    url: "/details/{id}",
    templateUrl: "views/news/newsDetails.html",
    controller: "NewsDetailsController as NewsDetails"
});

news.list 的 url 为空,因此当进入 #/news url 时,news.list 状态将被触发,因为 news 状态被标记为 abstract

news 状态的模板只是 <ui-view/> 以渲染子项。

新闻详细信息视图中的 **关闭** 按钮也需要进行一些小改动,因为 news 状态是 abstract 的,无法重定向到

<input type="button" value="Back" ui-sref="news.list"/>

可能的改进

就是这样!一个功能齐全的应用程序,包含路由、模板视图、指令和 WebApi 服务使用已经完成。最有价值的是,整个应用程序都用强类型的 TypeScript 编写。

应用程序看起来不太好。可以通过一些 Bootstrap 样式、Angular UI-Bootstrap 指令和等进行改进。然后所有 CSS 文件都可以通过 gulp 任务进行连接和最小化。

但这并不是本教程的重点,我将把它留给您作为家庭作业。:)

创建的项目已附加到文章中,请随意下载和测试。

希望您喜欢这篇文章和教程。:)

关注点

不幸的是,从我创建项目到现在,Angular 已经发布了许多新版本。因此,需要指定 angular 和所有组件的版本,以便它们能够协同工作。

© . All rights reserved.