总结一下几个月时间加班赶工升级的两个项目,一个项目使用的是UpgradeModule,另一个使用的是DowngradeModule,如下做个记录:(ng1->AngularJS; ng2+->Angular2+),具体的可以看官方文档:https://angular.io/guide/upgrade
需要用到的typescript的优缺点和使用方法就不在这里赘述了。
一、 UpgradeModule:这个是Angular最早放出来的并行升级方式,缺点是性能方面比较差,理解起来还比较简单,就是先创建一个Angular2+的app,然后把ng1的controller/service/directive之类的直接包裹一下,让他们在ng2+的环境下直接跑。
Step by step:
* 从ng2 app 启动:
** 从index.html删除ng-app;
** 加main.ts:
import { NgModule } from '@angular/core';
import { UpgradeModule } from '@angular/upgrade/static';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule).then(platformRef => {
const upgrade = platformRef.injector.get(UpgradeModule) as UpgradeModule;
upgrade.bootstrap(document.body, ['carepilot'], { strictDi: true });
});
** 加app.module.ts: 所有的模块都在这里定义
import { NgModule, forwardRef } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
@NgModule({
imports: [
BrowserModule,
UpgradeModule
],
bootstrap: []
})
export class AppModule {
ngDoBootstrap() {}
}
** 加tsconfig.json:定义怎么编译ts->js
{
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noImplicitAny": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2015",
"dom"
]
},
"exclude": [
"node_modules"
]
}
** systemjs.config.js: 用systemjs加载依赖的模块,在并行转换的时候一般的例子都是使用systemJS来加载模块;
/**
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*/
(function (global) {
System.config({
paths: {
'npm:': 'node_modules/'
},
map: {
app: 'app',
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'rxjs': 'npm:rxjs'
},
packages: {
app: {
defaultExtension: 'js'
},
rxjs: {
defaultExtension: 'js',
format: 'cjs'
}
}
});
})(this);
** 改index.html:需要加载一些ng2需要的库(注意:下面的几个zone.js等等,需要自己使用gulp之类的工具拷贝到你的运行目录中)
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<!-- inject:js -->
<!-- endinject -->
<script>
System.import('main.js')
.then(null, console.error.bind(console));
</script>
现在,这个hybrid的app应该可以跑起来了,下面的工作就是把controller一个一个升级,一个个踩坑的过程;
简单来说,升级的时候controller->component, directive -> component/directive, service -> service, pipe/filter -> pipe;
* 例子:升级一个controller到component
import {
Component,
Inject
} from '@angular/core';
import { Broadcaster } from '../../services/broadcaster';
@Component({
selector: 'about',
templateUrl: './about.component.html'
})
export class AboutComponent {
constructor(private broadcaster: Broadcaster) {
}
print(): void {
this.broadcaster.broadcast('printContents', 'printable');
}
}
* 因为程序里大部分还是ng1的代码,为了能让ng1和ng2+的东西能沟通,需要把ng2+的模块先downgrade:
angular.module('interestApp')
.directive('pinControls',
upgradeAdapter.downgradeNg2Component(PinControlsComponent));
更新config.js里的路由(假设使用ui-state):
.state('add', {
template: "<add-pin></add-pin>",
url: '/add'
* 同样的问题,一些controller升级了以后需要依赖一些以前的service,这时候需要暂时将ng1的service升级到ng2+,以供ng2+的component使用,我在升级过程中只遇到ui-route需要这么做,这时候需要用types,可以到这里找对应的库的定义http://definitelytyped.org/docs/angular-ui--angular-ui-router/modules/ng.html
* 升级后,最别扭的就是$scope和$rootscope,在ng1里头,全局的rootscope用的爽的不得了,在ng2+里头取消了,需要自己定义一个service来替代以前的rootscope,现在的component不能直接使用scope用= , >这样来绑定值了,要用@input和@output把值传入和传出来,对应于以前的=,>传值过程;
* 使用ng2+的Router定义路由:当然也可以继续使用ui-route的升级版@ui-route;
const AppRouting: Routes = [
{ path: 'app', component: DashboardComponent, canLoad: [AuthGuard], canActivate: [AuthGuard], data: { pageTitle: 'Dashboard' }];
* 经过痛苦的斗争(新方法的使用,习惯的改变,原来一些ng1的库没人维护也没人升级到ng2+,找替代品或者自己写一个
),终于把ng1的东西都升级完了,这时候该删除ng1的遗留和支持程序了,变成一个完全的ng2+程序了。
** 删除upgrademodule:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
** 删除全部的upgrade 模块、downgrade模块;
** 加一个appcomponent用来加载初始的路由:(这里假设你用ng2+默认的route, 如果继续使用ui-route,继续使用ui-view即可)
import { Component } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-root',
template: `
<div class="main-page">
<router-outlet></router-outlet>
</div>
`
})
export class AppComponent {
constructor(private route: Router) {
let loc = window.location.pathname.toString();
if (loc == '/login' || loc == '/') {
this.route.navigate(['login']);
}
}
}
* 马上就成功了,最后使用angular-cli来管理编译过程和模块加载,这东西是个好玩意(虽然也有一些不如人意的地方,比如release不支持JIT等等的问题
)
- 现在只需要npm就好了(有人用yarn),bower可以下岗了.
- Systemjs也就可以谢幕了;
- Gulp 或者grunt这时候需要具体情况具体分析,如果服务端比较复杂,建议还是保留,用来启动服务端,使用concurrent就可以在npm run dev时候angular-cli管客户端代码,gulp/grunt管服务端;
现在,全部的升级就完成了,当然实际过程肯定会遇到种种不如意,具体情况具体对待吧,这种方式如果项目小的话可以采用,比较好理解,网上的例子也多,很快升级完。如果项目比较大,需要几个月时间升级的话,那就要使用下面的downgrade的方式了。
二、downgrade:官方的介绍惜墨如金,给第一次弄这玩意的人挺难理解的。
原理:downgrade是不改变原来的ng1程序,新建一个ng2+程序,然后把这个程序做成一个模块,直接注入到ng1里头,这样会避免一些额外的change事件,从而避免了不必要的消息循环,这样来提升性能。
用下面的5步升级到ng2+:
第一步:写一个新的ng2+程序,然后注入到ng1里头:
main.ts: (XXXXXX替换成你的ng1的app的名字就行了)
import { NgModule, forwardRef, StaticProvider } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { downgradeModule, downgradeComponent, setAngularLib } from '@angular/upgrade/static';
declare const angular: any;
declare const window: any;
const bootstrapFn = (extraProviders: StaticProvider[]) => {
const platformRef = platformBrowserDynamic(extraProviders);
return platformRef.bootstrapModule(AppModule);
};
const downgradedModule = downgradeModule(bootstrapFn);
angular.module('XXXXXX.ng2', [
downgradedModule
]);
angular.module('XXXXXX.ng2').directive('template', downgradeComponent({ component: template}));
* app.js (就这里麻烦,试了一天才试出来,这么注入,然后加载)
(function() {
angular.module('XXXXXXX', [
// all the old modules
'XXXXXXX.ng2'
]);
// eslint-disable-next-line
if (window.ignoreNg2ForTest) {
// temporary not load ng2 module in karma test
fetchData().then(bootstrapApplication);
} else {
// normal hybrid entrance
loadMain().then(fetchData).then(bootstrapApplication);
}
function loadMain() {
return System.import('main'); // load main.ts
}
function fetchData() {
// old logic
}
function bootstrapApplication() {
angular.element(document).ready(function() {
angular.bootstrap(document, ['XXXXXXXX'], { strictDi: true }); // bootstrap app with ng1
});
}
}());
* 同样,在index.html里加入一些依赖(升级完都可以删)
<script src="node_modules/systemjs/dist/system.js"></script>
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
* 手动拷一些依赖到dest 文件夹:(我们用的gulp)
pipes.builtAngularVendorScriptsDev = function() {
return gulp.src([
'./node_modules/core-js/client/shim.min.js',
............
], {base: './node_modules'})
.pipe(gulp.dest('dist.dev/node_modules'));
};
* JSPM: 不用这东西的话release 里头的客户端有近百M,会疯的,虽然这个东西也bug重重,临时用用还是可以的。用它以后release的客户端代码只有十几M,可以接受了。
gulp.task('build-jspm-prod', ['hybrid-tsbuild'], function() {
return jspm({
config: './client/systemjs.config.js',
bundleOptions: {
minify: true,
mangle: false
},
bundleSfx: true,
bundles: [
{ src: paths.tsOutput + '/*', dst: 'main.js' }
]
}).pipe(gulp.dest('dist.prod'));
});
第二步:升级一个个的controller/service/pipe/directive等等,这个过程相当痛苦,原来代码的耦合,莫名其妙的event emit和不知道在哪儿的event handler。全局的rootscope遍地都是,牵一发动全身。自己解耦吧,没什么好办法。
第三步:升级路由,这个虽然没有上一步那么煎熬,也很头疼,很多地方都要改,
* 如果用route的话参见上面upgrademodule里的例子就好了;
* 如果使用@ui-route的话,state现在需要这么定义:(abstract我还没有研究出来有什么用)
export const registerState = {
parent: 'app',
name: 'register',
url: '/register/{activationCode}',
component: RegisterComponent,
data: { pageTitle: 'Register' }
};
第四步:删除ng1的东西;
第五步:使用angular-cli管理依赖:
* 参照上面upgrademodule对应的步骤就好;
* 这时候可以删除JSPM了,这东西和SystemJS都是定时炸弹,bug重重啊,目前angular-cli比较稳定;
到这里,应该你的程序已经升级到了ng2+,目前是ng5,建议升级过程中把anglar相应的版本写死,这东西更新太快,你随着它升随时出现已有的包不兼容,很头疼,我们暂时定格在5.0。
我是升级完了站着说话不腰疼,上面的东西只能帮有需要的朋友节约一点点时间,最大的时间消耗在升级controller和解耦的过程中。那里如果有什么问题可以在下面留言,能帮上的话尽量帮大家省省时间。
备注一:替换规则。
- ng-bind-html [innerHTML]
- ng-model [(ngModel)] // 这个和ng1的双向绑定基本一个意思
- ng-class [ngClass]
- ng-disabled [disabled]
- ng-click (click)
- ng-if *ngIf
- ng-repeat *ngFor // E.g. *ngFor="let task of currentChecklist.open; let i = index;"
- ng-show *ngIf
- ng-src [src]
- ng-hide *ngIf
- ng-submit (ngSubmit)
- ng-style [ngStyle]
备注二:可能遇到的问题
- JS:
- 消息处理:ng2+里没有了scope继承,所以自己定义全局的消息处理是必要的;
- HTML模板:
- directive冲突:定义的名字最好分开,ng1里叫<my-input的话,ng2+里最好叫<ng-my-input,不然冲突.