从Angularjs升级到Angular(译文)


如何从Angularjs 升级到 Angular(译文)

原文: Upgrading from AngularJS to Angular

Author: AngularJS 官方

译者:philoenglish.com 团队

关键字: Angularjs Angular Angular1.x Angular2.x migration 迁移, 升级

这里的Angular是指Angular 2.x, 而AngularJS 是指AngularJS 1.x版本。 Angular (通常是指 "Angular 2+" 或 "Angular v2 及更高版本") 是一个基于 TypeScript 的 开源 Web 应用框架 由 Google 的 Angular 团队以及社区共同领导。Angular 是由 AngularJS 的同一个开发团队完全重写的。

Angular 和 AngularJS 之间的区别

在设计上,Angular 是 AngularJS 的完全重写。

  • Angular 没有“作用域”或控制器的概念,其架构中的主要角色是一些层次化的组件。
  • Angular 具有不同的表达式语法,主要是用 "[ ]" 来表示属性绑定,以及用 "( )" 来表示事件绑定
  • 模块化 – 许多核心功能都已模块化
  • Angular 建议使用 Microsoft 的 TypeScript 语言,该语言引入了如下特性:
    • 静态类型,包括 泛型
    • 装饰器,语法上类似于注解
  • TypeScript 是 ECMAScript 6 (ES6) 的超集,并且与 ECMAScript 5 (即: JavaScript) 向下兼容。
  • 动态加载
  • 异步模板编译
  • RxJS 提供了迭代式回调。RxJS 在状态可见性和调试方面有局限,不过可以使用诸如 ngReact 或 ngrx 之类的响应式第三方库来解决这些问题
  • 支持 Angular Universal,它可以在服务器上运行 Angular 应用程序

升级注意事项

  • 虽然AngularJS版本很低了,但是用AngularJS应用程序很棒。在迁移到Angular之前,请务必根据自身的实际情况考虑是否升级的必要性, 以及所需花费的时间和精力是否合算。 本指南介绍了用于将 AngularJS 项目高效迁移到 Angular平台的内置工具。

  • 某些应用程序将比其他应用程序更容易升级,并且有很多方法可以使自己更容易升级。甚至可以在开始升级过程之前准备 AngularJS 应用程序并将其与 Angular 的技术栈, 架构模式对齐。这些准备步骤都是为了让代码更分离、更易于维护,并且更好地与现代开发工具保持一致。这意味着除了使升级更容易之外,您还将改进现有的AngularJS应用程序。

  • 成功升级的关键之一是以增量方式进行升级,方法是在同一应用程序中并行运行两个框架,并将AngularJS组件逐个移植到Angular。这样,即使是大型和复杂的应用程序,也可以在不中断其他业务的情况下进行升级,因为工作可以协作完成,并且可以在一段时间内分散。
    Angular 中的升级模块旨在使增量升级无缝衔接。

升级前准备工作

  • 当前有很多方法可以规划AngularJS应用程序的结构。当您开始将这些应用程序升级到Angular时,可以考虑将一些面向未来的工具或应用程序先应用到当前的AngularJS应用程序上,这样会使得迁移过程比不使用这些工具或程序或使用其它蹩脚的工具更轻松一些。

遵循 AngularJS 风格指南

AngularJS风格指南收集了已被证明可以产生更简洁、更易于维护的 AngularJS 应用程序的模式和实践。它包含了大量有关如何编写和组织AngularJS代码的资料以及一些反例。

Angular是AngularJS最佳部分的重新构想版本。从这个意义上说,它的目标与AngularJS的风格指南相同:保留AngularJS的良好部分,并避免坏部分。 遵循风格指南有助于使您的AngularJS应用程序与Angular更加紧密地保持一致,

其中有一些规则可以使得通过 Angular upgrade/static模块模式进行增量升级变得更加轻松:

  • 规则 1: 一个组件应该只有一个文件。 这样不仅使得导航和查找组件更容易易,而且还允许一次性将其迁移到不同语言不同框架。在此示例应用程序中,每个控制器、组件、服务和筛选器都位于其自己的源文件中。
  • 规则 2: 文件夹按功能组织文件和模块化规则, 不同的功能模块应该存放在一起, 不同功能模块应该在不同的模块中, 功能应该是判断相识性的依据。

当应用程序以功能特性组织时,可以一次迁移一个功能。即使不是为了升级, 这也是对规划应用的结构有益的忠告。 对于不符合AngularJS 风格指南的应用程序, 强烈建议是先遵循 AngularJS 风格指南, 再开始迁移。

使用打包工具

当您将应用程序打散为一个组件一个文件后, 您通常最终会得到一个包含大量较小的文件的项目结构。这是一种比少量大文件更整洁的组织方式,但是如果您必须将所有这些文件加载到浏览器时,则效果不佳,特别是当您还必须维护他们之间的依赖关系时时。这就是为什么需要开始使用打包工具的原因。

使用诸如SystemJS, Webpack, gulp, grunt 或Browserify之类的打包工具,我们可以使用TypeScript或ES2015的内置模块系统。您可以使用导入和导出功能,这些功能显式指定哪些代码可以并且将在应用程序的不同部分之间共享。对于ES5应用程序,您可以使用CommonJS样式要求和module.exports功能。在这两种情况下,模块加载器将负责以正确的顺序加载应用程序所需的所有代码。

在将应用投入生产时,模块装载机还可以更轻松地将它们全部打包到成bundle包。

迁移到typescript

如果 Angular 升级计划的一部分是同时使用 TypeScript,那么甚至在升级本身开始之前引入 TypeScript 编译器也是有意义的。这意味着在实际升级过程中,需要学习和思考的事情少了一件事。这也意味着你可以开始在AngularJS代码中使用TypeScript功能。
由于 TypeScript 是 ECMAScript 2015 的超集,而 ECMAScript 2015 又是 ECMAScript 5 的超集,因此"切换到"TypeScript 并不一定需要安装 TypeScript 编译器并将文件从 *.js 重命名为 *.ts。但是,当然,仅仅这样做并不是非常有用或令人兴奋。诸如下面的其他步骤可以给我们带来更多的收益:

  • 对于使用module loader的应用程序,TypeScript 导入和导出(实际上是 ECMAScript 2015 导入和导出)可用于将代码组织到模块中。
  • 类型注释可以逐渐添加到现有函数和变量中,以固定其类型并获得诸如构建时错误检查,出色的自动完成支持和内联文档等好处。
  • ES2015的JavaScript功能,如箭头函数,lets和consts,默认函数参数和解构赋值也可以逐步添加,以使代码更具表现力。
  • service和controller可以转换为类。这样,它们将更接近成为Angular服务和组件类,这将使升级更轻松。

使用 Component Directives

在Angular中,Component是构建用户界面的主要组成部分。将 UI 的不同部分定义为组件,并将它们组合成完整的页面。

您也可以在 AngularJS 中使用Component directive做相同的事情。 这些是定义自己的模板,控制器和输入/输出绑定的指令 - 与Angular组件定义的内容相同。与使用 ng 控制器、ng 包含和作用域继承等较低级别功能构建的应用程序相比,使用组件指令构建的应用程序更容易迁移到 Angular。

要与 Angular 兼容,AngularJS Component directive应该配置如下属性:

  • restrict: 'E'组件通常用作元素。

  • scope: {} - 隔离作用域。在Angular中,组件始终与周围环境隔离,您也可以在AngularJS中执行此操作。

  • bindToController: {}.组件输入和输出应绑定到控制器,而不是使用$scope。

  • controller 和 controllerAs, 组件有自己的控制器。

  • template or templateUrl, 组件有自己的模板。

Component directive还可以使用以下属性:

  • transclude:true/{},如果组件需要超越来自其他地方的内容。
  • require,如果组件需要与某个父组件的控制器进行通信。

组件指令不应使用以下属性:

  • compile, 这在 Angular 中不受支持。

  • replace: true, Angular 从不将组件元素替换为组件模板。此属性在 AngularJS 中也被弃用。

  • priority 和 terminal。虽然AngularJS组件可以使用这些,但它们不在Angular中使用,最好不要再使用这两个属性。

一个能与 Angular 完全一致的 AngularJS Component directive示例:

hero-detail.directive.ts  
export function heroDetailDirective() {
  return {
    restrict: 'E',
    scope: {},
    bindToController: {
      hero: '=',
      deleted: '&'
    },
    template: `
      

{{$ctrl.hero.name}} details!

{{$ctrl.hero.id}}
`, controller: function HeroDetailController() { this.onDelete = () => { this.deleted({hero: this.hero}); }; }, controllerAs: '$ctrl' }; }

从AngularJS 1.5 开始引入了Component,可以更轻松地定义此类Component directive。将此Component API 用于组件指令是一个好主意,原因如下:

  • 它需要较少的样板代码。
  • 它强制使用controllerAs这种组件的最佳实践。
  • 它具有更好的默认值, 例如scope和restrict等为指令属性。

上面的Component directive示例在使用Component 表示时如下所示:

export const heroDetail = {
  bindings: {
    hero: '<',
    deleted: '&'
  },
  template: `
    

{{$ctrl.hero.name}} details!

{{$ctrl.hero.id}}
`, controller: function HeroDetailController() { this.onDelete = () => { this.deleted(this.hero); }; } };

Controller生命周期钩子方法$onInit()、$onDestroy()和$onChanges()是从AngularJS 1.5开始引入的其他方便的功能。它们在 Angular 中几乎都有完全相同的功能,因此围绕它们组织组件生命周期逻辑将简化最终的 Angular 升级过程。

借助 ngUpgrade 包进行升级工作

Angular中的ngUpgrade包是一个非常有用的升级工具。有了它,您可以在同一应用程序中混合使用AngularJS和Angular组件,并使它们无缝衔接。这意味着您不必一次完成所有升级工作,因为在过渡期间,两个框架之间存在自然共存。

AngularJS的生命周期将于2021年12月31日结束。ngUpgrade 现在处于功能完整可用状态。我们继续发布 ngUpgrade 的安全和错误修复直到 2022 年 12 月 31 日。

ngUpgrade 工作原理

ngUpgrade提供的主要工具之一称为UpgradeModule。这是一个包含用于引导和支持 Angular 和 AngularJS 混合开发的实用程序模块。

当你使用ngUpgrade时,你真正要做的是同时运行AngularJS和Angular。所有 Angular 代码都在 Angular 框架中运行,AngularJS 代码在 AngularJS 框架中运行。这两者都拥有框架的完整功能和特性。不是模拟仿真,因此您可以期望同时拥有两个框架的所有功能和自然行为。

除此之外,由一个框架管理的组件和服务可以与另一个框架中的组件和服务进行交互。这主要包括:依赖注入、DOM 和数据感知。

依赖注入

依赖注入在AngularJS和Angular中都是前端开发的前沿技术和核心功能,但是这两个框架在实际工作方式上存在一些重要的差异。

ANGULARJS ANGULAR
依赖关系注入的tokens始终是字符串 依赖关系注入的tokens可以具有不同的类型。它们通常是类, 它们也可能是字符串。
全局仅有一个injector。即使在多模块应用程序中,所有内容都导入一个大的命名空间中. 有一个injector的树状结构,每个组件都有一个根injector和一个附加的injector。

即使有这样大的差异,您仍然可以拥有依赖注入互操作性。upgrade/static解决了这些差异,使一切可以无缝工作:
您可以通过升级 AngularJS service来将其注入 Angular 代码。每个单例服务能在框架之间共享。在 Angular 中,这些服务将始终位于根注入器中,并可供所有组件使用。

您也可以通过降级 Angular 服务来将其注入 AngularJS 代码。只有来自 Angular 根注入器的服务才能降级。同样,相同的单例实例服务在框架之间共享。注册降级的服务时,必须显式指定要在 AngularJS 中使用的字符串token。

组件 和 DOM

在混合模式的DOM中,有来自AngularJS的组件和指令也有Angular的组件和指令。这些组件通过使用各自框架的双向通道相互通信,通过ngUpgrade组件进行桥接。也还可以通过共享的对象进行通信, 比如共享服务。

关于混合应用程序,要了解的关键是: DOM 中的每个元素都由两个框架中的一个拥有。另一个框架忽略了它。如果一个元素归 AngularJS 所有,Angular 会将其视为不存在,反之亦然。

因此,通常混合应用程序作为AngularJS应用程序开始启动,由AngularJS处理root template,例如,index.html。然后,当遇到Angular指令或组件时Angular才参与进来, 相应指令或组件后续也由Angular负责管理,即使template包含任意数量的 Angular 组件和指令, 也能被管理起来。

除此之外,两个框架之间的组件还可以进行交互。 您通过以下两种方式之一跨越两个框架之间的边界:

通过使用一个框架元素使用另一个框架中的元素, 例如 在AngularJS 的模板中使用 Angular 组件 ,或在Angular 模板中使用 AngularJS 组件。

通过包含或投影来自其他框架的内容。ngUpgrade 将 AngularJS transclusion 和 Angular 内容投影的相关概念桥接在一起。

每当在一个框架的模板中使用属于另一个框架的组件时,都会在框架边界之间发生切换。但是,这种切换仅发生在该组件的模板中的元素上, 不会涉及到其他部分。
例如使用AngularJS模板中的Angular定义的组件的场景,如下所示:


虽然DOM元素

你可以从 HTML 中删除 ng-app 和 ng-strict-di 指令,而是切换到从 JavaScript 调用 angular.bootstrap,像这样:

angular.bootstrap(document.body, ['heroApp'], { strictDi: true });

要开始将 AngularJS 应用程序转换为混合应用程序,您需要加载 Angular 框架。 至于如何加载Angular 框架,可以参考这篇文章Setup for Upgrading to AngularJS中的详细说明。 根据项目的需求,从项目QuickStart github repository拷贝一些代码来使用.

您还需要使用 npm 安装@angular/upgrade --save 来安装@angular/升级软件包,并为@angular/升级/静态软件包添加映射:

要构建混合应用, 有一个npm包是必须安装的, 他们分别是@angular/upgrade

 npm install @angular/upgrade --save

然后要在配置文件中添加一条映射:
systemjs.config.js

'@angular/upgrade/static': 'npm:@angular/upgrade/fesm2015/static.mjs',

接着要创建一个模块文件app.module.ts, 引入并配置NgModule class

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['heroApp'], { strictDi: true });
  }
}

在NgModule decorator配置中, 至少需要导入BrowserModule, 这是每个基于Angular浏览器的应用程序都必须具有的模块, 还从@angular/upgrade/static 导入 UpgradeModule,UpgradeModule用于升级和降级服务和组件需要用到的模块。

在 AppModule 的构造函数中,使用依赖关系注入来获取 UpgradeModule 实例,并使用它来引导 AppModule.ngDoBootstrap 方法中的 AngularJS 应用程序。upgrade.bootstrap 方法采用与 angular.bootstrap 完全相同的参数

注意: 您无需将bootstrap 声明添加到@NgModule装饰器上,因为 AngularJS 将掌管应用程序的root template。

现在,您可以使用AppModuleBrowserDynamic.bootstrapModule 方法引导AppModule。

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

platformBrowserDynamic().bootstrapModule(AppModule);

这一步完成后, 恭喜你!您正在运行混合应用程序!现有的 AngularJS 代码与以前一样工作,您已准备好开始添加 Angular 代码。

在 AngularJS 代码使用 Angular Components

运行混合应用后,可以开始逐步升级代码。执行此操作的更常见模式之一是在 AngularJS 上下文中使用 Angular 组件。这可能是一个全新的组件,或者以前是AngularJS但已经为Angular重写的组件。

假设您有一个 Angular 组件如下,用于显示有关Hero的信息:
hero-detail.component.ts

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

@Component({
  selector: 'hero-detail',
  template: `
    

Windstorm details!

1
` }) export class HeroDetailComponent { }

如果你想在Anguarjs代码中使用这个组件, 你需要使用downgradeComponent()方法先将angular组件降级, 得到一个Angular一个AngularJS的directive后将它注册到AngularJS module, 这样就可以在AngularJS模板中使用了.

import { HeroDetailComponent } from './hero-detail.component';

/* . . . */

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

angular.module('heroApp', [])
  .directive(
    'heroDetail',
    downgradeComponent({ component: HeroDetailComponent }) as angular.IDirectiveFactory
  );

默认情况下,对于每个AngularJS $digest周期, Angular 数据感知将在组件上运行。如果只想在输入更改时运行数据感知,则可以在调用downgradeComponent()时将 propagateDigest 设置为 false。

由于 HeroDetailComponent 是一个 Angular 组件,因此您还必须将其添加到 AppModule 中的声明中。

import { HeroDetailComponent } from './hero-detail.component';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  declarations: [
    HeroDetailComponent
  ]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['heroApp'], { strictDi: true });
  }
}

所有 Angular 组件、指令和管道都必须在 NgModule 中声明。

最终结果是一个名为heroDetail的AngularJS指令创建成功,您可以像AngularJS模板中的任何其他指令一样使用它。


注意: heroDetail是一个AngularJS元素指令(restrict:‘E’)。AngularJS 元素指令根据其名称进行匹配。降级的 Angular 组件的selector元数据将被忽略。

当然,大多数组件都不是这么简单。他们中的许多人都有input和output,将他们与外部世界联系起来。具有输入和输出的 Angular hero 组件可能如下所示:

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Hero } from '../hero';

@Component({
  selector: 'hero-detail',
  template: `
    

{{hero.name}} details!

{{hero.id}}
` }) export class HeroDetailComponent { @Input() hero!: Hero; @Output() deleted = new EventEmitter(); onDelete() { this.deleted.emit(this.hero); } }

这些输入和输出可以从AngularJS模板提供,downgradeComponent()()方法负责连接它们:

即使您位于 AngularJS 模板中,您也使用 Angular 属性语法来绑定输入和输出。这是降级组件的要求。表达式本身仍然是AngularJS正则表达式。

使用降级组件的 KEBAB-CASE 写法

对于降级的组件使用 Angular 属性语法的规则时,有一个值得注意的例外。当属性名称由多个单词组成时。在 Angular中,您可以使用 camelCase 绑定这些属性:

[myHero]="hero"
(heroDeleted)="handleHeroDeleted($event)"

但是,在AngularJS模板中使用它们时,您必须使用kebab-case:

[my-hero]="hero"
(hero-deleted)="handleHeroDeleted($event)"

看出差别来了吗? AngularJS模板中使用中划线分割, 而Angular是驼峰风格。

由于这是一个 AngularJS template,您仍然可以在元素上使用其他 AngularJS 指令,即使它具有 Angular 绑定属性。例如,您可以使用 ng-repeat 轻松创建组件的多个副本:

在 Angular 代码使用 AngularJS Components

因此,您可以编写一个 Angular 组件,然后从 AngularJS 代码中使用它。当您开始从较低级别的组件迁移并逐步向上移动时,这很有用。但在某些情况下,以相反的顺序做事会更方便:从更高级别的组件开始,然后向下工作。这也可以使用 upgrade/static来完成。您可以升级 AngularJS 组件指令,然后从 Angular 使用它们。

并非所有种类的 AngularJS 指令都可以升级。该指令实际上必须是组件指令,具有准备指南中描述的特征。为了确保兼容性, 最安全的选择是使用AngularJS 1.5或以上的版本。

下面是一个可升级组件示例,该组件仅具有模板和控制器:

export const heroDetail = {
  template: `
    

Windstorm details!

1
`, controller: function HeroDetailController() { } };

您可以使用 UpgradeComponent 类将此组件升级到 Angular元素。 方法是通过继承 UpgradeComponent 来创建 Angular 指令并在其构造函数执行父类的构造方法, 这样您将拥有一个完全升级的 AngularJS 组件,可在 Angular 内部使用。剩下的就是将其添加到 AppModule 的声明数组中。

import { Directive, ElementRef, Injector, SimpleChanges } from '@angular/core';
import { UpgradeComponent } from '@angular/upgrade/static';

@Directive({
  selector: 'hero-detail'
})
export class HeroDetailDirective extends UpgradeComponent {
  constructor(elementRef: ElementRef, injector: Injector) {
    super('heroDetail', elementRef, injector);
  }
}
@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  declarations: [
    HeroDetailDirective,
  /* . . . */
  ]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['heroApp'], { strictDi: true });
  }
}

升级后的组件是一个Angular指令,而不是组件,因为Angular不知道AngularJS将在它下面创建元素。据Angular所知,升级后的组件只是一个指令——一个标签——Angular不必关心它的元素内部的内容。

升级后的组件还可以具有输入和输出,如原始 AngularJS 组件指令的作用域/控制器绑定所定义。使用 Angular 模板中的组件时,请按照以下规则使用 Angular 模板语法提供输入和输出:

BINDING DEFINITION TEMPLATE SYNTAX
Attribute binding myAttribute: '@myAttribute'
Expression binding myOutput: '&myOutput'
One-way binding myValue: '
Two-way binding myValue: '=myValue' As a two-way binding: Since most AngularJS two-way bindings actually only need a one-way binding in practice, is often enough.

例如,想象一个带有一个输入和一个输出的英雄细节AngularJS组件指令:

export const heroDetail = {
  bindings: {
    hero: '<',
    deleted: '&'
  },
  template: `
    

{{$ctrl.hero.name}} details!

{{$ctrl.hero.id}}
`, controller: function HeroDetailController() { this.onDelete = () => { this.deleted(this.hero); }; } };

您可以将此组件升级到 Angular,在升级指令中注释输入和输出,然后使用 Angular 模板语法提供输入和输出:

TODOimport { Directive, ElementRef, Injector, Input, Output, EventEmitter } from '@angular/core';
import { UpgradeComponent } from '@angular/upgrade/static';
import { Hero } from '../hero';

@Directive({
  selector: 'hero-detail'
})
export class HeroDetailDirective extends UpgradeComponent {
  @Input() hero: Hero;
  @Output() deleted: EventEmitter;

  constructor(elementRef: ElementRef, injector: Injector) {
    super('heroDetail', elementRef, injector);
  }
}

参考文档

Upgrading from AngularJS to Angular

Angular 官网

Angular 官方文档

Angular 中文文档

Angular教程_Angular8 Angular9 Angular12入门实战视频教程-2021年更新【IT营】

Angular1.x + TypeScript 编码风格