Опыт миграции от AngularJS к Angular6 ngrx/store в условиях стартапа
Юрий Дадычин
FE team-lead
Samples
All given in this presentation
has
no unique value
and
no any intellectual property,
it's just a compilation of
known facts
Entry point
AngularJS
- Tone of existing controllers, routes, components, styles
- API's(not so interesting)
- 100500 reasons to move from AngularJS to Angular5 or React(according to my co-speakers)
Problem 1
Add Angular5 in same project with AngularJS
Lerna
- https://github.com/lerna/lerna
- helps to keep and init multiple npm packages in same repo
- Able to have cross-packages links
- Install's everything from repo root
lerna init && lerna bootstrap --loglevel verbose
cat lerna.json =>
{
"lerna": "2.11.0",
"packages": [
"packages/*"
],
"version": "0.0.0"
}
cd packages
npm install -g @angular/cli
ng new ng-demo
cd ng-demo
pwd =>
<project-root>/packages/ng-demo
Problem 2
Have AngularJS and Angular5 on the same page
html:
<!-- bootstrap point for AngularJS -->
<div ng-app="demo-app" class="ng-cloak">
<ng-view></ng-view>
</div>
<!-- bootstrap point for Angular5 -->
<ng-demo></ng-demo>
Friendly Routing
AngularJS
Angular5
(function (angular) {
angular.module('demo-app').config([
'$routeProvider',
function ($routeProvider) {
$routeProvider
.when('/test', {
templateUrl: 'route-one.html',
controller: 'FirstController',
controllerAs: 'ctrl',
bindToController: true
})
// and many more
;
},
]);
}(window.angular));
import { Routes } from '@angular/router';
import { EmptyComponent } from
'./shared/components/empty/empty.component';
export const appRoutes: Routes = [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full',
canActivate: [
],
},
{
path: '**', // to be friendly with others
component: EmptyComponent, // does nothing
},
];
What if html?:
<!-- bootstrap point for AngularJS -->
<div ng-app="demo-app" class="ng-cloak">
<ng-view></ng-view>
</div>
<!-- bootstrap point for Angular5 -->
<ng-demo></ng-demo>
<ng-demo-one-more></ng-demo-one-more>
// custom boostrapper
const bootstrapRootComponent = (app, selector, component) => {
// check root element existance
if (window.document.querySelector(selector)) {
app.bootstrap(component);
}
};
// components registry selector: component
const entryComponentsRegistry = {
'ng-demo': NgDemoComponent,
'ng-demo-one-more': OneMoreRootNgDemoComponent
};
// ...
export class AppModule {
ngDoBootstrap(app) {
for (const selector in entryComponentsRegistry) {
bootstrapRootComponent(app, selector, entryComponentsRegistry[selector]);
}
}
}
Problem 3
Angular5 is useless as AngularJS in glance
ngrx/store
- No way to integrate by yourself
- Does not work with Angular6 yes(beta only)
- copy/pasta from example app https://github.com/ngrx/platform/tree/master/example-app
- Tired to copy but made it...
Problem 4
It came, instantly... How to bind store selection to form?
Just forget about the template driven forms
Reactive forms
- https://angular.io/guide/reactive-forms
- Looks like a many things to read
- Even more to code around
AbstractReactiveForm
// created in constructor
formGroup: FormGroup;
protected formGroupValuesChanges: Subscription;
@Input() value = {};
@Output() onChangeCallback: EventEmitter<any>;
@Output() onSubmitValue: EventEmitter<any>;
protected debounceTime = 300;
protected patchOptions = { emitEvent: false };
ngOnInit() {
const patch = Object
.keys(this.formGroup.controls)
.reduce(this.getFormGroupValuesReducer(), {});
this.formGroup.patchValue(patch, this.patchOptions);
this.formGroupValuesChanges = this.formGroup.valueChanges
.debounceTime(this.debounceTime)
.subscribe(() => {
this.onChangeCallback.emit(this.getAggregatedValue());
});
}
getFormGroupValuesReducer() {
return (acc: Object, field: string) => {
acc[field] = this.value[field];
return acc;
};
}
onSubmit() {
if (this.formGroup.valid) {
this.onSubmitValue.emit(this.getAggregatedValue());
}
}
getAggregatedValue() {
return {
...this.value,
...this.formGroup.value,
};
}
Problem5
Something wrong with containers
export class DemoContainerComponent implements OnInit {
items$: Observable<Items[]>;
constructor(private store: Store<fromItems.State>) {
this.items$ = store.select(fromItems.getItems);
}
ngOnInit() {
// silly place to do it
this.store.dispatch(new itemsActions.Load());
}
}
It's a Resolvers job
{
path: '',
component: DemoContainerComponent,
resolve: {
doesNotMetterWhatKey: DemoResolver,
},
children: [
...
...
}
In a router
DemoResolver
@Injectable()
export class DemoResolver implements Resolve<Item[]> {
constructor(
private store: Store<itemsReducers.State>,
) { }
// carefull with the return value, if you are not
// returning void resolver will wait for end of Obs..
resolve(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<Item[]> {
return this.store
.select(itemsReducers.selectItems)
.do((items: Item[]) => {
if (items.length === 0) {
this.store.dispatch(new itemsActions.Load());
}
})
.take(1);
}
}
Problem 6
Guards
Problem with guards is that you have to wait
export const waitForUserToLoad = (store):
Observable<boolean> => {
return store
.select(fromAuth.authInProgress)
.map((isLoading: boolean) => isLoading)
.filter(isLoading => !isLoading)
.first();
};
And you can chain waits
@Injectable()
export class UserLoadedGuard
implements CanActivate {
constructor(
private store: Store<fromAuth.State>,
) {}
canActivate(route: ActivatedRouteSnapshot):
Observable<boolean> {
return waitForUserToLoad(this.store)
.switchMap(() => {
return Observable.from([true]);
});
}
}
Problem 7
We are hosted on AWS and we have different static assets root
__webpack_public_path__
// main.ts
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
const meta = document
.querySelector('meta[name="static-base-url"]');
const baseUrl = meta ? meta.getAttribute('content') : '/';
declare let __webpack_public_path__;
if (environment.production) {
__webpack_public_path__ = `${baseUrl}js/`;
enableProdMode();
} else {
__webpack_public_path__ = `/`;
}
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
Problem 8
AngularJS wants(should) notify Angular6 about it's status
Actions Bridge
AngularJS
(function(angular) {
function PubSubService() { return window.PubSub; }
angular.module('demo-app')
.factory('PubSubService', PubSubService);
})(window.angular);
(function(angular) {
function StoreBridgeService(PubSubService) {
this.PubSubService = PubSubService;
}
angular.extend(StoreBridgeService.prototype, {
dispatch: function(action) {
this.PubSubService.publish('Store::dispatch', action);
}
});
StoreBridgeService.$inject = ['PubSubService'];
angular.module('demo-app')
.service('StoreBridgeService', StoreBridgeService);
})(window.angular);
Angular5
@Injectable()
export class PubsubService {
public instance: any = null;
constructor() {
this.instance = PubSub;
}
}
@Injectable()
export class StoreBridgeService {
constructor(
private pubsub: PubsubService,
private store: Store<appReducers.State>,
) {
this.pubsub.instance.subscribe('Store::dispatch',
(msg, data) => {
this.store.dispatch(data);
});
}
}
Problem 9
Tooooooooo many scaffolding creating ngrx/store things
Schematics
- https://blog.angular.io/schematics-an-introduction-dc1dfbc2a2b2
- Those extend ng generator functionality with your custom generators
- You can bootstrap new ngrx/store driven modules with one command
Benefits
Single
Way
Data flow
Universal design scheme
Questions?
AngularJS, Angular6, ngrx/store
By Yuriy Dadichin
AngularJS, Angular6, ngrx/store
О том как мы внедрили самый фронтенд топ рядом с легаси и с какими задачами мы столкнулись, а именно: организация сборки, синхронизация состояний, общие стили и много другого.
- 413