Кращі практики для чистого та ефективного кутового застосування

Я працюю над масштабним кутовим застосуванням у Trade Me, Нова Зеландія, вже пару років. Протягом останніх декількох років наша команда вдосконалювала наше додаток як щодо стандартів кодування, так і продуктивності, щоб зробити його в найкращому стані.

У цій статті викладено практику, яку ми використовуємо в нашому додатку, і пов’язано з Angular, Typescript, RxJs та @ ngrx / store. Ми також ознайомимось із загальними рекомендаціями щодо кодування, щоб зробити додаток більш чистим.

1) trackBy

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

Чому?

Коли масив змінюється, Angular повторно рендерує все дерево DOM. Але якщо ви використовуєте trackBy, Angular дізнається, який елемент змінився, і внесе зміни DOM лише для цього конкретного елемента.

Для детального пояснення цього питання, будь ласка, зверніться до цієї статті Нетанеля Базаля.

Раніше

  • {{item}}
  • Після

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

    2) const vs let

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

    Чому?

    Використання let та const, де це доречно, робить намір декларацій яснішим. Це також допоможе визначити проблеми, коли значення перевласнено на константу випадково, передавши помилку часу компіляції. Це також допомагає поліпшити читабельність коду.

    Раніше

    нехай автомобіль = 'смішний автомобіль';
    нехай myCar = `Мій $ {автомобіль}`;
    нехай yourCar = `Ваш $ {car};
    якщо (iHaveMoreThanOneCar) {
       myCar = `$ {myCar} s`;
    }
    якщо (youHaveMoreThanOneCar) {
       yourCar = `$ {youCar} s`;
    }

    Після

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

    3) Трубопровідні оператори

    Використовуйте трубопровідні оператори при використанні операторів RxJs.

    Чому?

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

    Це також дозволяє легко ідентифікувати невикористані оператори у файлах.

    Примітка. Для цього потрібна кутова версія 5.5+.

    Раніше

    імпорт 'rxjs / add / operator / map';
    імпорт 'rxjs / add / operator / take';
    iAmAnObservable
        .map (value => value.item)
        .прийняти (1);

    Після

    import {map, take} від 'rxjs / operator';
    iAmAnObservable
        .pipe (
           map (value => value.item),
           взяти (1)
         );

    4) Ізолювати хаки API

    Не всі API є кулезахисними - іноді нам потрібно додати певну логіку в код, щоб компенсувати помилки в API. Замість того, щоб мати хаки в компонентах там, де вони потрібні, краще ізолювати їх в одному місці - як у сервісі та використовувати службу від компонента.

    Чому?

    Це допомагає хакам "наблизитись до API", щоб максимально наблизитись до того, де можливий мережевий запит. Таким чином, менше вашого коду має справу з невлаштованим кодом. Крім того, це одне місце, де живуть усі хаки, і їх легше знайти. Виправляючи помилки в API, їх простіше шукати в одному файлі, а не шукати хаки, які можуть бути розповсюджені по кодовій базі.

    Ви також можете створити власні теги, такі як API_FIX, схожі на TODO, і позначити їх виправлення, щоб їх було легше знайти.

    5) Підписатися в шаблоні

    Уникайте передплати спостережних даних із компонентів, а замість цього підписуйтесь на спостереження з шаблону.

    Чому?

    Асинхронні труби скасовують підписку автоматично, і це робить код простішим, усуваючи необхідність вручну керувати підписками. Це також зменшує ризик випадкового забуття скасувати підписку на компонент, що може спричинити витік пам'яті. Цей ризик можна також зменшити, використовуючи правило лінт для виявлення незапланованих спостережень.

    Це також не дозволяє компонентам стати державними і вводити помилки, коли дані мутуються поза підпискою.

    Раніше

    // шаблон

    {{textToDisplay}}

    // компонент
    iAmAnObservable
        .pipe (
           map (value => value.item),
           takeUntil (це $. знищено $)
         )
        .subscribe (item => this.textToDisplay = item);

    Після

    // шаблон

    {{textToDisplay $ | async}}

    // компонент
    this.textToDisplay $ = iAmAnObservable
        .pipe (
           map (value => value.item)
         );

    6) Очистити підписки

    Підписуючись на спостереження, завжди переконайтеся, що ви належним чином скасували підписку на них, використовуючи такі оператори, як 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 (це $. знищено $)
         )
        .subscribe (item => this.textToDisplay = item);
    }
    public ngOnDestroy (): void {
        this._destroyed $ .next ();
        this._destroyed $ .complete ();
    }

    Використання приватного предмета, подібного до цього, є схемою для керування відпискою багатьох спостережуваних компонентів.

    Використовуючи взяти, коли потрібно лише перше значення, випущене спостережуваним:

    iAmAnObservable
        .pipe (
           map (value => value.item),
           взяти (1),
           takeUntil (це $. знищено $)
        )
        .subscribe (item => this.textToDisplay = item);

    Зверніть увагу на використання тут takeUntil with take. Це дозволяє уникнути витоків пам'яті, спричинених, коли підписка не отримала значення перед тим, як компонент був знищений. Без takeUntil тут підписка все одно буде зависати, поки вона не отримає перше значення, але оскільки компонент уже знищений, він ніколи не отримає значення - це призведе до витоку пам'яті.

    7) Використовуйте відповідні оператори

    Під час використання операторів вирівнювання з вашими спостереженнями використовуйте відповідного оператора для ситуації.

    switchMap: коли ви хочете ігнорувати попередні викиди, коли є нові викиди

    mergeMap: коли ви хочете одночасно обробляти всі викиди

    concatMap: коли ви хочете обробляти викиди одна за одною у міру їх викиду

    exhaustMap: коли ви хочете скасувати всі нові викиди під час обробки попереднього викиду

    Для більш детального пояснення цього питання, будь ласка, зверніться до цієї статті Ніколаса Джеймісона.

    Чому?

    Використання одного оператора, коли це можливо, замість того, щоб об'єднати кілька інших операторів для досягнення того ж ефекту, може призвести до того, що користувачеві буде доставлено менше коду. Використання неправильних операторів може призвести до небажаної поведінки, оскільки різні оператори по-різному обробляють спостереження.

    8) Ледачий навантаження

    Коли це можливо, намагайтеся ледаче завантажувати модулі у вашій програмі Angular. Ледаче завантаження - це коли ви щось завантажуєте лише тоді, коли воно використовується, наприклад, завантажуючи компонент лише тоді, коли його потрібно побачити.

    Чому?

    Це зменшить розмір завантажуваної програми та може покращити час завантаження програми, не завантажуючи модулі, які не використовуються.

    Раніше

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

    Після

    // app.routing.ts
    {
      шлях: 'ледачий вантаж',
      loadChildren: 'lazy-load.module # LazyLoadModule'
    }
    // lazy-load.module.ts
    імпорт {NgModule} з '@ angular / core';
    імпорт {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 (секундаObservable $),
        спочатку()
    )
    .subscribe (([firstValue, secondValue]) => {
        console.log (`Комбіновані значення: $ {firstValue} & $ {secondValue}`);
    });

    Чому?

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

    Ефективність: Якщо спостереження холодні, воно підпишеться на першеЗабезпечене, дочекайтеся його завершення, ТОГО розпочніть роботу другого спостережуваного. Якби це запити мережі, вони відображалися б як синхронний / водоспад.

    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" "не призначається типу" число ".
    const y: число

    Таким чином ми можемо уникнути помилок, спричинених відсутніми типами.

    Ще одна перевага хорошого типізації у вашій програмі полягає в тому, що це робить рефакторинг простішим та безпечнішим.

    Розглянемо цей приклад:

    public ngOnInit (): void {
        нехай myFlashObject = {
            назва: "Моє круте ім'я",
            вік: "Мій класний вік",
            loc: "Моє прохолодне місце"
        }
        this.processObject (myFlashObject);
    }
    public processObject (myObject: будь-який): void {
        console.log (`Ім'я: $ {myObject.name}`);
        console.log (`Вік: $ {myObject.age}`);
        console.log (`Розташування: $ {myObject.loc}`);
    }
    // Вихід
    Ім'я: Моє круте ім’я
    Вік: Мій класний вік
    Розташування: Моє прохолодне місце

    Скажімо, ми хочемо перейменувати локальний ресурс у розташування в myFlashObject:

    public ngOnInit (): void {
        нехай myFlashObject = {
            назва: "Моє круте ім'я",
            вік: "Мій класний вік",
            місцезнаходження: "Моє прохолодне місце"
        }
        this.processObject (myFlashObject);
    }
    public processObject (myObject: будь-який): void {
        console.log (`Ім'я: $ {myObject.name}`);
        console.log (`Вік: $ {myObject.age}`);
        console.log (`Розташування: $ {myObject.loc}`);
    }
    // Вихід
    Ім'я: Моє круте ім’я
    Вік: Мій класний вік
    Місцезнаходження: не визначено

    Якщо у нас немає набору тексту myFlashObject, він вважає, що властивість loc у myFlashObject просто не визначена, а не що це недійсна властивість.

    Якби у нас було введення тексту myFlashObject, ми отримали б непогану помилку часу компіляції, як показано нижче:

    введіть FlashObject = {
        назва: рядок,
        вік: рядок,
        розташування: рядок
    }
    public ngOnInit (): void {
        нехай myFlashObject: FlashObject = {
            назва: "Моє круте ім'я",
            вік: "Мій класний вік",
            // Помилка компіляції
            Введіть '{ім'я: рядок; вік: рядок; loc: рядок; } 'не призначається типу "FlashObjectType".
            Літерал об'єкта може вказувати лише відомі властивості, а "loc" не існує у типі "FlashObjectType".
            loc: "Моє прохолодне місце"
        }
        this.processObject (myFlashObject);
    }
    public processObject (myObject: FlashObject): void {
        console.log (`Ім'я: $ {myObject.name}`);
        console.log (`Вік: $ {myObject.age}`)
        // Помилка компіляції
        Властивість 'loc' не існує у типі 'FlashObjectType'.
        console.log (`Розташування: $ {myObject.loc}`);
    }

    Якщо ви починаєте новий проект, варто встановити строго: true у файлі tsconfig.json, щоб увімкнути всі строгі параметри перевірки типу.

    11) Скористайтеся правилами ворсу

    tslint має різні варіанти, вбудовані вже як ні-які, ні-магічні номери, ні консолі тощо, які ви можете налаштувати у своєму tslint.json для виконання певних правил у своїй кодовій базі.

    Чому?

    Якщо встановити правила підказки, це означає, що ви отримаєте приємну помилку, коли будете робити щось, чого не повинно бути. Це забезпечить послідовність вашої програми та читабельність. Будь ласка, зверніться сюди, щоб отримати більше правил, які ви можете налаштувати.

    Деякі правила вказівки навіть поставляються з виправленнями, щоб вирішити помилку волокна. Якщо ви хочете налаштувати власне правило власного вказівника, ви також можете це зробити. Будь ласка, зверніться до цієї статті Крейга Спенса про те, як написати власні власні правила вказівки за допомогою TSQuery.

    Раніше

    public ngOnInit (): void {
        console.log ("Я неслухняне повідомлення журналу консолі");
        console.warn ("Я неслухняне попередження консолі");
        console.error ("Я неслухняне повідомлення про помилку консолі");
    }
    // Вихід
    Немає помилок, друкується нижче на вікні консолі:
    Я неслухняне консольне повідомлення
    Я неслухняне попередження консолі
    Я неслухняне повідомлення про помилку консолі

    Після

    // tslint.json
    {
        "правила": {
            .......
            "без консолі": [
                 правда,
                 "log", // no console.log не дозволено
                 "застерегти" // no console.warn дозволено
            ]
       }
    }
    // ..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: рядок;
    if (itShouldHaveFirstValue) {
       myStringValue = 'Перший';
    } else {
       myStringValue = 'Другий'
    }

    Після

    приватний myStringValue: 'Перший' | ‘Другий’;
    if (itShouldHaveFirstValue) {
       myStringValue = 'Перший';
    } else {
       myStringValue = 'Інше'
    }
    // Це призведе до наведеної нижче помилки
    Тип "Інше" 'не призначається типу "Перший" | "Другий" '
    (властивість) AppComponent.myValue: "Перший" | "Другий"

    Більша картина

    Державне управління

    Подумайте про використання @ ngrx / store для підтримки стану вашої програми та @ ngrx / ефекти як моделі побічних ефектів для зберігання. Зміни стану описуються діями, а зміни здійснюються чистими функціями, які називаються редукторами.

    Чому?

    @ ngrx / store виділяє всю логіку, пов’язану зі станом, в одному місці та робить її послідовною у додатку. Він також має механізм запам'ятовування, коли він отримує доступ до інформації в магазині, що веде до більш ефективного застосування. @ ngrx / store у поєднанні зі стратегією виявлення змін Angular призводить до швидшого застосування.

    Незмінний стан

    Використовуючи @ ngrx / store, подумайте про використання ngrx-store-freeze, щоб зробити стан незмінним. ngrx-store-freeze запобігає мутації стану, викинувши виняток. Це дозволяє уникнути випадкових мутацій стану, що призводять до небажаних наслідків.

    Чому?

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

    Jest

    Jest - це тестова рамка Facebook для тестування JavaScript. Це робить тестування одиниць більш швидким шляхом паралелізації тестових запусків по всій базі коду. У режимі його перегляду виконуються лише тести, пов'язані із внесеними змінами, що робить цикл зворотного зв'язку для способу тестування коротшим. Jest також забезпечує кодове покриття тестів і підтримується на VS Code та Webstorm.

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

    Карма

    Karma - тестовий бігун, розроблений командою AngularJS. Для запуску тестів потрібен справжній браузер / DOM. Він також може працювати в різних браузерах. Для запуску тестів Jest не потрібні хромовані безголові / phantomjs, і він працює в чистому вузлі.

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

    Якщо ви ще не зробили свій додаток універсальним додатком, зараз настав час зробити це. Angular Universal дозволяє запускати вашу програму Angular на сервері та робить рендеринг на стороні сервера (SSR), який обслуговує статичні заздалегідь надані HTML-сторінки. Це робить додаток дуже швидким, оскільки він показує вміст на екрані майже миттєво, не чекаючи завантаження та розбору пакетів JS або для кутового завантаження.

    Це також зручно для SEO, оскільки Angular Universal створює статичний контент і полегшує веб-сканерам індексувати додаток та робить його пошуковим, не виконуючи JavaScript.

    Чому?

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

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

    Висновок

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

    Дякую за прочитання! Якщо вам сподобалася ця стаття, не соромтесь та допомагайте іншим її знайти. Будь ласка, не соромтеся поділитися своїми думками у розділі коментарів нижче. Слідкуйте за мною на "Середній" або "Twitter", щоб отримати більше статей. Щасливі люди з кодування !! ️