前言
不知道還有多少人的項目還在用Angular,這篇升級實戰可以看看,今日早讀文章由@Perry翻譯投稿分享。
正文從這開始~
當我們從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應用:
ng new migrated
為了讓這個新的方案結構清晰,在src目錄下創建了一個文件夾給已有的AngularJS代碼,另一個文件夾給新的Angular代碼。
在下面的實例中,我使用了ng1和ng2來命名:
創建完之後,移動除了tsconfig.app.json, tsconfig.spec.json, favicon.ico和index.html之外的文件到ng2文件夾中。
通過.angular-cli.json文件來通知CLI的編譯任務有關修改的新代碼結構。在這個文件中使用assets欄位,我們也可以告訴CLI直接拷貝ng1文件夾到輸出的目錄中。
{
"$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下新的文件中。
<!doctype html>
<htmllang="en">
<head>
<metacharset="utf-8">
<title>Migrated</title>
<basehref="/">
<!-- ng1 -->
<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>
<!-- /ng1 -->
<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">
<!-- ng1 -->
<div class="view-container">
<div ng-viewclass="view-frame"></div>
</div>
<!-- /ng1 -->
<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模塊:
npm install @angular/upgrade --save
由於我們不想啟動Angular(2/4/5等)應用,我們將indexl.html文件中的根組件移除:
<!-- remove root component -->
<!--
<app-root></app-root>-->
現在,我們可以一起同時啟動兩個應用。為此,引入UpgradeModule模塊到Angular應用的AppModule中。從bootstrap中移除AppComponent,從而手動啟動混合應用:
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: [],
// bootstrap: [AppComponent] // No Bootstrap-Component
})
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組件,教程中會使用一個非常簡單的組件:
// src/app/ng2-demo.component.ts
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來實現:
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";
declarevar angular: any;
angular.module('phonecatApp')
.directive(
'ng2Demo',
downgradeComponent({component: Ng2DemoComponent})
);
@NgModule({
declarations: [
AppComponent,
Ng2DemoComponent
],
imports: [
BrowserModule,
UpgradeModule
],
entryComponents: [
Ng2DemoComponent // Don't forget this!!!
],
providers: [],
// bootstrap: [AppComponent] // No Bootstrap-Component
})
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組件:
<!-- src/ng1/phone-list/phone-list.template.html -->
<div class="row">
<div class="col-md-2">
<!--Sidebar content-->
<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>
<!-- Angular 2 Component -->
<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)的引用,並使用它獲取服務:
// src/ng2/app/phone.service.ts
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模塊中註冊:
// src/ng2/app/phone.service.ts
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中,並使用它價值所有的電話信息:
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組件是很簡單的。我們僅需要配置一個路由的模板指向相應的模板即可:
$routeProvider.
when('/phones', {
template: '<phone-list></phone-list>'// AngularJS 1.x template
}).
when('/phones/:phoneId', {
template: '<phone-detail></phone-detail>'// AngularJS 1.x template
}).
when('/ng2-demo', {
template: '<ng2-demo></ng2-demo>'// Angular component
})
這樣就允許使用Angular組件和AngularJS的路由一起使用,並可以和傳統的指令和組件一起使用。
這裡要強調一下,同樣使用流行的UI-Router。
這個方案簡單的同時,同時也有一個缺點:我們不能利用Angular路由來使用新寫的組件。為了讓這個成為可能,我們會實現Victor Savkin提出的Sibling Outlet approach,使兩種路由共存。實現的基礎是他提出的升級殼模式(Upgrade Shell pattern)。下兩章會介紹如何實現這裡的想法。
第六步:使用Victor Savkin的升級殼模式Angular的主策劃之一Victor Savkin提出了升級殼模式。他在他的電子書和博客中描述了升級殼模式。它正視了Angular組件在混合應用的頂層。這是升級殼包含了AngularJS構建塊(指令、組件和控制器)和Angular組件。
為了實現這個模式,在開始實現文章中的努力時我們可以使用CLI生成的AppComponent:
// src/ng2/app/app.component.html
<!--The whole content below can be removed with the new code.-->
<div style="text-align:center">
<h1>
Welcome to {{title}}!!
</h1>
</div>
<!-- ng1 -->
<div class="view-container">
<div ng-viewclass="view-frame"></div>
</div>
<!-- /ng1 -->
注意Angular組件中包含了AngularJS 1.x路由的ng-view。
為了讓這個組件作為我們應用的最頂層,我們需要直接啟動它。我們需要將它放到AppModule的bootstrap數組中:
// src/ng2/app/app.module.ts
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";
declarevar angular: any;
angular.module('phonecatApp')
.directive(
'ng2Demo',
downgradeComponent({component: Ng2DemoComponent})
);
@NgModule({
declarations: [
AppComponent,
Ng2DemoComponent
],
imports: [
BrowserModule,
UpgradeModule
],
entryComponents: [
Ng2DemoComponent // Don't forget this!!!
],
providers: [
phoneServiceProvider ],
bootstrap: [AppComponent]
})
export class AppModule {
// Remove code for bootstrapping hybrid app manually !!!
/*
constructor(private upgrade: UpgradeModule) { } ngDoBootstrap() { this.upgrade.bootstrap(document.body, ['phonecatApp'], { strictDi: true }); }*/
}
請注意,我們也需要移除手動啟動應用的代碼。代碼被移動到AppComponent中,並在升級殼啟動後開始幹活。
// src/ng2/app/app.component.ts
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引用了我們的升級殼:
<!-- src/index.html -->
<body>
<app-root></app-root>
</body>
重新加載應用,並查看包含AngularJS 1.x應用的升級殼。
當這個開始工作,我們就提供了下一章目標的基礎:同時使用AngularJS 1.x和Angular路由。
第七步:使用Victor Savkin的兄弟姐妹出口來同時使用兩種路由Victor Savkin的兄弟姐妹出口描述了一種同時使用兩個版本Angular的路由方法。為了實現這一點,我們需要加載Angular路由:
npm install @angular/router --save
之後,擴展app.component.html。它會各種路由獲取一個出口。針對AngularJS 1.x路由我們使用帶有ng-view的div,針對Angular路由是一個router-outlet元素:
<!-- src/ng2/app/app.component.html -->
<div class="view-container">
<div ng-viewclass="view-frame"></div>
<router-outlet></router-outlet>
</div>
當激活了一個基於AngularJS 1的路由,第一個獲得一個模板;當激活了一個Angular路由,後者被使用。
現在,讓我們配置Angular路由:
// src/ng2/app/app.module.ts
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";
declarevar 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:
// src/ng2/app/app.module.ts
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:
// src/ng2/app/app.module.ts
@NgModule({
[...]
providers: [
phoneServiceProvider,
{ provide: UrlHandlingStrategy, useClass: CustomHandlingStrategy }
],
bootstrap: [AppComponent]
})
export class AppModule {
}
之後,我們需要對AngularJS 1.x的路由配置做一些小修改。首先,我們必須要移除配置的哈希前綴,因為這會影響Angular路由。我們必須要使用otherwise加載一個空白模板來添加一個默認路由到版本1的出口中,當路由被其他路由處理的時候:
// src/app1/app.config.js
// No Prefix for the sake of uniformity
// $locationProvider.hashPrefix('!');
$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的路由間切換。
<!-- src/app2/app.component.html -->
<a routerLink="ng2-route">ng2-route</a> |
<a href="#/phones">Phones</a>
加載應用後,我們就可以在我們的路由間切換:
最後,為你推薦
【第1190期】完美升級 AngularJS 至 Angular
【第950期】Angular組件間通信
關於本文
譯者:@Perry
譯文:http://bookcell.org/2018/02/19/directly-upgrading-from-angularjs-to-angular-without-preparing-the-exiting-code-base/index.html
作者:@ManfredSteyer
原文:https://www.softwarearchitekt.at/post/2017/07/14/directly-upgrading-from-angularjs-1-x-to-angular-by-skipping-preparation.aspx