如何使用ngUpgrade从AngularJS升级到Angular
介绍
Angular (2+) 就在这里,我们都对此感到非常兴奋。 但是,对于我们中的一些人来说,我们仍在工作中维护大型 AngularJS (1.x) 代码库。 我们如何开始将我们的应用程序迁移到新版本的 Angular 中——尤其是如果我们不能花六个月的时间来进行功能开发来完全重写?
这就是 ngUpgrade 库的用武之地。 ngUpgrade 是官方工具,允许您一点一点地迁移您的应用程序。 只要您需要慢慢升级,它就可以让 Angular 与您的 AngularJS 代码一起运行。
在本指南中,您将安装和设置 ngUpgrade 和 Angular。 然后,您将学习重写组件的基础知识。
(附言 如果本指南的长度让您感到害怕,请不要担心。 我已经构建了一个名为 Upgrading AngularJS 的一步一步的超级详细的视频程序,它详细介绍了所有这些。)
先决条件
要完成本教程,您需要:
- Node.js 安装在本地,您可以按照【X57X】如何安装Node.js 并创建本地开发环境【X126X】进行。
本教程已使用 Node v14.4.0、npm
v6.14.5、angular
v1.6.6 和 @angular/common
v5.2.11 进行了验证。
第 1 步 — 启动项目
要开始使用 ngUpgrade,您的应用程序需要满足一些先决条件:
- 按功能(不是按类型)组织的代码,每个文件只包含一个项目(如指令或服务)
- 打字稿设置
- 使用模块打包器(大多数人使用 Webpack)
- 使用 AngularJS 1.5+ 和由组件替换的控制器
不过现在,请花一点时间在 GitHub 上克隆或分叉 课程示例项目 :
git clone https://github.com/upgradingangularjs/ordersystem-project.git
导航到项目目录:
cd ordersystem-project
签出 this commit 以查看我们的起点:
git checkout fdfcf0bc3b812fa01063fbe98e18f3c2f4bcc5b4
我们有一个订单系统项目,我们可以使用它来通过 ngUpgrade 工作。 从这次提交开始,我们的应用程序满足上述所有标准。 我们正在使用组件架构、TypeScript 和 Webpack(我们甚至有用于开发和生产的构建)。
注意:在许多大型 AngularJS 应用程序中,您不能将所有内容都移入一个全新的 Git 存储库并抹去多年的历史。 您也可能使用与 CLI 不同的应用程序结构。 如果您可以使用 CLI 进行升级,请随意这样做。 但是,本指南将在此处教您手动设置,以便您可以完全控制升级。
第 2 步 — 安装 Angular 和 ngUpgrade
我们已准备好安装 Angular、ngUpgrade 和所有对等依赖项。 在示例项目中,继续更新您的 package.json
依赖项数组,使其如下所示:
包.json
"dependencies": { "@angular/common": "^5.2.5", "@angular/compiler": "^5.2.5", "@angular/core": "^5.2.5", "@angular/forms": "^5.2.5", "@angular/platform-browser": "^5.2.5", "@angular/platform-browser-dynamic": "^5.2.5", "@angular/router": "^5.2.5", "@angular/upgrade": "^5.2.5", "angular": "1.6.6", "angular-route": "1.6.6", "bootstrap": "3.3.7", "core-js": "^2.5.3", "jquery": "^2.2.4", "lodash": "4.17.4", "moment": "~2.17.1", "reflect-metadata": "^0.1.12", "rxjs": "^5.5.6", "zone.js": "^0.8.20" }
(我们将在本系列中使用 Angular 5,即使示例项目使用版本 4,但步骤是相同的。)
我们可以将所有这些包放在带有保存标志的终端中的一个长命令中,但我们将花时间解释这些包中的每一个是什么。
首先是我们在 @angular
命名空间下的库:
@angular/common
:这些是 Angular 常用的服务、管道和指令。 该软件包还包含 4.3 版的新HttpClient
,因此我们不再需要@angular/http
。@angular/compiler
:这是 Angular 的模板编译器。 它接受模板并将它们转换为使您的应用程序运行和呈现的代码。 您几乎不需要与它进行交互。@angular/core
:这些是每个应用程序都需要的 Angular 的关键运行时部分。 这包括元数据装饰器(例如,Component
、Injectable
)、所有依赖注入和组件生命周期挂钩(如OnInit
)。@angular/forms
:这就是我们需要的表单的一切,无论是模板还是反应式。@angular/platform-browser
:这是 DOM 和浏览器相关的所有内容,尤其是有助于渲染 DOM 的部分。 这是包含bootstrapStatic
的软件包,这是我们用于引导应用程序以进行生产构建的方法。@angular/platform-browser-dynamic
:此软件包包括提供程序和另一种用于在客户端编译模板的应用程序的引导方法。 这是我们在开发过程中用于引导的包,我们将在另一个视频中介绍两者之间的切换。@angular/router
:你可能猜到了,这只是 Angular 的路由器。@angular/upgrade
:这是 ngUpgrade 库,它允许我们将 AngularJS 应用程序迁移到 Angular。
在我们所有的 Angular 包之后,我们的 polyfill 包都是 Angular 的依赖项:
core-js
:使用 ES6 或 ES2015 的某些特性修补全局上下文或窗口。reflect-metadata
:这是一个 polyfill 库,用于 Angular 在其类中使用的注释。rxjs
:这是一个包含我们将用于处理数据的所有可观察对象的库。zone.js
:这是 Zone 规范的 polyfill,它是 Angular 管理变更检测的一部分。
有时,您使用的 TypeScript 版本存在冲突。 这可能是由于 RxJS、Angular 编译器或 Webpack 造成的。 如果您开始遇到奇怪的编译错误,请进行一些研究以找出其中任何一个需要您正在使用的版本的特定版本范围的 TypeScript。
打开你的终端, cd
进入项目的 public
文件夹:
cd public
并使用 npm
安装依赖项(如果您愿意,欢迎安装和使用 Yarn):
npm install
您将看到所有软件包都已安装。
我们现在已经准备好通过双重启动 AngularJS 和 Angular 来使我们的应用程序成为一个混合应用程序。
第 3 步 — 设置 ngUpgrade
要设置 ngUpgrade,我们需要执行一系列步骤以允许 AngularJS 和 Angular 一起运行。
从 index.html
中移除引导程序
我们需要做的第一件事是从 index.html
中删除我们的引导指令。 这就是 AngularJS 通常在页面加载时启动的方式,但我们将使用 ngUpgrade 通过 Angular 引导它。 因此,只需打开 index.html
并删除 data-ng-app
标签。 (如果您在自己的应用程序中使用严格的 DI,则在此步骤中也将删除 ng-strict-di
。)
您的 index.html
文件现在将如下所示:
公共/src/index.html
<html> <head> <title>Amazing, Inc. Order System</title> </head> <body> <navigation></navigation> <div class="container" ng-view></div> </body> </html>
更改 AngularJS 模块
现在,我们需要对 AngularJS 模块进行一些更改。 打开 app.ts。
我们需要做的第一件事是将 app.ts
重命名为 app.module.ajs.ts
以反映它是 AngularJS 的模块。 这是一个很长的名字,但在 Angular 中,我们希望在文件名中包含我们的类型。 这里我们使用 app.module
然后我们添加 ajs
来指定它是用于 AngularJS 而不是我们的根 app.module
用于 Angular(我们将在第二)。
就像现在的应用程序一样,我们只是使用 AngularJS,所以我们在这里拥有所有的导入语句,并且我们在 Angular 模块上注册了所有内容。 然而,现在我们要做的是导出这个模块并将其导入到我们的新 Angular 模块中以启动并运行它。
因此,在 第 28 行 上,让我们创建一个应用名称的字符串常量:
公共/src/app.module.ajs.ts
const MODULE_NAME = 'app';
然后我们将在我们的 angular.module
声明中用模块名称替换我们的应用程序字符串:
公共/src/app.module.ajs.ts
angular.module(MODULE_NAME, ['ngRoute']) // component and service registrations continue here
最后,我们需要导出常量:
公共/src/app.module.ajs.ts
export default MODULE_NAME;
此时,对 app.module.ajs.ts 所做的 更改应类似于此差异 。
创建 Angular 应用模块
我们的 AngularJS 模块已经准备就绪,所以我们现在可以制作我们的 Angular 模块了。 然后我们将导入我们的 AngularJS 模块,以便我们可以在这里手动引导它。 这就是让两个框架一起运行的原因,并使 ngUpgrade 能够弥合它们之间的差距。
我们需要做的第一件事是在与我们的 AngularJS 模块相同的级别创建一个名为 app.module.ts
的新文件。 现在,您将第一次看到在整个升级过程中您会熟悉的模式:创建和导出一个类,用注释装饰它,以及导入所有依赖项。
在我们的新应用模块中,让我们创建一个名为 AppModule
的类:
公共/src/app.module.ts
export class AppModule { }
现在,让我们添加我们的第一个 annotation(也称为 decorator)。 注释只是 Angular 在构建我们的应用程序时使用的一些元数据。 在我们的新类之上,我们将使用 NgModule
注释并传入一个选项对象:
公共/src/app.module.ts
@NgModule({}) export class AppModule { }
如果您在 Visual Studio Code 之类的编辑器中进行操作,您会发现 TypeScript 对我们很生气,因为它不知道 NgModule
是什么。 这是因为我们需要从 Angular 核心库中导入它。 在我们的装饰器之上,我们可以通过以下方式解决这个问题:
公共/src/app.module.ts
import { NgModule } from '@angular/core';
现在,在 ngModule 的选项对象中,我们需要传递一个导入数组。 导入数组指定了这个 NgModule
将依赖的其他 NgModule。 (这些导入不同于我们文件顶部的 TypeScript 导入。)现在,我们需要 BrowserModule
和 UpgradeModule
:
公共/src/app.module.ts
import { NgModule } from '@angular/core'; @NgModule({ imports: [ BrowserModule, UpgradeModule ] }) export class AppModule { }
当然,我们也没有在文件顶部导入那些,所以我们也需要这样做。 在我们第一次导入之后,我们可以添加:
公共/src/app.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { UpgradeModule } from '@angular/upgrade/static';
upgrade
和 upgrade/static
中都有一个 UpgradeModule
。 我们希望使用 static
之一,因为它提供了更好的错误报告并与 AOT(提前)编译一起使用。
我们已经为 Angular 设置了根模块的基本脚手架,我们已经准备好自己进行引导。
Angular 模块中的引导
要引导我们的应用程序,我们需要做的第一件事是使用构造函数注入 UpgradeModule
:
公共/src/app.module.ts
constructor(private upgrade: UpgradeModule){ }
我们不需要在构造函数中做任何事情。 接下来我们要做的是覆盖 doBootstrap
函数。 在构造函数之后,键入:
公共/src/app.module.ts
ngDoBootstrap(){ }
接下来,我们将使用 UpgradeModule 的引导函数。 它与 Angular 引导函数具有相同的签名,但它为我们做了一些额外的事情。 首先,它确保 Angular 和 AngularJS 在正确的区域中运行,然后它设置了一个额外的模块,允许 AngularJS 在 Angular 中可见并且 Angular 在 AngularJS 中可见。 最后,它调整了可测试性 API,以便 Protractor 可以与混合应用程序一起使用,这非常重要。
让我们添加它:
公共/src/app.module.ts
ngDoBootstrap(){ this.upgrade.bootstrap(document.documentElement, [moduleName], {strictDi: true}); }
我们首先传入我们的 document
元素,然后传入数组中的 AngularJS 模块。 最后,您可以看到一个示例,我们正在添加一个配置对象,以便我们可以打开严格的依赖注入。
您可能想知道 moduleName
是从哪里来的。 我们需要将它与我们的其他导入语句一起导入:
公共/src/app.module.ts
import moduleName from './app.module.ajs';
这是我们完成的 app.module.ts 文件现在的样子:
app.module.ts
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { UpgradeModule } from '@angular/upgrade/static'; import moduleName from './app.module.ajs'; @NgModule({ imports: [ BrowserModule, UpgradeModule ] }) export class AppModule { constructor(private upgrade: UpgradeModule) { } ngDoBootstrap(){ this.upgrade.bootstrap(document.documentElement, [moduleName], {strictDi: true}); } }
随着时间的推移,这将成为您熟悉的模式。
创建 main.ts
现在我们已经设置了 AngularJS 模块和 Angular 模块,我们需要一个入口点,将这两者结合在一起并让我们的应用程序运行。 让我们在我们的 src
文件夹下创建一个名为 main.ts
的新文件。
在 main.ts
中,我们需要导入一些东西,告诉 Angular 要加载哪个版本的 AngularJS,然后告诉它引导我们的 Angular 模块。 首先,我们需要导入两个 polyfill 库和 Angular 的 platformBrowserDynamic
函数:
公共/src/main.ts
import 'zone.js'; import 'reflect-metadata'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
为什么是 platformBrowserDynamic
而不是 platformBrowser
? Angular 有两种编译方式:动态选项和静态选项。 在动态选项(称为即时,或 JIT)中,Angular 编译器在浏览器中编译应用程序,然后启动应用程序。 静态选项(称为提前,或 AOT)产生一个启动速度更快的小得多的应用程序。 这是因为 Angular 编译器作为构建过程的一部分提前运行。 我们将在这里使用 JIT 方法和 Webpack 开发服务器。
现在我们需要导入我们的 Angular 和 AngularJS 模块,以及一个告诉 Angular 使用哪个版本的 AngularJS 的方法:
公共/src/main.ts
import { setAngularLib } from '@angular/upgrade/static'; import * as angular from 'angular'; import { AppModule } from './app.module';
现在要完成这个,我们只需要调用 setAngularLib
并传入我们的 AngularJS 版本,我们需要调用 platformBrowserDynamic
并告诉它引导我们的应用程序模块。
完成的文件如下所示:
公共/src/main.ts
import 'zone.js'; import 'reflect-metadata'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { setAngularLib } from '@angular/upgrade/static'; import * as angular from 'angular'; import { AppModule } from './app.module'; setAngularLib(angular); platformBrowserDynamic().bootstrapModule(AppModule);
现在我们已经设置好了,我们只需要在我们的配置中更改我们的 Webpack 入口点。
更新 Webpack
希望这个引导混合应用程序的过程开始对您有意义。 我们有一个 main.ts
文件作为我们的入口点,它设置我们的 AngularJS 库并引导我们的 Angular 模块。 然后,我们的 Angular 模块引导我们的 AngularJS 模块。 这就是让我们两个框架一起运行的原因。
我们现在准备好更改我们的 Webpack 配置,以便它从我们的 main.ts
文件开始,而不是我们的应用程序模块文件之一。 打开 webpack.common.js(它在 webpack-configs
文件夹下)。 在 entry
的 module.exports
下,我们将应用根目录更改为 main.ts
:
公共/webpack-configs/webpack.common.js
entry: { app: './src/main.ts', }
测试应用程序
现在,我们已经准备好看到我们的混合应用程序在运行了。 您可以通过打开终端并运行以下命令来运行开发服务器:
# ensure that you are in the `ordersystems-project` directory cd server npm install npm start
在第二个终端会话中,运行以下命令:
# ensure that you are in the `ordersystems-project` directory cd public npm run dev
您将看到 Webpack 正在加载,并且我们的 TypeScript 已成功编译。
让我们在 localhost:9000
上查看浏览器。 您可以看到我们的应用程序仍然在我们的开发服务器上运行。
根据您的版本,您可能会在控制台中看到一些关于 core-js
的警告,但不要担心,它们不会影响我们。 您还可以打开网络选项卡并查看供应商捆绑包和应用捆绑包:
供应商捆绑包绝对是巨大的,这是因为:
- 我们正在运行 Webpack 开发服务器,这意味着它不会缩小任何内容。
- 我们在动态编译中运行 Angular,因此它也将编译器代码传送到浏览器。
当我们谈论 AOT 编译时,我们将在下游解决这个问题,但是我们可以在这里浏览并看到我们所有的数据仍在加载。
我们现在让 Angular 和 AngularJS 一起运行,这意味着我们已经成功地设置了我们的混合应用程序。 这意味着我们已经准备好开始逐步升级我们的应用程序。
第 4 步 — 重写和降级您的第一个组件
我们的应用程序已启动并在混合模式下运行,因此我们准备开始迁移应用程序的每个部分。 一种常见的方法是选择一条路线,然后从下往上重写每个部分,从具有最少依赖性的任何部分开始。 这使我们能够迭代地升级我们的应用程序,以便沿途的每一点,我们都有一些可部署到生产中的东西。
让我们从 home 路由开始,因为这很简单,只需要 Home 组件。 我们首先将 Home 组件从 home.ts
重命名为 home.component.ts
。
现在,我们需要将 Home 组件重写为 Angular 类。 我们需要做的第一件事是从文件顶部的 Angular 核心库中导入 Component
:
公共/src/home.component.ts
import { Component } from '@angular/core'
接下来我们要做的是将我们的函数 homeComponentController
转换为一个类。 我们也可以把它大写,去掉名字末尾的controller
,就叫HomeComponent
。 最后,让我们去掉括号。 现在看起来像这样:
公共/src/home.component.ts
class HomeComponent { var vm = this; vm.title = 'Awesome, Inc. Internal Ordering System'; }
现在,让我们清理类中的内容。 我们不再需要 vm
的声明,因为我们正在使用一个类。 我们还可以将 title
的属性添加为字符串,并将标题设置移动到构造函数。 我们的类现在看起来像这样:
公共/src/home.component.ts
class HomeComponent { title: string; constructor(){ title = 'Awesome, Inc. Internal Ordering System'; } }
我们还需要 export
这个类,然后删除那个 export default
行。
现在,我们需要应用我们导入的 Component
元数据装饰器来告诉 Angular 这是一个组件。 我们可以用组件装饰器和选项对象替换 Home 组件对象:
公共/src/home.component.ts
@Component({ }
我们组件装饰器的第一个选项是 selector
。 这只是我们将用来引用该组件的 HTML 标记,它只是“home”。 请注意,在 Angular 中,选择器是一个字符串文字。 这与在 AngularJS 中不同,我们将组件命名为驼峰式,然后将其转换为带有连字符的 HTML 标记。 在这里,我们将准确地放置我们想要使用的标签。 在这种情况下,我们只是将其保留在“家”中,因此无关紧要。 之后,我们将指定我们的 template
,就像我们对 AngularJS 所做的那样,所以我只会说 template: template
。 不管你信不信,这就是它的全部。 我们完成的组件如下所示:
公共/src/home.component.ts
import { Component } from '@angular/core'; const template = require('./home.html'); @Component({ selector: 'home', template: template }) export class HomeComponent { title: string; constructor(){ this.title = 'Awesome, Inc. Internal Ordering System'; } }
注意:如果你正在开发一个将使用 AOT 编译器的应用程序,你会想要使用 templateUrl
而不是我们在这里所做的,并对 Webpack 进行一些更改。 不过,这对于 JIT 和开发服务器来说完全没问题。
为 AngularJS 降级组件
我们现在需要使用 ngUpgrade 库来“降级”这个组件。 “降级”意味着使 Angular 组件或服务对 AngularJS 可用。 另一方面,“升级”意味着使 AngularJS 组件或服务对 Angular 可用。 我们将在另一篇文章中介绍。 幸运的是,降级非常容易。
首先,我们需要在文件顶部与导入一起做两件事。 我们需要从 Angular 升级库中导入 downgradeComponent
函数,声明一个名为 angular
的变量,以便我们可以在 AngularJS 模块上注册这个组件。 这看起来像这样:
公共/src/home.component.ts
import { downgradeComponent } from '@angular/upgrade/static'; declare var angular: angular.IAngularStatic;
降级组件相当简单。 在我们组件的底部,我们将此组件注册为指令。 我们将传入指令名称,即 home
,与本例中的选择器相同。 然后,我们将从 ngUpgrade 传入 downgradeComponent
函数。 该函数将我们的 Angular 组件转换为 AngularJS 指令。 最后,我们将此对象转换为 angular.IDirectiveFactory
。 完成的注册如下所示:
公共/src/home.component.ts
app.module('app') .directive('home', downgradeComponent({component: HomeComponent} as angular.IDirectiveFactory);
此时,对 home.component.ts 所做的 更改应该类似于此差异 。
现在,我们有一个可用于 AngularJS 应用程序的降级 Angular 组件。 您可能想知道为什么我们在这个文件的底部注册了该指令,而不是在我们的 AngularJS 模块 TypeScript 文件中导入和注册它。 最终目标是在我们的所有应用程序都转换后完全删除该文件,因此我们希望逐渐从该文件中删除内容,然后在卸载 AngularJS 时最终将其完全删除。 这对于示例应用程序或快速迁移非常有用(稍后会详细介绍)。
继续打开 app.module.ajs.ts
并删除 Line 12 上 homeComponent
的导入和 Line 37 上的组件注册。
此时,对 app.module.ajs.ts 所做的 更改应类似于此差异 。
关于 AOT 编译的快速说明
这种降级方法——在组件文件中注册降级的组件并将其从 AngularJS 模块文件中删除——非常适合开发,或者如果您计划在部署之前快速重写应用程序。 但是,用于生产的 Angular AOT 编译器不适用于此方法。 相反,它希望我们在 AngularJS 模块中进行所有降级的注册。
降级是相同的,但你会:
- 在
app.module.ajs.ts
中导入downgradeComponent
(你已经有angular
在那里,所以你不需要声明它)。 - 由于我们切换到命名导出,因此将
homeComponent
的导入更改为import { HomeComponent } from './home/home.component';
。 - 将组件注册更改为与上面显示的完全相同的指令注册。
您可以在 这篇文章 中阅读有关为 AOT 设置 ngUpgrade 的更多信息。
更新模板
组件更新后,我们需要确保更新其模板,使其符合新的 Angular 语法。 在这种情况下,您只需对 homeComponent
进行少量更改。 我们只需要删除 Line 2 上的 $ctrl
。 模板现在看起来像这样:
<div class="row"> <h1>{{title}}</h1> </div>
现在,我们的混合应用程序中有一个功能齐全的降级 Home 组件。
添加到 Angular 应用程序模块
让我们将新的 Angular 组件添加到 Angular 模块中。 打开 app.module.ts
。 首先,我们只需要在所有其他导入之后导入我们的 HomeComponent
:
公共/src/app.module.ts
import { HomeComponent } from './home/home.component';
现在,我们需要将 HomeComponent
添加到我们的 Angular 应用程序中。 所有 Angular 组件都必须添加到 NgModule 的 declarations
数组中。 因此,在选项对象中的 Line 12 之后,我们将添加一个名为 declarations 的新数组并添加我们的组件:
公共/src/app.module.ts
declarations: [ HomeComponent ]
我们还需要创建一个 entryComponents
数组并将我们的 HomeComponent
添加到其中。 所有降级的组件都必须添加到这个 entryComponents
数组中。 我们将在 declarations
之后添加它:
公共/src/app.module.ts
entryComponents: [ HomeComponent ]
此时,对 app.module.ts 所做的 更改应类似于此差异 。
这样,我们就完成了。
第 5 步 — 测试应用程序
让我们像以前一样运行这些相同的命令,并确保我们的应用程序仍在工作。 以下是这些命令:
# ensure that you are in the `ordersystems-project` directory cd server npm start
在第二个终端会话中,运行以下命令:
# ensure that you are in the `ordersystems-project` directory cd public npm run dev
回到 localhost:9000
。 您可以看到我们的 HomeComponent
正在作为重写的 Angular 组件加载到浏览器中!
您甚至可以查看 Chrome DevTools 的 Sources 选项卡,以保持积极态度。 打开webpack://
,向下滚动到./src/home/home.component.ts
,果然,到了!
结论
在本教程中,您安装了 Angular 和 ngUpgrade,设置了 Angular 模块,引导 Angular 和 AngularJS,更新了 Webpack,并重写并降级了您的第一个组件。
要了解有关 Angular 的更多信息,请查看 Angular 主题页面。