Автор статьи, первую часть перевода которой мы публикуем, говорит, что он уже около двух лет работает над крупномасштабным Angular-приложением в Trade Me. В течение последних нескольких лет команда разработчиков приложения постоянно занимается совершенствованием проекта — как в плане качества кода, так и в том, что касается производительности.
В этой серии материалов речь пойдёт о подходах к разработке, используемые командой Trade Me, которые выражены в виде более чем двух десятков рекомендаций, касающихся таких технологий, как Angular, TypeScript, RxJS и @ngrx/store. Кроме того, определённое внимание здесь будет уделено универсальным техникам программирования, которые направлены на то, чтобы сделать код приложений чище и аккуратнее.
ngFor
для обхода массивов в шаблонах, используйте эту конструкцию с функцией trackBy
, которая возвращает уникальный идентификатор для каждого элемента.trackBy
, система будет знать о том, какой элемент изменился и внесёт в DOM изменения, касающиеся только этого конкретного элемента. Подробности об этом можно почитать здесь.<li *ngFor="let item of items;">{{ item }}</li>
// в шаблоне
<li *ngFor="let item of items; trackBy: trackByFn">{{ item }}</li>
// в компоненте
trackByFn(index, item) {
return item.id; // уникальный id, соответствующий элементу
}
const
.let
и const
проясняет намерение, касающееся применения объявленных с их помощью сущностей. Кроме того, такой подход облегчает распознавание проблем, вызванных случайной перезаписью значений констант. В такой ситуации выдаётся ошибка компиляции. Кроме того, это улучшает читабельность кода.let car = 'ludicrous car';
let myCar = `My ${car}`;
let yourCar = `Your ${car};
if (iHaveMoreThanOneCar) {
myCar = `${myCar}s`;
}
if (youHaveMoreThanOneCar) {
yourCar = `${youCar}s`;
}
// значение car не перезаписывается, поэтому мы можем сделать car константой
const car = 'ludicrous car';
let myCar = `My ${car}`;
let yourCar = `Your ${car};
if (iHaveMoreThanOneCar) {
myCar = `${myCar}s`;
}
if (youHaveMoreThanOneCar) {
yourCar = `${youCar}s`;
}
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';
iAmAnObservable
.map(value => value.item)
.take(1);
import { map, take } from 'rxjs/operators';
iAmAnObservable
.pipe(
map(value => value.item),
take(1)
);
API_FIX
(что напоминает тег TODO
), и помечать ими исправления. Это упрощает поиск таких исправлений.// шаблон
<p>{{ textToDisplay }}</p>
// компонент
iAmAnObservable
.pipe(
map(value => value.item),
takeUntil(this._destroyed$)
)
.subscribe(item => this.textToDisplay = item);
// шаблон
<p>{{ textToDisplay$ | async }}</p>
// компонент
this.textToDisplay$ = iAmAnObservable
.pipe(
map(value => value.item)
);
take
, takeUntil
и так далее.iAmAnObservable
.pipe(
map(value => value.item)
)
.subscribe(item => this.textToDisplay = item);
takeUntil
в том случае, если вы хотите наблюдать за изменениями какого-то объекта до тех пор, пока другой наблюдаемый объект не сгенерирует некое значение:private destroyed$ = new Subject();
public ngOnInit (): void {
iAmAnObservable
.pipe(
map(value => value.item)
// Мы хотим прослушивать iAmAnObservable до разрушения компонента
takeUntil(this._destroyed$)
)
.subscribe(item => this.textToDisplay = item);
}
public ngOnDestroy (): void {
this._destroyed$.next();
}
this
— это паттерн, применяемый для управления удалением подписок на множество наблюдаемых объектов в компоненте.take
если вам нужно лишь первое значение, выдаваемое наблюдаемым объектом:iAmAnObservable
.pipe(
map(value => value.item),
take(1),
takeUntil(this._destroyed$)
)
.subscribe(item => this.textToDisplay = item);
takeUntil
с take
. Это делается для того, чтобы избежать утечек памяти, вызванных тем, что подписка не привела к получению значения до разрушения компонента. Если бы здесь не использовалась функция takeUntil
, подписка существовала бы до получения первого значения, но так как компонент был бы уже уничтожен, это значение никогда не было бы получено, что привело бы к утечке памяти.switchMap
когда вам нужно игнорировать предыдущее диспетчеризованное действие при поступлении нового действия.mergeMap
в том случае, если нужно параллельно обрабатывать все диспетчеризованные действия.concatMap
тогда, когда действия нужно обрабатывать одно за другим, в порядке их поступления.exhaustMap
в ситуациях, когда, в процессе обработки ранее поступивших действий, вам нужно игнорировать новые.// app.routing.ts
{ path: 'not-lazy-loaded', component: NotLazyLoadedComponent }
// app.routing.ts
{
path: 'lazy-load',
loadChildren: 'lazy-load.module#LazyLoadModule'
}
// lazy-load.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { LazyLoadComponent } from './lazy-load.component';
@NgModule({
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: LazyLoadComponent
}
])
],
declarations: [
LazyLoadComponent
]
})
export class LazyModule {}
subscribe
других наблюдаемых объектов. Вместо этого применяйте подходящие операторы для объединения команд в цепочки. Среди таких операторов можно отметить withLatestFrom
и combineLatest
. Рассмотрим примеры, после чего прокомментируем их.firstObservable$.pipe(
take(1)
)
.subscribe(firstValue => {
secondObservable$.pipe(
take(1)
)
.subscribe(secondValue => {
console.log(`Combined values are: ${firstValue} & ${secondValue}`);
});
});
firstObservable$.pipe(
withLatestFrom(secondObservable$),
first()
)
.subscribe(([firstValue, secondValue]) => {
console.log(`Combined values are: ${firstValue} & ${secondValue}`);
});
firstObservable
, потом система будет ждать завершения операции, и только после этого начнётся работа со вторым наблюдаемым объектом. Если эти объекты представляют собой сетевые запросы, то выглядеть это будет как синхронное выполнение запросов.any
.const x = 1;
const y = 'a';
const z = x + y;
console.log(`Value of z is: ${z}`
// Вывод
Value of z is 1a
y
здесь является числом, но наша программа об этом не знает, поэтому она выводит нечто такое, что выглядит неправильно, но никаких сообщений об ошибках не выдаёт. Подобных проблем можно избежать, назначая переменным и константам подходящие типы. const x: number = 1;
const y: number = 'a';
const z: number = x + y;
// Тут появится ошибка компиляции:
Type '"a"' is not assignable to type 'number'.
const y:number
public ngOnInit (): void {
let myFlashObject = {
name: 'My cool name',
age: 'My cool age',
loc: 'My cool location'
}
this.processObject(myFlashObject);
}
public processObject(myObject: any): void {
console.log(`Name: ${myObject.name}`);
console.log(`Age: ${myObject.age}`);
console.log(`Location: ${myObject.loc}`);
}
// Вывод
Name: My cool name
Age: My cool age
Location: My cool location
myFlashObject
, имя свойства loc
на location
и допустили ошибку в ходе редактирования кода:public ngOnInit (): void {
let myFlashObject = {
name: 'My cool name',
age: 'My cool age',
location: 'My cool location'
}
this.processObject(myFlashObject);
}
public processObject(myObject: any): void {
console.log(`Name: ${myObject.name}`);
console.log(`Age: ${myObject.age}`);
console.log(`Location: ${myObject.loc}`);
}
// Вывод
Name: My cool name
Age: My cool age
Location: undefined
myFlashObject
не используется типизация, то в нашем случае система полагает, что значением свойства loc
объекта myFlashObject
является undefined
. Она не задумывается о том, что loc
может представлять собой недопустимое имя свойства.myFlashObject
применяется типизация, то в подобной ситуации мы увидим, при компиляции кода, замечательное сообщение об ошибке:type FlashObject = {
name: string,
age: string,
location: string
}
public ngOnInit (): void {
let myFlashObject: FlashObject = {
name: 'My cool name',
age: 'My cool age',
// Ошибка компиляции
Type '{ name: string; age: string; loc: string; }' is not assignable to type 'FlashObjectType'.
Object literal may only specify known properties, and 'loc' does not exist in type 'FlashObjectType'.
loc: 'My cool location'
}
this.processObject(myFlashObject);
}
public processObject(myObject: FlashObject): void {
console.log(`Name: ${myObject.name}`);
console.log(`Age: ${myObject.age}`)
// Ошибка компиляции
Property 'loc' does not exist on type 'FlashObjectType'.
console.log(`Location: ${myObject.loc}`);
}
tsconfig.json
, опцию strict:true
для того, чтобы включить строгую проверку типов.tslint.json
для того, чтобы организовать проверку кода по определённым правилам.public ngOnInit (): void {
console.log('I am a naughty console log message');
console.warn('I am a naughty console warning message');
console.error('I am a naughty console error message');
}
// Вывод. Никаких сообщений об ошибках, в консоль выводится следующее:
I am a naughty console message
I am a naughty console warning message
I am a naughty console error message
// tslint.json
{
"rules": {
.......
"no-console": [
true,
"log", // команда console.log запрещена
"warn" // команда console.warn запрещена
]
}
}
// ..component.ts
public ngOnInit (): void {
console.log('I am a naughty console log message');
console.warn('I am a naughty console warning message');
console.error('I am a naughty console error message');
}
// Вывод. Линтер выводит ошибки для команд console.log and console.warn и не сообщает об ошибках применительно к console.error, так как эта команда не упомянута в настройках
Calls to 'console.log' are not allowed.
Calls to 'console.warn' are not allowed.
К сожалению, не доступен сервер mySQL