Най-добри практики за чисто и изискано ъглово приложение

Вече от няколко години работя по широкомащабно приложение на Angular в Trade Me, Нова Зеландия. През последните няколко години екипът ни усъвършенства нашето приложение както по отношение на стандартите за кодиране, така и по отношение на производителността, за да бъде то в най-доброто си състояние.

Тази статия очертава практиките, които използваме в нашето приложение и е свързана с Angular, Typescript, RxJs и @ ngrx / store. Ще прегледаме и някои общи указания за кодиране, за да помогнем за по-чистото приложение.

1) trackBy

Когато използвате ngFor за циклиране на масив в шаблони, използвайте го с функция trackBy, която ще върне уникален идентификатор за всеки елемент.

Защо?

Когато масивът се промени, Angular рендерира цялото DOM дърво. Но ако използвате trackBy, Angular ще знае кой елемент се е променил и ще направи DOM промени само за този конкретен елемент.

За подробно обяснение на това, моля, вижте тази статия от Netanel Basal.

Преди

  • {{item}}
  • След

    // в шаблона
  • {{item}}
  • // в компонента
    trackByFn (индекс, елемент) {
       връщане item.id; // уникален идентификатор, съответстващ на елемента
    }

    2) const срещу let

    Когато декларирате променливи, използвайте const, когато стойността няма да бъде преназначена.

    Защо?

    Използването на let and const, когато е подходящо, прави намерението на декларациите по-ясни. Той също така ще помогне за идентифициране на проблеми, когато дадена стойност се пренасочи към константа случайно чрез хвърляне на грешка във времето за компилиране. Освен това помага за подобряване на четенето на кода.

    Преди

    нека автомобил = 'нелепа кола';
    нека myCar = `My $ {car}`;
    нека yourCar = `Вашият $ {car};
    ако (iHaveMoreThanOneCar) {
       myCar = `$ {myCar} s`;
    }
    ако (youHaveMoreThanOneCar) {
       yourCar = `$ {youCar} s`;
    }

    След

    // стойността на автомобила не е преназначена, така че можем да го направим const
    const car = 'нелепа кола';
    нека myCar = `My $ {car}`;
    нека yourCar = `Вашият $ {car};
    ако (iHaveMoreThanOneCar) {
       myCar = `$ {myCar} s`;
    }
    ако (youHaveMoreThanOneCar) {
       yourCar = `$ {youCar} s`;
    }

    3) Тръбни оператори

    Използвайте тръбопроводни оператори, когато използвате оператори RxJs.

    Защо?

    Операторите на тръбопровода са разклащащи дърво, което означава, че само кодът, който трябва да изпълним, ще бъде включен при импортирането им.

    Това също така улеснява идентифицирането на неизползваните оператори във файловете.

    Забележка: Това се нуждае от ъглова версия 5.5+.

    Преди

    import 'rxjs / add / operator / map';
    import 'rxjs / add / operator / take';
    iAmAnObservable
        .map (value => value.item)
        .Извадете (1);

    След

    import {map, take} от 'rxjs / оператори';
    iAmAnObservable
        .тръба(
           карта (value => value.item),
           вземат (1)
         );

    4) Изолирайте API хакове

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

    Защо?

    Това помага да запазите хаковете „по-близо до API“, така че да сте по-близо до мястото, където е направена мрежовата заявка. По този начин, по-малко от вашия код се справя с не-хаквания код. Също така, това е едно място, където живеят всички хакове и е по-лесно да ги намерите. Когато коригирате грешките в API, е по-лесно да ги търсите в един файл, отколкото да търсите хакове, които биха могли да бъдат разпространени в кодовата база.

    Можете също да създадете персонализирани маркери като API_FIX, подобни на TODO, и да маркирате поправките с него, така че да бъде по-лесно да се намери.

    5) Абонирайте се в шаблон

    Избягвайте да се абонирате за наблюдателни елементи от компоненти и вместо това се абонирайте за наблюденията от шаблона.

    Защо?

    async тръби се отпишете автоматично и това прави кода по-прост, като елиминира необходимостта от ръчно управление на абонаментите. Освен това намалява риска случайно да забравите да отпишете абонамент в компонента, което би причинило изтичане на памет. Този риск може също да бъде смекчен, като се използва правило за влакна, за да се открият незаписаните наблюдения.

    Това също спира компонентите да бъдат състоятелни и да въвежда грешки, при които данните се мутират извън абонамента.

    Преди

    // шаблон

    {{textToDisplay}}

    // компонент
    iAmAnObservable
        .тръба(
           карта (value => value.item),
           takeUntil (this._destroyed $)
         )
        .пишете абонамент (item => this.textToDisplay = item);

    След

    // шаблон

    {{textToDisplay $ | async}}

    // компонент
    this.textToDisplay $ = iAmAnObservable
        .тръба(
           карта (value => value.item)
         );

    6) Почистете абонаментите

    Когато се абонирате за наблюдение, винаги се уверете, че се отпишете от тях по подходящ начин, като използвате оператори като take, takeUntil и т.н.

    Защо?

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

    Още по-добре е да се направи правило за откриване на наблюдения, които не са отписани.

    Преди

    iAmAnObservable
        .тръба(
           карта (value => value.item)
         )
        .пишете абонамент (item => this.textToDisplay = item);

    След

    Използване на takeUntil, когато искате да слушате промените, докато друг наблюдаван не излъчва стойност:

    private _destroyed $ = new Subject ();
    public ngOnInit (): void {
        iAmAnObservable
        .тръба(
           карта (value => value.item)
          // Искаме да слушаме iAmAnObservable, докато компонентът бъде унищожен,
           takeUntil (this._destroyed $)
         )
        .пишете абонамент (item => this.textToDisplay = item);
    }
    public ngOnDestroy (): void {
        this._destroyed $ .next ();
        this._destroyed $ .complete ();
    }

    Използването на частен обект като този е модел за управление на отписването на много наблюдателни елементи в компонента.

    Използване на Take, когато искате само първата стойност, излъчвана от наблюдаваното:

    iAmAnObservable
        .тръба(
           карта (value => value.item),
           вземат (1),
           takeUntil (this._destroyed $)
        )
        .пишете абонамент (item => this.textToDisplay = item);

    Имайте предвид използването на takeUntil с take тук. Това е за да се избегнат течове на памет, причинени, когато абонаментът не е получил стойност преди компонентът да бъде унищожен. Без takeUntil тук, абонаментът все още ще виси наоколо, докато не получи първата стойност, но тъй като компонентът вече е унищожен, той никога няма да получи стойност - което води до изтичане на паметта.

    7) Използвайте подходящи оператори

    Когато използвате оператори за изравняване с вашите наблюдения, използвайте подходящия оператор за ситуацията.

    switchMap: когато искате да игнорирате предишните емисии, когато има нова емисия

    mergeMap: когато искате едновременно да се справите с всички емисии

    concatMap: когато искате да се справите с емисиите една след друга, тъй като те се излъчват

    източник на карта: когато искате да отмените всички нови емисии, докато обработвате предишна емисия

    За по-подробно обяснение на това, моля вижте тази статия от Никълъс Джеймисън.

    Защо?

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

    8) Мързелив товар

    Когато е възможно, опитайте се да мързеливо заредите модулите във вашето приложение Angular. Мързеливо зареждане е, когато зареждате нещо само когато се използва, например, зареждане на компонент само когато трябва да се види.

    Защо?

    Това ще намали размера на приложението за зареждане и може да подобри времето за стартиране на приложението, като не зареждате модулите, които не се използват.

    Преди

    // app.routing.ts
    {path: 'not-lazy-load', компонент: NotLazyLoadedComponent}

    След

    // app.routing.ts
    {
      път: „мързелив товар“,
      loadChildren: 'lazy-load.module # LazyLoadModule'
    }
    // lazy-load.module.ts
    import {NgModule} от '@ angular / core';
    import {CommonModule} от „@ angular / common“;
    импортиране {RouterModule} от „@ angular / router“;
    импортиране {LazyLoadComponent} от './lazy-load.component';
    @NgModule ({
      внос: [
        CommonModule,
        RouterModule.forChild ([
             {
                 път: '',
                 компонент: LazyLoadComponent
             }
        ])
      ],
      декларации: [
        LazyLoadComponent
      ]
    })
    експорт клас LazyModule {}

    9) Избягвайте да имате абонаменти вътре в абонаментите

    Понякога може да искате стойности от повече от един наблюдаем за извършване на действие. В този случай избягвайте да се абонирате за един наблюдаем в абонаментния блок на друг наблюдаем. Вместо това използвайте подходящи верижни оператори. Операторите на веригата работят върху наблюдаеми от оператора преди тях. Някои верижни оператори са: withLatestFrom, combLatest и т.н.

    Преди

    firstObservable $ .pipe (
       вземат (1)
    )
    .пишете абонамент (firstValue => {
        secondObservable $ .pipe (
            вземат (1)
        )
        .пишете абонамент (secondValue => {
            console.log (`Комбинираните стойности са: $ {firstValue} & $ {secondValue}`);
        });
    });

    След

    firstObservable $ .pipe (
        withLatestFrom (secondObservable $),
        първи ()
    )
    .subscribe (([firstValue, secondValue]) => {
        console.log (`Комбинираните стойности са: $ {firstValue} & $ {secondValue}`);
    });

    Защо?

    Кодовата миризма / четливост / сложност: Ако не използвате RxJs в пълната си степен, предполага разработчикът да не е запознат с RxJs API повърхността.

    Производителност: Ако наблюденията са студени, тя ще се абонира за firstObservable, изчакайте да завърши, след това стартира работата на втория наблюдаем. Ако това бяха мрежови заявки, тя би се показвала като синхрон / водопад.

    10) Избягвайте всякакви; напишете всичко;

    Винаги декларирайте променливи или константи с тип, различен от всеки.

    Защо?

    При деклариране на променливи или константи в Typescript без въвеждане, въвеждането на променливата / константата ще бъде изведено от стойността, която му се присвоява. Това ще доведе до нежелани проблеми. Един класически пример е:

    const x = 1;
    const y = 'a';
    const z = x + y;
    console.log (`Стойността на z е: $ {z}`
    // Изход
    Стойността на z е 1a

    Това може да доведе до нежелани проблеми, когато очаквате и y да бъде число. Тези проблеми могат да бъдат избегнати, като напишете променливите по подходящ начин.

    const x: число = 1;
    const y: number = 'a';
    const z: число = x + y;
    // Това ще даде грешка при компилация, казваща:
    Тип "a" "не се приписва на тип" number ".
    const y: число

    По този начин можем да избегнем бъгове, причинени от липсващи типове.

    Друго предимство на доброто типизиране в приложението ви е, че прави рефакторинга по-лесен и безопасен.

    Разгледайте този пример:

    public ngOnInit (): void {
        нека myFlashObject = {
            име: „Моето готино име“,
            възраст: „Моята готина възраст“,
            loc: „Моето готино местоположение“
        }
        this.processObject (myFlashObject);
    }
    обществен процесObject (myObject: който и да е): void {
        console.log (`Име: $ {myObject.name}`);
        console.log (`Възраст: $ {myObject.age}`);
        console.log (`Местоположение: $ {myObject.loc}`);
    }
    // Изход
    Име: Моето готино име
    Възраст: Моята готина възраст
    Местоположение: Моето готино местоположение

    Нека да кажем, че искаме да преименуваме локацията на имота на местоположение в myFlashObject:

    public ngOnInit (): void {
        нека myFlashObject = {
            име: „Моето готино име“,
            възраст: „Моята готина възраст“,
            местоположение: „Моето готино местоположение“
        }
        this.processObject (myFlashObject);
    }
    обществен процесObject (myObject: който и да е): void {
        console.log (`Име: $ {myObject.name}`);
        console.log (`Възраст: $ {myObject.age}`);
        console.log (`Местоположение: $ {myObject.loc}`);
    }
    // Изход
    Име: Моето готино име
    Възраст: Моята готина възраст
    Местоположение: неопределено

    Ако нямаме въвеждане на myFlashObject, той смята, че свойството loc на myFlashObject е просто неопределено, а не че не е валидно свойство.

    Ако имахме въвеждане за myFlashObject, ще получим приятна грешка във времето за компилиране, както е показано по-долу:

    тип FlashObject = {
        име: низ,
        възраст: низ,
        местоположение: низ
    }
    public ngOnInit (): void {
        нека myFlashObject: FlashObject = {
            име: „Моето готино име“,
            възраст: „Моята готина възраст“,
            // Грешка при компилация
            Въведете '{name: string; възраст: низ; loc: низ; } 'не се приписва да въведете „FlashObjectType“.
            Обектният буквал може да посочва само известни свойства, а 'loc' не съществува в тип 'FlashObjectType'.
            loc: „Моето готино местоположение“
        }
        this.processObject (myFlashObject);
    }
    обществен процесObject (myObject: FlashObject): void {
        console.log (`Име: $ {myObject.name}`);
        console.log (`Възраст: $ {myObject.age}`)
        // Грешка при компилация
        Свойството 'loc' не съществува от тип 'FlashObjectType'.
        console.log (`Местоположение: $ {myObject.loc}`);
    }

    Ако стартирате нов проект, струва си да зададете строго: true във файла tsconfig.json, за да активирате всички строги опции за проверка на типа.

    11) Използвайте правила за влакна

    tslint има различни опции, вградени вече като no-any, no-magic-номера, no-console и т.н., които можете да конфигурирате във вашия tslint.json, за да наложите определени правила във вашата кодова база.

    Защо?

    Наличието на правила за обличане означава, че ще получите приятна грешка, когато правите нещо, което не бива да бъдете. Това ще наложи последователност в приложението ви и четимост. Моля, вижте тук за повече правила, които можете да конфигурирате.

    Някои правила за влакна дори идват с корекции, за да разрешат грешката на влакна. Ако искате да конфигурирате ваше собствено правило за влакна, можете да го направите и вие. Моля, вижте тази статия от Крейг Спенс за това как да напишете свои собствени правила за влакна с помощта на TSQuery.

    Преди

    public ngOnInit (): void {
        console.log („Аз съм палаво съобщение на дневника на конзолата“);
        console.warn („Аз съм палаво предупредително съобщение за конзолата“);
        console.error („Аз съм палаво съобщение за грешка в конзолата“);
    }
    // Изход
    Без грешки, отпечатва по-долу на прозореца на конзолата:
    Аз съм палаво конзолно съобщение
    Аз съм палаво предупредително съобщение за конзолата
    Аз съм палаво съобщение за грешка в конзолата

    След

    // tslint.json
    {
        "правила": {
            .......
            "без конзола": [
                 вярно,
                 "log", // не е разрешен console.log
                 "предупреждавам" // не е разрешено конзолата
            ]
       }
    }
    // ..component.ts
    public ngOnInit (): void {
        console.log („Аз съм палаво съобщение на дневника на конзолата“);
        console.warn („Аз съм палаво предупредително съобщение за конзолата“);
        console.error („Аз съм палаво съобщение за грешка в конзолата“);
    }
    // Изход
    Грешки на крилото за операторите console.log и console.warn и грешка за console.error няма, тъй като не е споменато в конфигурацията
    Обажданията към „console.log“ не са позволени.
    Обажданията към „console.warn“ не са позволени.

    12) Малки компоненти за многократна употреба

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

    Като общо правило последното дете в компонентното дърво ще бъде най-тъпото от всички.

    Защо?

    Компонентите за многократна употреба намаляват дублирането на кода, поради което улесняват поддържането и извършването на промени.

    Тъмните компоненти са по-прости, така че е по-малко вероятно да имат бъгове. Тъмните компоненти ви карат да мислите по-трудно за API на публичните компоненти и ви помагат да преодолеете смесените проблеми.

    13) Компонентите трябва да се занимават само с логиката на дисплея

    Избягвайте да имате каквато и да е логика, различна от логиката за показване във вашия компонент, когато можете, и накарайте компонента да се занимава само с логиката на дисплея.

    Защо?

    Компонентите са проектирани за презентационни цели и контролират какво трябва да прави изгледът. Всяка бизнес логика трябва да бъде извлечена в собствени методи / услуги, когато е подходящо, като се разделя бизнес логиката от логиката на изгледа.

    Бизнес логиката обикновено е по-лесна за тестване на единица, когато се извлече в услуга и може да се използва повторно от всички други компоненти, които се нуждаят от прилаганата същата бизнес логика.

    14) Избягвайте дългите методи

    Дългите методи обикновено показват, че правят твърде много неща. Опитайте се да използвате единния принцип на отговорност. Самият метод като цяло може да прави едно нещо, но вътре в него има няколко други операции, които биха могли да се случат. Можем да извлечем тези методи в свой собствен метод и да ги накараме да правят едно по едно и да ги използват вместо тях.

    Защо?

    Дългите методи са трудни за четене, разбиране и поддържане. Те също са предразположени към бъгове, тъй като промяната на едно нещо може да засегне много други неща в този метод. Те също затрудняват рефакторинга (което е ключово нещо при всяко приложение).

    Това понякога се измерва като „цикломатична сложност“. Има и някои TSLint правила за откриване на цикломатична / когнитивна сложност, които бихте могли да използвате във вашия проект, за да избегнете грешки и да откриете миризми на код и проблеми с поддръжката.

    15) СУХА

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

    Защо?

    Наличието на един и същ код на няколко места означава, че ако искаме да променим логиката в този код, трябва да го направим на няколко места. Това затруднява поддържането и също така е предразположено към грешки, при които бихме могли да пропуснем актуализирането му във всички случаи. Отнема повече време, за да се правят промени в логиката, а тестването също е дълъг процес.

    В тези случаи извлечете повтарящия се код и го използвайте вместо това. Това означава само едно място за промяна и едно нещо за тестване. Имането на по-малко дублиран код, изпратен до потребителите, означава, че приложението ще бъде по-бързо.

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

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

    Ако стойностите се променят, но не често, можете да въведете време за кеш, където можете да проверите кога е било последно кеширано и да решите дали да извикате или не API.

    Защо?

    Наличието на механизъм за кеширане означава избягване на нежелани API повиквания. Чрез извършване на обаждания в API само когато е необходимо и избягване на дублирането, скоростта на приложението се подобрява, тъй като не е необходимо да чакаме мрежата. Това също означава, че не изтегляме една и съща информация отново и отново.

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

    Ако имате някаква логика в шаблоните си, дори и да е проста && клауза, е добре да я извлечете в нейния компонент.

    Защо?

    Това, че има логика в шаблона, означава, че не е възможно тестът да бъде тестван и следователно е по-предразположен към грешки при промяна на кода на шаблона.

    Преди

    // шаблон
    

    Състояние: Програмист

    // компонент
    public ngOnInit (): void {
        this.role = 'разработчик';
    }

    След

    // шаблон
    

    Състояние: Програмист

    // компонент
    public ngOnInit (): void {
        this.role = 'разработчик';
        this.showDeveloperStatus = true;
    }

    18) Струните трябва да са безопасни

    Ако имате променлива от тип низ, която може да има само набор от стойности, вместо да я декларирате като тип низ, можете да декларирате списъка с възможни стойности като тип.

    Защо?

    Чрез деклариране на типа на променливата по подходящ начин, можем да избегнем грешки, докато пишем кода по време на компилиране, а не по време на изпълнение.

    Преди

    частни myStringValue: низ;
    ако (itShouldHaveFirstValue) {
       myStringValue = 'Първо';
    } else {
       myStringValue = 'Втора'
    }

    След

    private myStringValue: 'Първо' | "Втора";
    ако (itShouldHaveFirstValue) {
       myStringValue = 'Първо';
    } else {
       myStringValue = 'Друг'
    }
    // Това ще даде грешката по-долу
    Тип "Други" 'не се приписва на тип "Първи" | "Втора" "
    (свойство) AppComponent.myValue: "Първо" | "Втора"

    По-голяма картина

    Управление на държавата

    Помислете дали да използвате @ ngrx / store за поддържане на състоянието на вашето приложение и @ ngrx / ефекти като модел за страничен ефект за съхраняване. Промените в състоянието се описват от действията, а промените се извършват от чисти функции, наречени редуктори.

    Защо?

    @ ngrx / store изолира цялата логика, свързана със състоянието, на едно място и я прави последователна в приложението. Той също има механизъм за запомняне, когато има достъп до информацията в магазина, което води до по-ефективно приложение. @ ngrx / store в комбинация със стратегията за откриване на промяна на Angular води до по-бързо приложение.

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

    Когато използвате @ ngrx / store, помислете дали да използвате ngrx-store-freeze, за да направите състоянието неизменяемо. ngrx-store-freeze предотвратява мутацията на държавата чрез хвърляне на изключение. Това избягва случайната мутация на състоянието, водеща до нежелани последствия.

    Защо?

    Мутационното състояние на компонентите води до приложението да се държи непоследователно в зависимост от зареждането на компонентите на поръчката. Той нарушава менталния модел на редукционния модел. Промените могат да се отменят, ако състоянието на магазина се промени и отново се изпрати. Разделяне на тревогите - компонентите са изглед слой, те не трябва да знаят как да променят състоянието.

    Jest

    Jest е рамката за тестване на JavaScript на Facebook за JavaScript. Това прави тестването на единицата по-бързо, като паралелизира тестовите изпълнения през кодовата база. Със своя режим на гледане се изпълняват само тестовете, свързани с направените промени, което прави цикъла за обратна връзка за начина на тестване по-кратък. Jest също осигурява кодово покритие на тестовете и се поддържа на VS Code и Webstorm.

    Можете да използвате предварителна настройка за Jest, която ще свърши по-голямата част от тежкия лифтинг за вас, когато настройвате Jest във вашия проект.

    Карма

    Karma е тестов бегач, разработен от екипа на AngularJS. За изпълнение на тестовете е необходим истински браузър / DOM. Може да работи и в различни браузъри. Jest не се нуждае от хром без глава / phantomjs, за да стартира тестовете и той работи в чист възел.

    универсален

    Ако не сте направили приложението си универсално приложение, сега е подходящ момент да го направите. Angular Universal ви позволява да стартирате вашето Angular приложение на сървъра и прави рендериране от страна на сървъра (SSR), което обслужва статични предварително представени html страници. Това прави приложението много бързо, тъй като показва съдържание на екрана почти моментално, без да се налага да чакате пакетите JS да се заредят и да анализират или Angular да се зарежда.

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

    Защо?

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

    Той също така позволява на вашия сайт правилно да се показва в фрагментите за визуализация на социалните медии. Първата смислена боя е наистина бърза и прави съдържание видимо за потребителите без никакви нежелани забавяния.

    заключение

    Изграждането на приложения е постоянно пътуване и винаги има място за подобряване на нещата. Този списък с оптимизации е добро място за стартиране и прилагането на тези модели последователно ще направи вашия екип щастлив. Вашите потребители също ще ви харесат за приятното изживяване с вашето по-малко бъги и изпълняващо приложение.

    Благодаря ви за четенето! Ако ви е харесала тази статия, моля не се колебайте да и помогнете на другите да я намерят. Моля, не се колебайте да споделите мислите си в секцията за коментари по-долу. Следвайте ме в Medium или Twitter за още статии. Честито кодиране хора !! ️