In January, we completed the translation of our Vimbox platform from AngularJS to Angular 4 in Skyeng. During the preparation and transition, we accumulated a lot of entries on planning, problem solving and new work conventions, and we decided to share them in three articles on Habré. We hope that our notes will be useful structurally similar to our Vimbox projects that have just started to move or are going to do it.
First, Angular is better at all than AngularJS - it is faster, easier, more convenient, it has fewer bugs (for example, typing templates helps to combat them). This is a lot said and written, it makes no sense to repeat. It was clear from Angular 2, but a year ago it was scary to start a transition: what if Google again decides to turn everything upside down with the next version, without backward compatibility? We have a big project, the transition to an essentially new framework requires serious resources, and we don’t want to do it once every two years. Angular 4 lets hope that there will be no more revolutions, which means that it is time to migrate.
Secondly, we wanted to update the technologies used in our platform. If this is not done according to the principle “if something doesn’t break, you don’t need to repair it”, at some point we will pass the line beyond which further progress will be possible only if the platform is rewritten from scratch. Sooner or later, switching over to Angular will be all the same, but the sooner this is done, the cheaper the transition will be (the amount of code increases all the time, and we will get benefits from the new technology earlier).
Finally, a third important reason: developers. AngularJS - passed stage, it performs its tasks, but does not develop and will never develop; our platform is constantly growing. We do not have a very large team consisting of strong developers, and strong developers are always interested in new technologies, they are just not interested in dealing with an outdated framework. The transition to Angular makes our jobs more interesting for strong candidates; in the next two or three years they will be quite relevant.
You can perform the transition in parallel mode - the platform works on AngularJS, we write from scratch and test the new version, and at some point simply switch the toggle switch. The second option is a hybrid mode, where changes occur directly on the production, where both AngularJS and Angular work simultaneously. Fortunately, this mode is well thought out and documented .
The choice between hybrid and parallel transition modes depends on how actively the product is developing. Our developer, who prepared the event plan, had experience of a parallel approach in another company - but in the event there were fewer dependencies (although the code was about the same), and most importantly, it was possible for a month to stop all development and deal only with the transition. The choice of mode depends on whether you can afford this luxury.
For us, in a parallel transition, there was a risk: at the time of the preparation of the new version, the entire development stops, and no matter how competently we calculate the move date, there is a chance that the process will be delayed, we will rest on something and will not understand what to do next. In the hybrid mode in this situation, we can just stop and calmly look for a solution, since we still have the current working version on production; it may not work as effectively and a bit harder, but no processes are stopped. In parallel, we would have rolled back with the corresponding losses. It is worth noting that our transition process really took a long time - it was planned for 412 hours, in fact it turned out twice as long (830). But at the same time, nothing stopped, the new functionality was constantly rolling out, everything worked as it should.
In general, it should be borne in mind that the hybrid transition is not force majeure, it is a completely normal, default procedure, according to the developers of Angular himself; it is not necessary to be afraid of him.
The sequence of actions looked like this:
head
, all work with the title / favicon / meta tags is placed in the services that directly interact with the necessary elements in the header.Well, now let's move on to the promised technical details. We cleaned these records a bit, removing unnecessary details relating only to our platform. These are not at all universal solutions, but maybe they will help someone to solve problems that arise.
In order not to fence the wall of the text, we hide everything under the spoilers.
If in the module in which we start to upgrade something, there is no an angular module, then we create it and attach it to the main application module:
import {NgModule} from "@angular/core"; @NgModule({ // }); export class SmthModule {} @NgModule({ imports: [ ... SmthModule, ], }); export class AppModule {}
If the angularization module is still alive, then the new module is called with the postfix .new
. We cut out postfix along with the old angular module.
In a good case, we add a decorator, remove the default
from export, manage imports (because the default has been removed), import into the anguly module, downgrade to the anguard module:
import {Injectable} from "@angular/core"; @Injectable() export class SmthService { ... } // angular module @NgModule({ providers: [ ... SmthService, ], }); // angularjs module import {downgradeInjectable} from "@angular/upgrade/static"; ... .factory("vim.smth", downgradeInjectable(SmthService))
The service remains available under the old name in Angurarzhs and does not require additional configuration.
A good option implies: all injecting services have already moved to Angulyar, no specific items like templateCache
or compiler
.
In the remaining 95% of cases, we suffer, first upgrading what is injected, getting rid of all sorts of weird angularies, etc.
We drop the decorator with the metadata to the controller, put the decorators into inputs / outputs and transfer them to the beginning of the class:
import {Component, Input, Output, EventEmitter} from "@angular/core"; @Component({ // `-` , camelCase selector: "vim-smth", // require("./smth.html") templateUrl: "smth.html", }) export class SmthComponent { @Input() smth1: string; @Output() smthAction = new EventEmitter<void>(); ... } // angular module @NgModule({ declarations: [ ... SmthComponent, ], // , exports: [ ... SmthComponent, ], }); // angularjs module import {downgradeInjectable} from "@angular/upgrade/static"; ... .directive("vimSmth", downgradeComponent({ component: SmthComponent }) as ng.IDirectiveFactory)
All injected services, all require components (how to cling them - below to Any ) and all components / directives / filters used inside the template should be on an angular basis.
All component variables used in the template must be declared as public
, otherwise it will fall on the AoT assembly.
If the component receives all the data for output from the component above (via input), then feel free to write to it in the meta-data changeDetection: ChangeDetectionStrategy.OnPush
. This tells the angular that it will only sync the data template (let change detection for this component) if any of the components of the component change. Ideally, most of the components should be in this mode (but we are unlikely, because very large components that receive data for output via services).
Same as the component, only there is no template and the @Directive
decorator. It is thrown into the module there, it is necessary to export it for use in the components of other modules as well.
The selector in camelCase is also used in component templates.
Now it is @Pipe
and must implement the PipeTransform
interface. The module is thrown in the same place as the components / directives, and also must be exported if used in other modules.
The selector in camelCase is also used in component templates.
Angular's directives and filters cannot be used in patterns of angular components and vice versa. Between the frameworks, only services and components are forwarded.
First, we get rid of export default, because AoT compiler cannot.
Secondly, because of the current structure of the modules (very large) and the use of interfaces (we put in a heap in the same file where the classes are) we caught a funny bug with importing such interfaces and using them with decorators: if the interface is imported from a file containing exports not only interfaces, but also, for example, classes / constants, and such an interface is used for typing next to the decorator (for example, @Input() smth: ISmth
), the compiler will export 'ISmth' was not found
import error export 'ISmth' was not found
. This can be fixed either by moving all interfaces to a separate file (which is bad due to large modules, such a file will be in a dozen screens), or by replacing interfaces with classes. Replacement for classes is not a ride, because can not inherit from multiple parents.
Selected solution: create an interface
directory in each module in which files with the name of the entity will be located, containing the corresponding interfaces (for example, room, step, content, workbook, homework). Accordingly, all interfaces that are not used locally are put there and imported from such file directories.
A more detailed description of the problem:
https://github.com/angular/angular/angular-cli/issues/2034#issuecomment-302666897
https://github.com/webpack/webpack/issues/2977#issuecomment-245898520
If transglud ( ng-content
) is used in the upgraded component, then when using the component from the angular templates:
ng-content
;When using an angular component in an angular component, the inputs are written as for a regular component angular (using []
and ()
), but in kebab-case
<vim-angular-component [some-input]="" (some-output)=""> </vim-angular-component>
When rewriting such a template on an angular, we kebab-case on camelCase.
Do not ride, because on it the AoT compiler will swear. Therefore, import the same files into the ts file and forward it through the component.
It was:
<span> ${require('!html-loader!image-webpack-loader?{}!./images/icon.svg')} </span>
has become:
const imageIcon = require<string>("!html-loader!image-webpack-loader?{}!./images/icon.svg"); public imageIcon = imageIcon; <span [innerHTML]="imageIcon | vimBaseSafeHtml"> </span>
Or for use via img
It was:
<img ng-src="${require('./images/icon.svg')}" />
has become:
const imageIcon = require<string>("./images/icon.svg"); public imageIcon = imageIcon; <img [src]="imageIcon | vimBaseSafeUrl" />
$compile
is no more, and there is no compilation from the string (in fact, there is a small hack, but here it’s about how to live in 95% of cases without $compile
).
Dynamically inserted components are forwarded as follows:
@Component({...}) class DynamicComponent {} @NgModule({ declarations: [ ... DynamicComponent, ], entryComponents: [ DynamicComponent, ], }) class SomeModule {} // @Component({ ... template: ` <vim-base-dynamic-component [component]="dynamicComponent"></vim-base-dynamic-component> ` }) class SomeComponent { public dynamicComponent = DynamicComponent; }
The class of the inserted component can be prokidyvatsya through the service, input or else how else.
vim-base-dynamic-component
is an already-written component for dynamically inserting other components with input / output support (in the future, if needed).
If you need to display different templates according to the condition, and the dynamic templateUrl
used for this, replace it with a structural directive and divide the component into three. An example for separating mobile / non mobile:
request / data processing
display for mobile
display for desktops
The first component has a minimal template and is engaged in working with data, processing user actions, and the like (such a template, because of its brevity, it makes sense to put the component component in the template via `` instead of a separate html file and templateUrl
). For example:
@Component({ selector: "...", template: ` <some-component-mobile *vimBaseIfMobile="true" [data]="data" (changeSmth)="onChangeSmth($event)"> </some-component-mobile> <some-component-desktop *vimBaseIfMobile="false" [data]="data" (changeSmth)="onChangeSmth($event)"> </some-component-desktop> `, })
vimBaseIfMobile
is a structural directive (in this case, a direct analogue of ngIf
), displaying the corresponding component by the internal condition and the parameter passed.
Components for mobile and desktop get data via input, send some events through output and deal only with the output of the necessary. All complex logic, processing, changing data - in the main component that displays them. In such components (desktop / mobile), you can safely write changeDetection: ChangeDetectionStrategy.OnPush
.
Open app/entries/angularjs-services-upgrade.ts
and follow the example of an existing copy-paste (all within this file):
// EXAMPLE: copy-paste, fix naming/params, add to module providers at the bottom, use // ----- import LoaderService from "../service/loader"; // NOTE: this function MUST be provided and exported for AoT compilation export function loaderServiceFactory(i: any) { return i.get(LoaderService.ID); } const loaderServiceProvider = { provide: LoaderService, useFactory: loaderServiceFactory, deps: [ "$injector" ] }; // ----- @NgModule({ providers: [ loaderServiceProvider, ] }) export class AngularJSServicesUpgrade {}
Those. we copy the existing block, import the necessary service, rule the names of the constants / functions under it, rule the service used in them and its name (most often, instead of SmthService.ID
you will need to insert just the name of the service under which the service is available (injected) in Angular); new smthServiceProvider
constant to the list of providers at the end of the file.
This service is used as native Angulyarovsky: just inject in the constructor by class.
We put in the file with the original component (at the beginning) the following stub, which will allow the component to be thrown into the angular environment:
import {Directive, ElementRef, Injector, Input, Output, EventEmitter} from "@angular/core"; import {UpgradeComponent} from "@angular/upgrade/static"; @Directive({ /* tslint:disable:directive-selector */ selector: "vim-smth" }) /* tslint:disable:directive-class-suffix */ export class SmthComponent extends UpgradeComponent { @Input() smth: boolean; @Output() someAction: EventEmitter<string>; constructor(elementRef: ElementRef, injector: Injector) { super("vimSmth", elementRef, injector); } } @NgModule({ declarations: [ ... SmthComponent, ] }) export class SmthModule {
Please note that in this case the Directive
decorator is used instead of the Component
, this is a feature of how the angular will process it.
We do not forget to register all Input / Output (binding from an original component) and to register a component in declarations
corresponding module.
Further, when upgrading this component, such a cap will become a real component of the angular.
If a component (or rather, the old component directive) injects $attrs
into the controller / link function, then such a component cannot be thrown into an angular from angularis, and it needs to be upgraded or put next to an upgraded copy for the angular.
Disabling tslint errors is necessary so that it does not swear at the discrepancy between the selector name and class to the decorator directive. These lines (comments) should be removed after the upgrade component.
$q
is replaced with native Promise
. They do not have finally
, but this is fixed by the core.js/es7.promise.finally
and now it is. It also has no deferred, added ts-deferred, not to write a bike every time;$timeout
and $interval
use native window.setTimeout
and window.setInterval
;ng-show="visible"
let us go to the attribute [hidden]="!visible"
;track by
now should always be a method, indicated as (do not forget about the Post's postfix method): *ngFor="let item of items; trackBy: itemTrack" public itemTrack(_index: number, item: IItem): number { return item.id; }
$digest
, $apply
, $evalAsync
and the like are cut without replacement;constructor(private someService: SomeService)
, the angular itself will understand where to get it;constructor(private element: ElementRef)
injectable constructor(private element: ElementRef)
and initialized in the AfterViewInit
hook ( ElementRef
is not a DOM object itself, it is accessible by this.element.nativeElement
);ng-include
not without replacement, we use dynamic component creation;angular.extend
, angular.merge
, angular.forEach
and the like is absent, use native js and lodash;angular.element
and all its methods are missing. We use @ViewChild/@ContentChild
and work through the native js;OnPush
- inject private changeDetectorRef: ChangeDetectorRef
and pull this.changeDetectorRef.markForCheck()
;$ctrl.
- access to Saint-you and methods directly by name;ng-bind-html="smth"
-> [innerHTML]="smth"
$sce
-> import {DomSanitizer} from "@angular/platform-browser";
ng-pural
-> [ngPlural]
https://angular.io/api/common/NgPluralngClass
cannot [ngClass]="{ [ styles.active ]: visible, [ styles.smth ]: smth }"
so replace with array
[ngClass]="[ visible ? styles.active : '', smth ? styles.smth : '' ]"
ui-router
services are imported from @uirouter/core
and injected without the old $
prefix import {StateService, TransitionService} from "@uirouter/core"; constructor(stateService: StateService, transitionService: TransitionService) {
attr.data-smth=""
as attr.data-smth=""
or [attr.data-smth]=""
;require
in components / directives is replaced by the component class injection directly in the constructor of the current component contructor(private parentComponent: ParentComponent)
. Angulyar himself will see that this is a component, and hook him. For fine tuning there are decorators @Host
(searches among parents), @Self
(searches directly on the component), @Optional
(may or may not, if not, the variable will be undefined). You can throw several @Host() @Optional() parentComponent: ParentComponent
. It is possible to requisition components / directives in components / directives;Output
with the same name and the postfix Change
. export class SmthComponent { @Input() variable: string; @Output() variableChange = new EventEmitter<string>(); <vim-smth [(variable)]="localVar"></vim-smth>
<!-- angular --> <ng-content></ng-content> <!-- angularjs --> <vim-angular-component> transcluded data </vim-angular-component>
In the following parts, we talk about the features of work in a hybrid mode , as well as about new conventions to which we have to get used to Angular .
Source: https://habr.com/ru/post/348356/
All Articles