В этой статье описаны практики, которые я использую в своем приложении, и связанные с Angular, Typescript, RxJs и @ ngrx / store. Я также ознакомлюсь с некоторыми общими рекомендациями по кодированию, чтобы сделать приложение более чистым.

1) trackBy

При использовании ngFor для перебора массива в шаблонах используйте его с функцией trackBy, которая вернет уникальный идентификатор для каждого элемента.

Почему?

Когда массив изменяется, Angular повторно визуализирует все дерево DOM. Но если вы используете trackBy, Angular будет знать, какой элемент был изменен, и внесет изменения DOM только для этого конкретного элемента.

До

<li *ngFor="let item of items;">{{ item }}</li>

После

// in the template
<li *ngFor="let item of items; trackBy: trackByFn">{{ item }}</li>
// in the component
trackByFn(index, item) {    
   return item.id; // unique id corresponding to the item
}

2) const vs let

При объявлении переменных используйте 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`;
}

После

// the value of car is not reassigned, so we can make it a const
const car = 'ludicrous car';
let myCar = `My ${car}`;
let yourCar = `Your ${car};
if (iHaveMoreThanOneCar) {
   myCar = `${myCar}s`;
}
if (youHaveMoreThanOneCar) {
   yourCar = `${youCar}s`;
}

3) Конвейерные операторы

Используйте конвейерные операторы при использовании операторов RxJs.

Почему?

Конвейерные операторы являются древовидными, что означает, что при их импорте будет включен только код, который нам нужно выполнить.

Это также упрощает идентификацию неиспользуемых операторов в файлах.

Примечание. Для этого требуется Angular версии 5.5+.

До

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)
     );

4) Изоляция хаков API

Не все API-интерфейсы являются пуленепробиваемыми - иногда нам нужно добавить некоторую логику в код, чтобы исправить ошибки в API. Вместо того, чтобы взламывать компоненты там, где они необходимы, лучше изолировать их в одном месте - например, в службе, и использовать службу из компонента.

Почему?

Это помогает держать взломанные «ближе к API», так что как можно ближе к тому, где делается сетевой запрос. Таким образом, меньшая часть вашего кода будет иметь дело с незащищенным кодом. Кроме того, это одно место, где живут все хаки, и их легче найти. При исправлении ошибок в API легче искать их в одном файле, чем искать хаки, которые могут быть распространены по кодовой базе.

Вы также можете создать собственные теги, такие как API_FIX, аналогичные TODO, и пометить им исправления, чтобы их было легче найти.

5) Подпишитесь в шаблоне

Избегайте подписки на наблюдаемые объекты из компонентов, вместо этого подпишитесь на наблюдаемые объекты из шаблона.

Почему?

async каналы автоматически отписываются от подписки, что упрощает код, избавляя от необходимости вручную управлять подписками. Это также снижает риск случайного отказа от подписки в компоненте, что может вызвать утечку памяти. Этот риск также можно снизить, используя правило lint для обнаружения наблюдаемых объектов, на которые нет подписки.

Это также мешает компонентам сохранять состояние и вносить ошибки, при которых данные изменяются вне подписки.

До

// // template
<p>{{ textToDisplay }}</p>
// component
iAmAnObservable
    .pipe(
       map(value => value.item),
       takeUntil(this._destroyed$)
     )
    .subscribe(item => this.textToDisplay = item);

После

// template
<p>{{ textToDisplay$ | async }}</p>
// component
this.textToDisplay$ = iAmAnObservable
    .pipe(
       map(value => value.item)
     );

6) Убрать подписки

При подписке на наблюдаемые объекты всегда убедитесь, что вы отписывались от них соответствующим образом, используя такие операторы, как take, takeUntil и т. Д.

Почему?

Неспособность отказаться от подписки на наблюдаемые объекты приведет к нежелательным утечкам памяти, поскольку наблюдаемый поток остается открытым, возможно, даже после того, как компонент был уничтожен / пользователь перешел на другую страницу.

Более того, создайте правило lint для обнаружения наблюдаемых, подписка которых не отменена.

До

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)
      // We want to listen to iAmAnObservable until the component is destroyed,
       takeUntil(this._destroyed$)
     )
    .subscribe(item => this.textToDisplay = item);
}
public ngOnDestroy (): void {
    this._destroyed$.next();
    this._destroyed$.complete();
}

Использование такой частной темы - это шаблон для управления отказом от подписки на многие наблюдаемые в компоненте.

Использование take, когда вам нужно только первое значение, испускаемое наблюдаемым:

iAmAnObservable
    .pipe(
       map(value => value.item),
       take(1),
       takeUntil(this._destroyed$)
    )
    .subscribe(item => this.textToDisplay = item);

Обратите внимание на использование takeUntil с take здесь. Это сделано для того, чтобы избежать утечек памяти, вызванных тем, что подписка не получила значения до того, как компонент был уничтожен. Без takeUntil подписка все равно зависла бы до тех пор, пока не получит первое значение, но поскольку компонент уже был уничтожен, он никогда не получит значения, что приведет к утечке памяти.

7) Используйте соответствующие операторы

При использовании операторов сглаживания с наблюдаемыми объектами используйте оператор, соответствующий ситуации.

switchMap:, если вы хотите игнорировать предыдущие выбросы при появлении нового выброса.

mergeMap:, если вы хотите одновременно обрабатывать все выбросы.

concatMap:, если вы хотите обрабатывать выбросы один за другим по мере их появления.

выхлопная карта: если вы хотите отменить все новые выбросы при обработке предыдущего выброса.

Более подробно об этом читайте в этой статье Николаса Джеймисона.

Почему?

Использование одного оператора, когда это возможно, вместо объединения нескольких других операторов для достижения того же эффекта, может привести к тому, что пользователю будет отправлено меньше кода. Использование неправильных операторов может привести к нежелательному поведению, поскольку разные операторы обрабатывают наблюдаемые по-разному.

8) Ленивая загрузка

По возможности старайтесь лениво загружать модули в свое приложение Angular. Ленивая загрузка - это когда вы загружаете что-то только тогда, когда оно используется, например, загружаете компонент только тогда, когда он должен быть виден.

Почему?

Это уменьшит размер загружаемого приложения и может улучшить время загрузки приложения, не загружая неиспользуемые модули.

До

// 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 {}

9) Избегайте подписок внутри подписок

Иногда вам могут потребоваться значения более чем одного наблюдаемого для выполнения действия. В этом случае избегайте подписки на один наблюдаемый объект в блоке подписки другого наблюдаемого объекта. Вместо этого используйте соответствующие операторы цепочки. Операторы цепочки запускаются на наблюдаемых от оператора перед ними. Вот некоторые операторы цепочки: 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}`);
});

Почему?

Запах кода / удобочитаемость / сложность. Неполное использование RxJs предполагает, что разработчик не знаком с поверхностной областью API RxJs.

Производительность: если наблюдаемые холодные, он подписывается на firstObservable, ждет его завершения, ЗАТЕМ запускает работу второго наблюдаемого. Если бы это были сетевые запросы, он бы отображался как синхронный / каскадный.

10) Избегайте любого; введите все;

Всегда объявляйте переменные или константы с типом, отличным от any.

Почему?

При объявлении переменных или констант в Typescript без набора текста тип переменной / константы будет определяться присвоенным ей значением. Это вызовет непредвиденные проблемы. Вот один классический пример:

const x = 1;
const y = 'a';
const z = x + y;
console.log(`Value of z is: ${z}`
// Output
Value of z is 1a

Это может вызвать нежелательные проблемы, если вы ожидаете, что y тоже будет числом. Этих проблем можно избежать, если правильно ввести переменные.

const x: number = 1;
const y: number = 'a';
const z: number = x + y;
// This will give a compile error saying:
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}`);
}
// Output
Name: My cool name
Age: My cool age
Location: My cool location

Допустим, мы хотим переименовать свойство loc в location в myFlashObject:

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}`);
}
// Output
Name: My cool name
Age: My cool age
Location: undefined

Если у нас нет типизации на myFlashObject, он думает, что свойство loc на myFlashObject просто не определено, а не что это недопустимое свойство.

Если бы у нас была типизация для myFlashObject, мы получили бы красивую ошибку времени компиляции, как показано ниже:

type FlashObject = {
    name: string,
    age: string,
    location: string
}
public ngOnInit (): void {
    let myFlashObject: FlashObject = {
        name: 'My cool name',
        age: 'My cool age',
        // Compilation error
        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}`)
    // Compilation error
    Property 'loc' does not exist on type 'FlashObjectType'.
    console.log(`Location: ${myObject.loc}`);
}

Если вы начинаете новый проект, стоит установить strict:true в файле tsconfig.json, чтобы включить все параметры строгой проверки типов.

11) Используйте правила линта

tslint уже имеет различные встроенные параметры, такие как no-any, no-magic-numbers, no-console и т. Д., Которые вы можете настроить в своем tslint.json для обеспечения соблюдения определенных правил в своей базе кода.

Почему?

Наличие правил lint означает, что вы получите приятную ошибку, когда будете делать то, чего не должны делать. Это обеспечит согласованность и удобочитаемость вашего приложения. Пожалуйста, обратитесь здесь для получения дополнительных правил, которые вы можете настроить.

Некоторые правила lint даже содержат исправления для устранения ошибки lint. Если вы хотите настроить собственное правило lint, вы тоже можете это сделать. Пожалуйста, обратитесь к этой статье Craig Spence о том, как писать свои собственные правила lint с помощью TSQuery.

До

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');
}
// Output
No errors, prints the below on console window:
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",    // no console.log allowed
             "warn"    // no console.warn allowed
        ]
   }
}
// ..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');
}
// Output
Lint errors for console.log and console.warn statements and no error for console.error as it is not mentioned in the config
Calls to 'console.log' are not allowed.
Calls to 'console.warn' are not allowed.

12) Небольшие компоненты многоразового использования

Извлеките части, которые можно повторно использовать в компоненте, и сделайте его новым. Сделайте компонент как можно более тупым, так как это заставит его работать в большем количестве сценариев. Сделать компонент немым означает, что компонент не имеет какой-либо специальной логики и работает исключительно на основе предоставленных ему входов и выходов.

Как правило, последний дочерний элемент в дереве компонентов будет самым тупым из всех.

Почему?

Повторно используемые компоненты сокращают дублирование кода, что упрощает обслуживание и внесение изменений.

Тупые компоненты проще, поэтому в них меньше ошибок. Глупые компоненты заставляют задуматься об API общедоступных компонентов и помогают избавиться от смешанных проблем.

13) Компоненты должны иметь дело только с логикой отображения

По возможности избегайте использования в компоненте какой-либо другой логики, кроме логики отображения, и заставляйте компонент работать только с логикой отображения.

Почему?

Компоненты предназначены для презентационных целей и управляют тем, что должно делать представление. Любая бизнес-логика должна быть извлечена в ее собственные методы / службы, где это необходимо, отделяя бизнес-логику от логики представления.

Бизнес-логику обычно легче тестировать при извлечении в службу, и ее можно повторно использовать любыми другими компонентами, которым требуется применение той же бизнес-логики.

14) Избегайте длинных методов

Длинные методы обычно указывают на то, что они делают слишком много вещей. Попробуйте использовать принцип единой ответственности. Сам метод в целом может делать что-то одно, но внутри него могут происходить еще несколько операций. Мы можем выделить эти методы в их собственные методы и заставить их делать что-то одно, а вместо этого использовать их.

Почему?

Длинные методы трудно читать, понимать и поддерживать. Они также подвержены ошибкам, так как изменение одного элемента может повлиять на многие другие в этом методе. Они также затрудняют рефакторинг (который является ключевым моментом в любом приложении).

Иногда это измеряется как цикломатическая сложность. Существуют также некоторые правила TSLint для определения цикломатической / когнитивной сложности, которые вы можете использовать в своем проекте, чтобы избежать ошибок, выявлять запахи кода и проблемы с ремонтопригодностью.

15) СУХАЯ

Не повторяйся. Убедитесь, что у вас нет одного и того же кода, скопированного в разные места в кодовой базе. Извлеките повторяющийся код и используйте его вместо повторяющегося кода.

Почему?

Наличие одного и того же кода в нескольких местах означает, что если мы хотим внести изменения в логику в этом коде, мы должны сделать это в нескольких местах. Это затрудняет поддержку, а также подвержено ошибкам, когда мы можем пропустить обновление во всех случаях. На внесение изменений в логику уходит больше времени, и ее тестирование также является длительным процессом.

В таких случаях извлеките повторяющийся код и используйте его вместо него. Это означает, что только одно место можно изменить и одно нужно проверить. Меньше дублирующего кода, отправляемого пользователям, означает, что приложение будет работать быстрее.

16) Добавить механизмы кеширования

При вызове API ответы от некоторых из них не часто меняются. В таких случаях вы можете добавить механизм кэширования и сохранить значение из API. Когда делается другой запрос к тому же API, проверьте, есть ли для него значение в кеше, и если да, используйте его. В противном случае выполните вызов API и кешируйте результат.

Если значения меняются, но не часто, вы можете ввести время кеширования, где вы можете проверить, когда оно было в последний раз кэшировано, и решить, вызывать ли API или нет.

Почему?

Наличие механизма кэширования означает избежание нежелательных вызовов API. Выполняя вызовы API только тогда, когда это необходимо, и избегая дублирования, скорость приложения повышается, поскольку нам не нужно ждать подключения к сети. Это также означает, что мы не загружаем одну и ту же информацию снова и снова.

17) Избегайте логики в шаблонах

Если у вас есть какая-то логика в ваших шаблонах, даже если это простое предложение &&, хорошо извлечь его в свой компонент.

Почему?

Наличие логики в шаблоне означает, что его невозможно провести модульное тестирование, и поэтому он более подвержен ошибкам при изменении кода шаблона.

До

// template
<p *ngIf="role==='developer'"> Status: Developer </p>
// component
public ngOnInit (): void {
    this.role = 'developer';
}

После

// template
<p *ngIf="showDeveloperStatus"> Status: Developer </p>
// component
public ngOnInit (): void {
    this.role = 'developer';
    this.showDeveloperStatus = true;
}

18) Струны должны быть безопасными

Если у вас есть переменная типа string, которая может иметь только набор значений, вместо объявления ее как строкового типа вы можете объявить список возможных значений как тип.

Почему?

Правильно объявив тип переменной, мы можем избежать ошибок при написании кода во время компиляции, а не во время выполнения.

До

private myStringValue: string;
if (itShouldHaveFirstValue) {
   myStringValue = 'First';
} else {
   myStringValue = 'Second'
}

После

private myStringValue: 'First' | 'Second';
if (itShouldHaveFirstValue) {
   myStringValue = 'First';
} else {
   myStringValue = 'Other'
}
// This will give the below error
Type '"Other"' is not assignable to type '"First" | "Second"'
(property) AppComponent.myValue: "First" | "Second"

Большая картина

Государственное управление

Рассмотрите возможность использования @ ngrx / store для поддержания состояния вашего приложения и @ ngrx / effects в качестве модели побочных эффектов для store. Изменения состояния описываются действиями, а изменения выполняются чистыми функциями, называемыми редукторами.

Почему?

@ ngrx / store изолирует всю логику, связанную с состоянием, в одном месте и делает ее согласованной во всем приложении. Он также имеет механизм мемоизации при доступе к информации в магазине, ведущий к более производительному приложению. @ ngrx / store в сочетании со стратегией обнаружения изменений Angular приводит к более быстрому приложению.

Неизменяемое состояние

При использовании @ ngrx / store рассмотрите возможность использования ngrx-store-freeze, чтобы сделать состояние неизменяемым. ngrx-store-freeze предотвращает изменение состояния путем создания исключения. Это позволяет избежать случайной мутации состояния, приводящей к нежелательным последствиям.

Почему?

Изменяющееся состояние компонентов приводит к тому, что приложение ведет себя непоследовательно в зависимости от того, в каком порядке загружены компоненты. Это ломает ментальную модель шаблона редукции. Изменения могут быть отменены, если состояние хранилища изменится и будет повторно выпущено. Разделение проблем - компоненты представляют собой слой представления, они не должны знать, как изменять состояние.

Шутка

Jest - это фреймворк Facebook для модульного тестирования JavaScript. Он ускоряет модульное тестирование за счет распараллеливания тестовых прогонов по базе кода. В режиме просмотра запускаются только тесты, связанные с внесенными изменениями, что сокращает цикл обратной связи для тестирования. Jest также обеспечивает покрытие кода тестов и поддерживается в VS Code и Webstorm.

Вы можете использовать пресет для Jest, который сделает большую часть тяжелой работы за вас при настройке Jest в вашем проекте.

Карма

Karma - это тестовый раннер, разработанный командой AngularJS. Для запуска тестов требуется настоящий браузер / DOM. Он также может работать в разных браузерах. Jest не нужны chrome headless / phantomjs для запуска тестов, и он работает на чистом Node.

Универсальный

Если вы еще не сделали свое приложение универсальным, сейчас подходящий момент для этого. Angular Universal позволяет запускать приложение Angular на сервере и выполнять рендеринг на стороне сервера (SSR), который обслуживает статические предварительно отрисованные html-страницы. Это делает приложение очень быстрым, поскольку оно отображает контент на экране почти мгновенно, без необходимости ждать загрузки и анализа пакетов JS или загрузки Angular.

Он также удобен для SEO, поскольку Angular Universal генерирует статический контент и упрощает поисковым роботам возможность индексировать приложение и делать его доступным для поиска без выполнения JavaScript.

Почему?

Universal значительно повышает производительность вашего приложения. Недавно мы обновили наше приложение для рендеринга на стороне сервера, и время загрузки сайта увеличилось с нескольких секунд до десятков миллисекунд !!

Это также позволяет вашему сайту правильно отображаться во фрагментах превью в социальных сетях. Первая значимая отрисовка выполняется очень быстро и делает контент видимым для пользователей без каких-либо нежелательных задержек.

Вывод

Создание приложений - это постоянный путь, и всегда есть возможность что-то улучшить. Этот список оптимизаций - хорошее начало, и последовательное применение этих шаблонов сделает вашу команду счастливой. Ваши пользователи также будут любить вас за приятный опыт работы с вашим менее глючным и производительным приложением.

Если у вас есть какие-либо комментарии, вопросы или рекомендации, не стесняйтесь размещать их в разделе комментариев ниже!