使用ngUpgrade将您的AngularJS服务迁移到Angular

来自菜鸟教程
跳转至:导航、​搜索

在我们的 最新指南 中,我们介绍了如何安装开始从 AngularJS 升级到 Angular 所需的一切。 我们还介绍了如何重写和降级组件。

在本指南中,您将使用 ngUpgrade 项目中的服务。 具体来说,您将:

  • 将 AngularJS 服务重写为 Angular
  • 将 observable 转换为 Promise
  • 降级服务,使其仍可与我们的 AngularJS 代码一起使用
  • 将 promise 转换为 observable

我们的起点

花点时间在 GitHub 上克隆或分叉这个 示例项目 (不要忘记在 publicserver 文件夹中运行 npm install)。 签出 this commit 以查看我们的起点:

git checkout 083ee533d44c05db003413186fbef41f76466976

我们有一个订单系统项目,我们可以使用它来通过 ngUpgrade 工作。 它使用组件架构、TypeScript 和 Webpack(用于开发和生产的构建)。 我们还设置并引导了 Angular 和 ngUpgrade,并且 home 组件已被重写为 Angular。

(如果您对此感到迷茫,我们将在综合视频课程 Upgrading AngularJS 中涵盖所有内容。)

一个简短的说明: Angular 和 RxJS 中的事情变化很快。 如果您使用的是 Angular 4.3+ 或 5+,与示例项目相比,您会在此处看到一些细微的差异。 示例项目在服务中使用 Http 进行 HTTP 调用,例如 GETPOST。 我们将使用从 4.3+ 版本开始添加的新 HttpClient,它具有本教程所需的功能…… RxJS 从 5.5 版本开始也对导入的方式进行了一些更改,所以我们将在这里使用这种新样式。

重写 AngularJS 服务

在进行 ngUpgrade 时,一次选择一条路线并自下而上工作是明智之举。 您可以利用保持 Angular 和 AngularJS 并排运行的优势,而不必担心会破坏应用程序。

由于我们在上一个指南中做了回家路线,我们现在准备开始 customers 路线。 我们将首先重写 CustomerService 并将其降级以使其可用于我们的 AngularJS 组件。 然后,我们将看看在服务中同时使用 observable 和 Promise,以便您自己选择最适合您的迁移过程。

将 HttpClient 添加到 NgModule

在我们重写 CustomerService 之前,我们必须将 Angular 的 HttpClientModule 显式导入应用程序的 NgModule (app.module.ts) 以便进行 HTTP 调用。 这与在默认情况下包含所有内容的 Angular JS 中不同。 在 Angular 中,我们需要明确我们想要使用 Angular 的哪些部分。 虽然一开始可能看起来不方便,但这很好,因为它通过不自动导入未使用的代码来帮助减少应用程序的占用空间。

所以在第 3 行之后,我们将像这样导入它:

import { HttpClientModule } from '@angular/common/http';

然后,我们需要在第 12 行的 UpgradeModule 之后将该模块添加到我们的 imports 数组中:

//app.module.ts

@NgModule({

    imports: [

        BrowserModule,

        UpgradeModule,

        HttpClientModule

    ],

    declarations: [

        HomeComponent

    ],

    entryComponents: [

        HomeComponent

    ]

})

现在我们可以在整个应用程序中使用 HttpClientModule。 我们只需要导入一次,我们就可以将它用于整个应用程序中的所有其他服务。

重写客户服务

现在我们已经将 HttpClientModule 添加到我们的 Angular 应用程序模块中,我们准备好在 Angular 中重写 CustomerService。 然后我们将 降级 它,以便我们仍然可以在 Angular JS 组件和 Angular 组件中使用它。

我们要做的第一件事是将 customerService.ts 文件重命名为 customer.service.ts 以便它遵循当前的命名约定。

现在,让我们打开文件。 你会看到我们已经在使用 ES2015 类:

//customer.service.ts

class CustomerService{

    $http: any;

    constructor($http) {

        this.$http = $http;

    }


    getCustomers(){

        return this.$http.get('/api/customers')

            .then((response) => response.data);

    }


    getCustomer(id){

        return this.$http.get(`/api/customers/${id}`)

            .then((response) => response.data);

    }


    postCustomer(customer){

        return this.$http.post('/api/customers', customer)

            .then((data) => data);

    }

}


CustomerService.$inject = ['$http'];

export default CustomerService;

Angular 2+ 服务是我们导出的类,但我们添加了 Injectable() 注释。 试图记住工厂、服务、供应商以及如何创建每一个的日子已经一去不复返了。 在 Angular 中,服务就是服务,它只是一个带有可注入注解的导出类。 这不是一个巨大的解脱吗?

准备代码

我们可以做的第一件事是删除此文件中的最后两行。 我们不再需要 AngularJS $inject 数组,我们将在类声明之前添加 export 关键字,而不是使用 export default

export CustomerService { //etc.

现在我准备在文件顶部从 Angular 导入两个东西。 首先是前面提到的Injectable()注解:

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

接下来我们需要HttpClient:

import { HttpClient } from '@angular/common/http';

现在我们准备好让它成为一个 Angular 服务。

将服务类更新为 Angular

首先,让我们将 Injectable() 注释添加到我们的 CustomerService,就在类的上方:

@Injectable()

没有选项对象被传递到此注释中。

接下来我们需要做的是将所有对 AngularJS 的 $http 服务的引用替换为 Angular 的 HttpClient。 我们将使用简写 http 代替,在本文档中执行查找和替换,将 $http 更改为 http,因为大多数调用将在很大程度上是相同的:

现在我们需要更改关于如何创建 http 属性的一件事。 而不是这个:

//customer.service.ts

class CustomerService{

    http: any;

    constructor(http) {

        this.http = http;

    }

…我们将删除声明 any 类型的 http 的公共属性的第 6 行。 相反,在我们的构造函数中,让我们在 http 之前添加 private 关键字,并指定它的类型为 HttpClient

//customer.service.ts

export class CustomerService{

    constructor(private http: HttpClient) {  }

通过 Angular 的依赖注入,我们在 CustomerService 上实例化 HttpClient 服务的私有实例。您还可以看到,使用 private 关键字,我们不需要设置 http 等于我们注入的实例(它在幕后为我们执行此操作)。

我们现在拥有的是 Angular 服务的基本框架,但是您现在会在我们使用 .then 的所有地方看到那些红色的波浪线。 您可以看到 IntelliSense 告诉我们该属性在响应的可观察类型上不存在:

那里发生了什么事? 让我们接下来解决这个问题。

将 Observable 转换为 Promise

我们已经将我们的客户服务在很大程度上重写为 Angular 服务,但是我们在尝试在这些 http 调用上使用 .then 时遇到了一点问题。 这是因为 Angular 中的 HttpClient 返回一个 observable 而不是一个承诺。 我们这里有两个选择:

  1. 实用的方法: 将这些响应转换为 Promise,我们应用程序的其余部分将同样工作,或者
  2. 有趣的方式: 将这些响应保持为可观察的并更新我们的组件。

对于任何大规模的重构或升级,目标始终是尽可能少地浪费应用程序的正常运行时间。 推荐的方法是首先将调用转换为承诺。 这样,您可以确定应用程序的哪些组件和其他部分依赖于服务及其调用。 完成此操作后,您可以一次将调用转换为可观察对象,并相应地更新每个组件。 所以,首先,将服务交给 Angular 并让它工作。 然后在你觉得合适的时候担心使用 observables。

因此,让我们首先将调用转换为 Promise。 不过不用担心 - 稍后我们会做一些有趣的事情并将调用转换为可观察对象。

使用 toPromise 运算符

要将 observable 转换为 Promise,我们首先需要从处理 observable 的库 RxJS 导入。 在我们的 Angular 导入之后,我们只需要添加:

import { Observable } from 'rxjs/Observable';

这让我们可以为 RxJS 提供的 observable 对象使用各种函数。

toPromise 方法让我们可以将 observables 转换为 Promise。 在以前的 RxJS 版本中,它曾经是一个单独的导入,但现在已经被整合到 Observable 中。 导入单个操作符是 RxJS 中的一种常见模式,但弄清楚你需要哪些操作符以及它们在库中的位置可能有点令人生畏。 请务必仔细阅读 RxJS 提供的 文档资源 ,以及 RxJS 上的 Angular 文档。

现在我们可以在调用中的每个 .then 之前使用 toPromise 运算符。 当您这样做时,您还会看到一个错误,指出 .data 不是“对象”类型上存在的属性。 这是因为响应已经返回 HTTP 响应内部的数据对象。 然后我们需要做的就是移除 .data。 这与最初的 Http 服务不同,我们需要调用 .json 函数来返回数据。

还有一件事。 既然我们有 TypeScript 的好处,让我们为每个函数添加返回类型。 尽可能在 TypeScript 中指定类型总是最好的,尽管从技术上讲,这不是必需的。 因此,在每个函数名称之后,我们将添加 :Promise<any>

服务中完成的功能将如下所示:

//customer.service.ts

getCustomers():Promise<any> {

   return this.http.get('/api/customers')

       .toPromise()

        .then(response => response);

}


getCustomer(id):Promise<any> {

    return this.http.get(`/api/customers/${id}`)

        .toPromise()

        .then(response => response);

}


postCustomer(customer):Promise<any> {

    return this.http.post('/api/customers', customer)

       .toPromise()

       .then(data => data);

}

这样,我们就成功地将调用中的 observable 转换为 Promise。

降级客户服务

现在我们已经将 observables 转换为 Promise,我们准备 降级 客户服务,以便尚未迁移的 AngularJS 组件仍然可以使用它。

这个过程类似于我们在上一个指南中降级 home 组件时的过程。 我们需要做的第一件事是从 ngUpgrade 库中导入 downgradeInjectable 函数,就像我们为 home 组件导入 downgradeComponent 一样。 所以在第二行之后,我们将添加:

import { downgradeInjectable } from '@angular/upgrade/static';

我们还需要声明一个名为 angular 的变量,就像我们在 home 组件中所做的那样。 所以在第四行之后,我们将添加:

declare var angular: angular.IAngularStatic;

然后在我们文件的底部,我们将我们的服务注册为降级工厂。 因此,在课程结束后,我们将输入:

angular.module('app')

    .factory('customerService', downgradeInjectable(CustomerService));

我们已将 CustomerService 降级为可供 AngularJS 使用。 这是完成的服务:

//customer.service.ts

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

import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs/Observable';

import { downgradeInjectable } from '@angular/upgrade/static';

declare var angular: angular.IAngularStatic;


@Injectable()

export class CustomerService {

    constructor(private http: HttpClient) {}


    getCustomers():Promise<any> {

        return this.http.get('/api/customers')

            .toPromise()

            .then(response => response);

    }


    getCustomer(id):Promise<any> {

        return this.http.get(`/api/customers/${id}`)

            .toPromise()

            .then(response => response);

    }


    postCustomer(customer):Promise<any> {

        return this.http.post('/api/customers', customer)

            .toPromise()

            .then((data) => data);

    }

}


angular.module('app')

    .factory('customerService', downgradeInjectable(CustomerService));

将服务移至 Angular 模块

我们的客户服务已被重写为 Angular 服务并降级为可用于 AngularJS。 现在我们需要删除我们在 AngularJS 模块中的引用并将其添加到我们的 Angular 模块中。

从 AngularJS 模块中删除内容

首先,让我们打开我们的 AngularJS 模块(app.module.ajs.ts)。 您可以删除第 22 行:

import CustomerService from './customers/customerService';

…以及第 41 行:

.service('customerService', CustomerService)

这些是您需要在此模块中进行的所有更改。

将服务移至 Angular 模块

现在让我们将服务添加到 app.module.ts 中的 NgModule 中,以便我们的 Angular 代码可以访问它。 我们需要做的第一件事是在第七行之后导入服务:

import { CustomerService } from './customers/customer.service';

现在要在我们的应用程序中注册我们的客户服务,我们需要在我们的 entryComponents 数组之后添加一个 providers 到我们的 NgModule 数组,并在那里添加我们的 CustomerService:

//app.module.ts

providers: [

        CustomerService

    ]

providers 数组是我们在应用程序中注册所有服务的地方。 现在我们已经在我们的 NgModule 中注册了我们的客户服务并准备好了。

关于 AOT 编译的快速说明

这种降级方法——在服务文件中注册降级的服务并将其从 AngularJS 模块文件中删除——非常适合开发,或者如果您计划在部署之前快速重写应用程序。 但是,用于生产的 Angular AOT 编译器不适用于此方法。 相反,它希望我们在 AngularJS 模块中进行所有降级的注册。

降级是相同的,但你会:

  • app.module.ajs.ts 中导入 downgradeInjectable (你已经有 angular 在那里,所以你不需要声明它)。
  • 由于我们切换到命名导出,因此将 CustomerService 的导入更改为 import { CustomerService } from './customers/customer.service';
  • 将服务注册更改为与上面显示的完全相同的工厂注册。

测试应用程序的功能

我们最好确保我们的应用程序仍在运行。 让我们启动我们的 Express API,然后运行我们的 Webpack 开发服务器。 打开终端并运行以下命令来启动 Express:

cd server

npm start

然后打开另一个终端并运行这些命令来启动 Webpack:

cd public

npm run dev

您应该看到所有内容都正确编译和捆绑。

现在,打开浏览器并转到 localhost:9000。 让我们导航到我们的客户路线,看看服务是否正常工作:

我们可以通过转到 Chrome 开发人员工具中的源选项卡,向下导航到客户文件夹,然后单击 CustomerService 源来仔细检查我们是否正在使用重写的 Angular 服务:

这显示了我们重写的服务。 我们已将服务更新为 Angular,但它同时用于客户组件和客户表组件,这两个组件仍在 AngularJS 中。

使用 GetCustomers 作为 Observable

现在我们已经将 CustomerService 降级并正常工作,让我们玩得开心,并将 getCustomers 调用用作可观察对象。 这样我们就可以开始利用 observables 的所有新特性了。 这会有点棘手,因为我们在客户组件和订单组件中都使用了调用,这两个组件都没有被重写为 Angular。 别担心 - 我会一步一步地告诉你如何做到这一点。

回到客服代码,我们要做的第一件事就是将第16行的返回类型改为Observable<any>。 当然,现在 TypeScript 向我们抱怨,因为我们正在转换为 Promise,所以我们只需要删除 toPromisethen 两个函数。 现在看起来像这样:

getCustomers():Observable<any> {

      return this.http.get('/api/customers');

}

现在我们需要更新我们的客户组件以使用可观察的而不是承诺。 我们接下来会这样做。

在客户组件中使用 Observable

我们的 getCustomers 调用现在返回 observable。 让我们更新我们的客户组件 (customers.ts) 以使用可观察对象而不是承诺。 客户组件仍然是一个 AngularJS 组件,这很好,我们还不需要弄乱它,但是让我们使用一点 TypeScript 来帮助我们。 让我们在文件顶部导入我们的 CustomerService:

import { CustomerService } from './customer.service';

现在我们已经导入了 CustomerService,我们可以在控制器函数定义中指定注入的 CustomerService 的类型:

//customers.ts

function customersComponentController(customerService: CustomerService){

我们现在拥有 TypeScript 抱怨我们的 .then 的优势,就像它在我们的 CustomerService 中所做的那样。 它知道 getCustomers 调用应该返回一个可观察对象,并且 .then 不存在于可观察对象上。

我们在组件中使用 observable 的方式,无论是 AngularJS 还是 Angular 组件,都是 subscribe 到它。 在订阅这个 observable 之前,我们需要像在服务中一样导入 Observable。 因此,在我们的 CustomerService 导入之上,我们将添加:

import { Observable } from 'rxjs/observable';

这将让我们在 observable 上使用函数,包括 subscribe。 所以,现在在我们的 $onInit 函数内部的第 18 行,我们可以将 then 更改为 subscribe,其他一切都可以保持不变。

让我们看看浏览器,看看它是否按预期工作。 如果您前往客户路线,您应该会看到一切正常。 但是,如果我们转到 Orders 选项卡,我们会看到一个大问题:控制台中没有数据和 TypeError: Cannot read property 'fullName' of undefined。 这里发生了什么?

事实证明,订单组件 also 使用了 getCustomers 调用,但它仍在尝试将其用作承诺。 让我们解决这个问题。

修复订单组件

当我们将 getCustomers 调用重写为 observable 而不是 Promise 时,我们不小心破坏了仍在 AngularJS 中的订单组件 (orders/orders.ts)。 这是因为在我们的 $onInit 函数中,我们使用 $q.all 等待两个 promise 返回,然后再将任何数据分配给我们的视图模型:

vm.$onInit = function() {

        let promises = [orderService.getOrders(), customerService.getCustomers()];

        return $q.all(promises).then((data) => {

            vm.orders = data[0];

            vm.customers = data[1];

            vm.orders.forEach(function (order) {

                var customer = _.find(vm.customers, function (customer) {

                    return order.customerId === customer.id;

                });

                order.customerName = customer.fullName;

            });

        });

    };

这是 AngularJS 中的常见模式。

解决这个问题的一种方法是将订单组件重写为 Angular,同时重写订单服务。 但是,在现实世界中,这并不总是可能的。 请记住,在任何大规模重构中,首要任务是确保我们最大限度地减少停机时间,并能够拥有一个我们始终可以部署到生产中的持续交付的应用程序。

但是,如果订单组件要复杂得多并且我们没有时间重写它怎么办? 在这种情况下,我们有两个选择:我们可以将 getCustomers 调用转换为订单组件中的承诺,或者我们可以将 getOrders 承诺转换为可观察的。

要将 getCustomers 转换为组件中的 Promise,我们只需执行我们之前在服务中所做的完全相同的事情 - 从 RxJS 导入 Observable 并添加 toPromise 运算符在 getCustomers 之后。 就这么简单,如果你还没有时间重构这个组件以使用 observables,这是一个方便的技巧。 然而,这并不是完全可取的,因为我们的长期目标是完全摆脱 Promise 并完全切换到 observables。 所以,我们在这里将我们的 getOrders 调用转换为 observable。

getCustomers 转换为 Promise

让我们将 getOrders 转换为 observable。 我们要做的第一件事是在文件顶部导入我们的 CustomerService,就像我们在客户组件中所做的那样:

import { CustomerService } from '../customers/customer.service';

然后我们可以在控制器函数定义中指定我们注入的 CustomerService 的类型:

//orders.ts

function ordersComponentController(orderService, customerService: CustomerService, $q) {

为了将 getOrders 调用转换为 observable,我们将在 observable 上使用两个静态方法,称为 fromPromiseforkJoinfromPromise 方法让我们将 promise 转换为 observable,而 forkJoin 让我们订阅多个 observable。 所以,你现在可能已经猜到了,我们需要做的第一件事就是在文件顶部导入这两个方法:

import { fromPromise } from 'rxjs/observable/fromPromise';

import { forkJoin } from 'rxjs/observable/forkJoin';.

现在我们可以在我们的 $onInit 函数中做一些工作。 在第 21 行上方,让我们声明一个名为 ordersData 的变量并使用 fromPromise 方法:

let ordersData = fromPromise(orderService.getOrders());

现在让我们重写 $q.all 以使用 forkJoin 代替。 因此,首先我们将 return $q.all 替换为 forkJoin。 我们需要传入一个数组,所以让我们移动 promises 数组并将 ordersData 添加到它的前面,然后去掉 promises 声明。 最后,让我们将 .then 更改为 .subscribe,就像使用单个 observable 一样。 这是我们完成的 $onInit 函数:

vm.$onInit = function() {

        let ordersData = fromPromise(orderService.getOrders());

        forkJoin([ordersData, customerService.getCustomers()]).subscribe((data) => {

            vm.orders = data[0];

            vm.customers = data[1];

            vm.orders.forEach(function (order) {

                var customer = _.find(vm.customers, function (customer) {

                    return order.customerId === customer.id;

                });

                order.customerName = customer.fullName;

            });

        });

    };

让我们回顾一下我们在这里所做的事情。 首先,我们调用了 fromPromise 并将我们的 getOrders 调用从 promise 转换为 observable。 然后,我们使用 forkJoin 订阅了 ordersDatagetCustomers 调用。 就像 $q.all 一样,订阅 forkJoin 将按照我们列出的顺序返回一个数据数组。 因此,data[0] 将是我们的订单,而 data[1] 将是我们的客户。

让我们再做一件事来清理它。 我们可以从 $inject 数组中的第 16 行和函数定义中的第 167 行中删除 $q 依赖项。

确认应用程序正在运行

让我们再看看浏览器并确保它有效。 您应该看到我们的应用程序正确编译和加载,因此请查看订单选项卡:

这表明我们的数据加载正确。 现在您已经了解了如何在 Promise 和 observable 之间来回转换,这在您处理大型应用程序时非常有用,因为您不能在升级时一次性将所有内容都转换为 observables。

结论

从这里开始,使用本指南和最后一个指南来转换 customersTable 组件和 products 路由。 您将需要学习一些使用 Angular 模板语法的新技巧,否则您将拥有所需的一切。