本文翻译自Directly Upgrading From AngularJS 1.X To Angular Without Preparing The Existing Code Base
正文:
当我们从AngularJS 1.x升级到Angular(2/4/5)时,我们通常会先准备AngularJS 1.x的代码:
这个过程会引入像组件这类新的AngularJS 1.x技术。并且,引入TypeScript和像SystemJS或者Webpack之类的模块加载器是准备已有代码的进一步工作。这样做的目的是为了让代码更接近Angular便于更好的集成。
但是,在一些情况下,准备已有的代码成本很大。例如,试想一下这样的情形,当你不想修修改已有的AngularJS1.x的代码,并且想要写一些Angular的应用。当这样的情况在你的项目中发生,跳过准备阶段是一个好的主意。
这篇文章一步步展示如何完成这个过程。像官方的升级教程 一样,包含准备代码的工作,这里也是升级流行的AngularJS 1.x 手机分类实例 。
即使这个实例覆盖了AngularJS 1.5中引入的组件,这里展示的对使用控制器(controller)和指令(directive)的代码也适用。
整个实例代码可以在Github 仓库 中找到。为了接下来每一步更容易,我针对每一步做了一个代码提交。
第一步:创建新的Angular应用 一开始,本文假设我们使用Angular CLI 来搭建一个新的Angular应用:
为了让这个新的方案结构清晰,在src目录下创建了一个文件夹给已有的AngularJS代码,另一个文件夹给新的Angular代码。 在下面的实例中,我使用了ng1和ng2来命名:
创建完之后,移动除了tsconfig.app.json , tsconfig.spec.json , favicon.ico 和index.html 之外的文件到ng2文件夹中。
通过.angular-cli.json文件来通知CLI的编译任务有关修改的新代码结构。在这个文件中使用assets 字段,我们也可以告诉CLI直接拷贝ng1 文件夹到输出的目录中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 { "$schema" : "./node_modules/@angular/cli/lib/config/schema.json" , "project" : { "name" : "migrated" } , "apps" : [ { "root" : "src" , "outDir" : "dist" , "assets" : [ "ng1" , "assets" , "favicon.ico" ] , "index" : "index.html" , "main" : "ng2/main.ts" , "polyfills" : "ng2/polyfills.ts" , "test" : "ng2/test.ts" , "tsconfig" : "tsconfig.app.json" , "testTsconfig" : "tsconfig.spec.json" , "prefix" : "app" , "styles" : [ "ng2/styles.css" ] , "scripts" : [ ] , "environmentSource" : "ng2/environments/environment.ts" , "environments" : { "dev" : "ng2/environments/environment.ts" , "prod" : "ng2/environments/environment.prod.ts" } } ] , "e2e" : { "protractor" : { "config" : "./protractor.conf.js" } } , "lint" : [ { "project" : "tsconfig.app.json" } , { "project" : "tsconfig.spec.json" } , { "project" : "tsconfig.e2e.json" } ] , "test" : { "karma" : { "config" : "./karma.conf.js" } } , "defaults" : { "styleExt" : "css" , "component" : { } } }
现在拷贝了整个AngularJS 1.x应用到ng1 文件夹中,但是忽略index.html 。为了旧的应用可以在修改过的文件结构下工作,我们要做一些调整。这包括修改模板文件的引用还有JSON文件和图片文件。
之后,我们可以合并旧的index.html 到文件夹src 下新的文件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 <!doctype html > <html lang ="en" > <head > <meta charset ="utf-8" > <title > Migrated</title > <base href ="/" > <link rel ="stylesheet" href ="ng1/bower_components/bootstrap/dist/css/bootstrap.css" /> <link rel ="stylesheet" href ="ng1/app.css" /> <link rel ="stylesheet" href ="ng1/app.animations.css" /> <script src ="ng1/bower_components/jquery/dist/jquery.js" > </script > <script src ="ng1/bower_components/angular/angular.js" > </script > <script src ="ng1/bower_components/angular-animate/angular-animate.js" > </script > <script src ="ng1/bower_components/angular-resource/angular-resource.js" > </script > <script src ="ng1/bower_components/angular-route/angular-route.js" > </script > <script src ="ng1/app.module.js" > </script > <script src ="ng1/app.config.js" > </script > <script src ="ng1/app.animations.js" > </script > <script src ="ng1/core/core.module.js" > </script > <script src ="ng1/core/checkmark/checkmark.filter.js" > </script > <script src ="ng1/core/phone/phone.module.js" > </script > <script src ="ng1/core/phone/phone.service.js" > </script > <script src ="ng1/phone-list/phone-list.module.js" > </script > <script src ="ng1/phone-list/phone-list.component.js" > </script > <script src ="ng1/phone-detail/phone-detail.module.js" > </script > <script src ="ng1/phone-detail/phone-detail.component.js" > </script > <meta name ="viewport" content ="width=device-width, initial-scale=1" > <link rel ="icon" type ="image/x-icon" href ="favicon.ico" > </head > <body ng-app ="phonecatApp" > <div class ="view-container" > <div ng-view class ="view-frame" > </div > </div > <app-root > </app-root > </body > </html >
注意这个合并后的index.html 包含了AngularJS 1.x应用所需要的CSS文件和脚本。还通过ng-app 启动AngularJS 1.x应用,并通过包含有ng-view 指令的div提供出来的壳。这个是路由激活对应配置模板的地方。
在这个文件中,我们也可以找到Angular应用的根元素。针对Angular生成打包文件的引用是不需要的,因为他们由编译任务自动生成。
当这个应用启动(ng serve),它将会将两个应用独立的加载到浏览器中。可以通过访问http://localhost:4200 来查看。
由于两个应用是独立启动的,因此他们无法互相通信和交换使用服务和组件。为了使这些工作,我们需要让他们作为混合应用启动。下一章节会介绍如何做到。
第二步:启动一个AngularJS+Angular的混合应用 为了同时启动AngularJS 1.x和Angular应用,我们可以利用Angular的ngUpgrade模块:
1 npm install @angular/upgrade --save
由于我们不想启动Angular(2/4/5等)应用,我们将indexl.html 文件中的根组件移除:
现在,我们可以一起同时启动两个应用。为此,引入UpgradeModule 模块到Angular应用的AppModule 中。从bootstrap 中移除AppComponent ,从而手动启动混合应用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import { BrowserModule } from '@angular/platform-browser' ;import { NgModule } from '@angular/core' ;import { UpgradeModule , downgradeComponent } from '@angular/upgrade/static' ;import { AppComponent } from './app.component' ;import { Ng2DemoComponent } from "ng2/app/ng2-demo.component" ;@NgModule ({ declarations : [ AppComponent ], imports : [ BrowserModule , UpgradeModule ], providers : [], }) export class AppModule { constructor (private upgrade : UpgradeModule ) { } ngDoBootstrap ( ) { this .upgrade .bootstrap (document .body , ['phonecatApp' ], { strictDi : true }); } }
就像你所看到的,这个例子通过使用注入的UpgradeModule 模块在ngDoBootstrap 中启动混合应用。为了阻止启动AngularJS 1.x应用两次,我们需要在index.html 文件中移除ng-app 指令。
当我们开始应用,我们可以看到AngularJS 1.x的组件:
尽管如此,这个一个包含两个版本Angular的混合应用。为了证明这一点,下一章节将会显示如何在展示的AngularJS组件中使用Angular组件。
第三步:降级一个Angular组件 为了展示如何在混合应用的AngularJS中使用Angular组件,教程中会使用一个非常简单的组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 import { Component , OnInit } from '@angular/core' ;@Component ({ selector : 'ng2-demo' , template : ` <h3>Angular 2 Demo Component</h3> <img width="150" src="..." /> ` }) export class Ng2DemoComponent {}
源代码中显示的图片可以在脚手架中AppComponent 找到。
为了在AngularJS模板中使用这个组件,我们需要降级它。ngUpgrade提供了一个函数downgradeComponent 来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import { BrowserModule } from '@angular/platform-browser' ;import { NgModule } from '@angular/core' ;import { UpgradeModule , downgradeComponent } from '@angular/upgrade/static' ;import { AppComponent } from './app.component' ;import { Ng2DemoComponent } from "ng2/app/ng2-demo.component" ;declare var angular : any ;angular.module ('phonecatApp' ) .directive ( 'ng2Demo' , downgradeComponent ({component : Ng2DemoComponent }) ); @NgModule ({ declarations : [ AppComponent , Ng2DemoComponent ], imports : [ BrowserModule , UpgradeModule ], entryComponents : [ Ng2DemoComponent ], providers : [], }) export class AppModule { constructor (private upgrade : UpgradeModule ) { } ngDoBootstrap ( ) { this .upgrade .bootstrap (document .body , ['phonecatApp' ], { strictDi : true }); } }
就如你在例子中看到的,这个降级的组件在AngularJS 1.x模块中注册为一个指令。为了做到,我们利用全局的变量angular 。为了告诉TypeScript这个已存在的变量,我们需要使用declare 关键字。
之后,我们可以在AngularJS 1.x模板中调用Angular组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <div class ="row" > <div class ="col-md-2" > <p > Search: <input ng-model ="$ctrl.query" /> </p > <p > Sort by: <select ng-model ="$ctrl.orderProp" > <option value ="name" > Alphabetical</option > <option value ="age" > Newest</option > </select > </p > <p > <ng2-demo > </ng2-demo > </p > </div >
如通常的AnguarJS中,我们在HTML文件中需要使用kebab惯例,而在JavaScript部分中注册指令时使用正常的命名。后者使用驼峰命名法。
当我们重新加载应用,将会同时显示AngularJS 1.x 电话列表和我们的Angular样例组件:
你可能会好奇一个新的Angular组件怎么使用AngularJS 1.x服务提供的应用逻辑,阅读下一章节来获得答案。
第四步:升级一个服务 为了能在一个新的Angular组件中使用既存的AngularJS 1.x服务,我们需要升级它。根据官方的文档,我们必须要使用factory创建一个Angular服务provider。这个factory获取一个AngularJS 1.x注入器($injector)的引用,并使用它获取服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { InjectionToken } from "@angular/core" ;export const PHONE_SERVICE = new InjectionToken <any >('PHONE_SERVICE' );export function createPhoneService (i ) { return i.get ('Phone' ); } export const phoneServiceProvider = { provide : PHONE_SERVICE , useFactory : createPhoneService, deps : ['$injector' ] }
正常情况,在provide 属性中我们可以使用服务的类型作为依赖注入符号。但是在这个例子中,我们决定不对既有的AngularJS 1.x代码升级到TypeScript,因此我们没有任何类型。因此,这个例子使用了一个基于常量的符号叫做PHONE_SERVICE 。在Angular 4+中提供的类型为InjectionToken ,在Angular 2中我们可以使用OpaqueToken 代替。InjectionToken 使用一个类型参数来判断它指向的服务类型。如提到的,我们没有这个服务的类型,因此我们仅使用any 。
这个讨论的服务provider必须要在我们的Angular模块中注册:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { InjectionToken } from "@angular/core" ;export const PHONE_SERVICE = new InjectionToken <any >('PHONE_SERVICE' );export function createPhoneService (i ) { return i.get ('Phone' ); } export const phoneServiceProvider = { provide : PHONE_SERVICE , useFactory : createPhoneService, deps : ['$injector' ] }
之后,我们可以注入phoneService 到我们的组件Ng2DemoComponent 中,并使用它价值所有的电话信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import { Component , OnInit , Inject } from '@angular/core' ;import { PHONE_SERVICE } from "ng2/app/phone.service" ;@Component ({ selector : 'ng2-demo' , template : ` <h3>Angular 2 Demo Component</h3> <img width="150" src="[...]" /> <p> {{phones.length}} Phones found. </p> ` }) export class Ng2DemoComponent implements OnInit { phones : any [] = []; constructor ( @Inject (PHONE_SERVICE) private phoneService : any ) { } ngOnInit ( ) { this .phones = this .phoneService .query (); } }
由于我们的符号是一个常量,这个实例使用Inject 装饰器来指向它。加载电话后,就可以显示数量了。
重新加载应用后,我们可以看到:
注意我们有一个Angular 1.x的组件和一个Angular组件并使用AngularJS 1.x服务提供的数据进行显示。
不仅仅是嵌套AngularJS 1.x和Angular的东西,我们还需要从各自的版本激活路由。下一节会处理相关内容。
第五步:导航到Angular组件 让AngularJS 1.x的路由来激活Angular组件是很简单的。我们仅需要配置一个路由的模板指向相应的模板即可:
1 2 3 4 5 6 7 8 9 10 $routeProvider. when ('/phones' , { template : '<phone-list></phone-list>' }). when ('/phones/:phoneId' , { template : '<phone-detail></phone-detail>' }). when ('/ng2-demo' , { template : '<ng2-demo></ng2-demo>' })
这样就允许使用Angular组件和AngularJS的路由一起使用,并可以和传统的指令和组件一起使用。
这里要强调一下,同样使用流行的UI-Router。
这个方案简单的同时,同时也有一个缺点:我们不能利用Angular路由来使用新写的组件。为了让这个成为可能,我们会实现Victor Savkin提出的Sibling Outlet approach,使两种路由共存。实现的基础是他提出的升级壳模式(Upgrade Shell pattern)。下两章会介绍如何实现这里的想法。
第六步:使用Victor Savkin的升级壳模式 Angular的主策划之一Victor Savkin提出了升级壳模式。他在他的电子书和博客中描述了升级壳模式。它正视了Angular组件在混合应用的顶层。这是升级壳包含了AngularJS构建块(指令、组件和控制器)和Angular组件。
为了实现这个模式,在开始实现文章中的努力时我们可以使用CLI生成的AppComponent :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // src/ng2/app/app.component.html <div style ="text-align:center" > <h1 > Welcome to {{title}}!! </h1 > </div > <div class ="view-container" > <div ng-view class ="view-frame" > </div > </div >
注意Angular组件中包含了AngularJS 1.x路由的ng-view 。
为了让这个组件作为我们应用的最顶层,我们需要直接启动它。我们需要将它放到AppModule 的bootstrap 数组中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import { BrowserModule } from '@angular/platform-browser' ;import { NgModule , InjectionToken } from '@angular/core' ;import { UpgradeModule , downgradeComponent } from '@angular/upgrade/static' ;import { AppComponent } from './app.component' ;import { Ng2DemoComponent } from "ng2/app/ng2-demo.component" ;import { phoneServiceProvider } from "ng2/app/phone.service" ;declare var angular : any ;angular.module ('phonecatApp' ) .directive ( 'ng2Demo' , downgradeComponent ({component : Ng2DemoComponent }) ); @NgModule ({ declarations : [ AppComponent , Ng2DemoComponent ], imports : [ BrowserModule , UpgradeModule ], entryComponents : [ Ng2DemoComponent ], providers : [ phoneServiceProvider ], bootstrap : [AppComponent ] }) export class AppModule {}
请注意,我们也需要移除手动启动应用的代码。代码被移动到AppComponent 中,并在升级壳启动后开始干活。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { Component , Inject } from '@angular/core' ;import { PHONE_SERVICE } from "ng2/app/phone.service" ;import { UpgradeModule } from "@angular/upgrade/static" ;@Component ({ selector : 'app-root' , templateUrl : './app.component.html' , styleUrls : ['./app.component.css' ] }) export class AppComponent { title = 'app' ; phones : any [] = []; constructor (private upgrade : UpgradeModule ) { } ngOnInit ( ) { this .upgrade .bootstrap (document .body , ['phonecatApp' ]); } }
并且,确保index.html 引用了我们的升级壳:
1 2 3 4 <body > <app-root > </app-root > </body >
重新加载应用,并查看包含AngularJS 1.x应用的升级壳。
当这个开始工作,我们就提供了下一章目标的基础:同时使用AngularJS 1.x和Angular路由。
第七步:使用Victor Savkin的兄弟姐妹出口来同时使用两种路由 Victor Savkin的兄弟姐妹出口 描述了一种同时使用两个版本Angular的路由方法。为了实现这一点,我们需要加载Angular路由:
1 npm install @angular/router --save
之后,扩展app.component.html 。它会各种路由获取一个出口。针对AngularJS 1.x路由我们使用带有ng-view 的div ,针对Angular路由是一个router-outlet 元素:
1 2 3 4 5 <div class ="view-container" > <div ng-view class ="view-frame" > </div > <router-outlet > </router-outlet > </div >
当激活了一个基于AngularJS 1的路由,第一个获得一个模板;当激活了一个Angular路由,后者被使用。
现在,让我们配置Angular路由:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import { BrowserModule } from '@angular/platform-browser' ;import { NgModule , InjectionToken } from '@angular/core' ;import { RouterModule } from '@angular/router' ;import { UpgradeModule , downgradeComponent } from '@angular/upgrade/static' ;import { AppComponent } from './app.component' ;import { Ng2DemoComponent } from "ng2/app/ng2-demo.component" ;import { phoneServiceProvider } from "ng2/app/phone.service" ;declare var angular : any ;angular.module ('phonecatApp' ) .directive ( 'ng2Demo' , downgradeComponent ({component : Ng2DemoComponent }) ); @NgModule ({ declarations : [ AppComponent , Ng2DemoComponent ], imports : [ BrowserModule , UpgradeModule , RouterModule .forRoot ([ { path : '' , pathMatch : 'full' , redirectTo : 'ng2-route' }, { path : 'ng2-route' , component : Ng2DemoComponent } ], { useHash : true } ) ], entryComponents : [ Ng2DemoComponent ], providers : [ phoneServiceProvider ], bootstrap : [AppComponent ] }) export class AppModule {}
就如你所看的,在这个例子中刚刚定义了Angular路由的配置。作为补充,为了两个版本的一致性,这里使用了哈希策略。
我们需要确保当AngularJS 1.x路由激活时,Angular路由不做任何事。为了做到这一点,Victor建议使用一个定制的UrlHandlingStrategy :
1 2 3 4 5 6 7 8 9 10 11 12 13 import { RouterModule , UrlHandlingStrategy } from '@angular/router' ;[...] export class CustomHandlingStrategy implements UrlHandlingStrategy { shouldProcessUrl (url ) { return url.toString ().startsWith ("/ng2-route" ) || url.toString () === "/" ; } extract (url ) { return url; } merge (url, whole ) { return url; } }
这个策略需要注册到*AppModule:
1 2 3 4 5 6 7 8 9 10 11 // src/ng2/app/app.module.ts @NgModule({ [...] providers: [ phoneServiceProvider, { provide: UrlHandlingStrategy, useClass: CustomHandlingStrategy } ], bootstrap: [AppComponent] }) export class AppModule { }
之后,我们需要对AngularJS 1.x的路由配置做一些小修改。首先,我们必须要移除配置的哈希前缀,因为这会影响Angular路由。我们必须要使用otherwise 加载一个空白模板来添加一个默认路由到版本1的出口中,当路由被其他路由处理的时候:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $routeProvider. when ('/phones' , { template : '<phone-list></phone-list>' }). when ('/phones/:phoneId' , { template : '<phone-detail></phone-detail>' }). when ('/ng2-demo' , { template : '<ng2-demo></ng2-demo>' }) .otherwise ({template : '' });
就如前面提的,使用AngularJS 1.x路由显示的一切也对流行的UI-Router适用。
之后,添加一些菜单到AppComponent 中来允许在基于AngularJS 1.x和Angular的路由间切换。
1 2 3 4 <a routerLink ="ng2-route" > ng2-route</a > |<a href ="#/phones" > Phones</a >
加载应用后,我们就可以在我们的路由间切换:
[结束]