Лучшие методы для чистого и продуктивного приложения

luchshie metody dlya chistogo i produktivnogo prilozheniya

от Вамси Вемпати

SDOlp6vTNKPYse3Oqe0CiNTSvEt3sQhpHQPq

Я работаю над крупномасштабным приложением Angular в Trade Me, Новая Зеландия уже несколько лет. В последние несколько лет наша команда совершенствовала наше приложение как с точки зрения стандартов кодирования, так и производительности, чтобы сделать его в лучшем состоянии.

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

1) trackBy

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

Почему?

Когда массив изменяется, Angular воспроизводит все дерево DOM. Но если вы используете trackByAngular узнает, какой элемент изменился и внесет изменения только в 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 против 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: когда вы хотите обрабатывать выбросы один за другим по мере их выбросов

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

Для более подробного объяснения по этому поводу, пожалуйста, обратитесь к этой статье Николаса Джеймисона.

Почему?

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

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.

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

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, вы можете это сделать. Пожалуйста, обратитесь к этой статье Крейга Спенса о том, как написать свои правила линта с помощью 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 как модели побочных эффектов для магазина. Изменения состояния описываются действиями, а изменения осуществляются чистыми функциями, называемыми редукторами.

Почему?

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

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

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

Почему?

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

Шутка

Jest – это платформа модульного тестирования Facebook для JavaScript. Это делает модульное тестирование более быстрым за счет распараллеливания тестовых запусков в базе кода. В режиме просмотра выполняются только тесты, связанные с изменениями, что значительно сокращает цикл обратной связи для тестирования. Шутка также обеспечивает кодовое покрытие тестов и поддерживается на 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 значительно улучшает производительность вашего приложения. Недавно мы обновили нашу программу, чтобы выполнить рендеринг на стороне сервера, и время загрузки сайта выросло от нескольких секунд до десятков миллисекунд!!

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

Вывод

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

Спасибо, что читаете! Если вам понравилась эта статья, не стесняйтесь ? и помочь другим найти его. Пожалуйста, не стесняйтесь поделитесь своим мнением в разделе комментариев ниже. Следите за мной на Medium или Twitter, чтобы получить больше статей. Счастливого кодирования люди!! ? ☕️

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *