Аз съм софтуерен инженер. Идвам от света на Java. Така че, когато ми беше скучно, хакнах някои странични проекти. Проблемът беше, че трябваше да избера език плюс библиотека/рамка като градивен елемент. Знам, че също работих малко с Python, но това не беше нещо, което бих използвал, за да правя неща за интерфейса.

Особено тъй като работих много върху мобилни приложения, реших да опитам React Native. Тъй като това беше в началото/средата на 2017 г., библиотеката беше сравнително млада. И никога преди не съм работил с JavaScript, с изключение на едно малко нещо в сайта „интегриране на лентови диаграми“. Хубавото на React като цяло е, че кривата на обучение е страхотна — много е лесно да се научи. В тази статия ще опиша грубо опита, който направих с фокус върху React през последните години, също и някои малки кодови фрагменти, за да го направя кодова статия.

Реагирайте

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

Обикновено, когато създавате софтуер, вие използвате архитектурен модел, за да имате обща структура и да решите проблема си. Освен това бих добавил към това термина шаблони за проектиране, който ви помага да решавате проблеми за конкретен контекст на вашата архитектура. Много често срещан архитектурен модел е Model View Controller (MVC). Накратко, това разделя частта за изглед от контрола (въвеждане от потребителя) и моделната част (основна логика), така че тези три отделени компонента да могат да се възползват от предимствата на повторното използване на кода и паралелното разработване. Angular, например, има MVC-подобна структура.

React няма строг архитектурен модел. Това е точката, в която се заинтересува. Ако нямате модел, който да следвате като софтуерен инженер, какво правите? Вие просто избирате каквото искате. Ако искате да създадете малък уебсайт с една страница, обикновено не искате да мислите за разделяне на вашите компоненти и други неща. С React имате проста библиотека, която ви предоставя основни инструкции за изграждане на потребителски интерфейс.

Друго много страхотно нещо за React са неговите широко разпространени алтернативи. Научавате концепцията за подпори, състояние, жизнен цикъл/изобразяване веднъж и можете основно да изградите всяко основно уеб приложение, за което се сетите. За изграждане на собствени мобилни приложения има React Native и Electron може да се използва за настолни приложения (така е създаден Slack). По този начин научените концепции могат да се използват повторно или дори да се споделя код между проекти.

Сега можете да си помислите, че React е слепена библиотека. Това не е напълно вярно (или не е непременно лошо). React използва стила JavaScript и XML (JSX). Накратко, това означава, че вашият HTML код ще живее заедно с JavaScript кода (същия файл). Първо звучи странно, но е някак страхотно, когато свикнете с него. Така че това на практика би означавало, че вашият изглед, контролер и модел може да са в един файл/компонент. Ако приложението ви остане малко, всичко може да е наред с този подход. Но ако стане все по-голям и повече хора започнат да работят върху една и съща кодова база, нещата стават интересни.

Настройка на проекта

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

Настройте Linter преди да започнете в екип

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

Linters гарантира, че кодът, който сте написали, винаги изглежда така, както искате. Eslint е много популярна помощна програма за linting за JavaScript, която конфигурирате за нуждите на вашия проект. Страхотното е, че след като го настроите, можете да използвате IDE плъгини, които автоматично проверяват вашия код и ви дават подсказки/грешки/предупреждения, ако кодът не се отнася за правило. Също така поправянето на грешки в линтера при запис на файл е добра идея. Ако комбинирате процесите на linting с git-hooks, можете да сте сигурни, че изходният код на проекта има определен стандарт за качество.

Използвайте типове, когато работите в екип

Дори и да не работите в екип, връщайки се към кода си след месеци, в които не сте го разглеждали — все още ли знаете какво сте направили? Особено в JavaScript, най-често не го правя. Това е мястото, където типовете могат да бъдат много полезни.

В JavaScript има неща като Flow, които функционират като проверка на статичен тип за вашия проект. Статичната проверка на типа означава, че се откриват грешки по време на кодирането, но теоретично може да пропусне грешки, за които не знаете (например в дълбоко вложени обекти). В допълнение към това има проверки на типа на изпълнение като PropTypes за React, които ще уловят още някои грешки по време на изпълнение (но само за подпори!), което е интересно за тестване.

TypeScript има, в сравнение с Flow, много неща, които вече са конфигурирани предварително. Това не означава непременно, че Flow е по-добър или по-лош от TypeScript, но направих по-добър опит с TypeScript. Това ме „принуди“ да напиша дефиниция на тип за повечето неща, което не беше случаят с Flow (може би съм го конфигурирал погрешно). Освен това Flow понякога изразходваше много живот на батерията на моя MacBook, което напълно съвпада с тази „страхотна статия“.

Прегледи на кодове › TypeScript

Като цяло добавянето на типове към JavaScript е добро нещо, особено след като улавя някои грешки предварително и прави по-голямата част от документацията за вашия код (вижте typedoc). Интересното е, че ако прочетете проучването за Да пишете или да не пишете: Количествено определяне на откриваемите грешки в JavaScript», което намерих в тази страхотна статия, около 80% от грешките не се откриват от TypeScript. Типовите грешки съставляват около 20% от грешките във вашия проект (средно), останалото е свързано предимно с грешки в спецификацията! Така че в заключение, TypeScript (или други допълнения към JavaScript тип) няма да са достатъчни, за да уловят всички грешки (или дори половината) във вашия код.

Прегледи на код и разработка, управлявана от тестове (TDD, напишете тест преди да кодирате) и програмиране на двойки събития, могат да разрешат до 80% от често срещаните грешки. Друго интересно твърдение в тази статия, което препоръчвам да прочетете, е, че един час преглед на код спестява средно 33 часа поддръжка. Имах подобен опит, който преглеждайки (ваш собствен) код заедно с колеж, доведе до огромно количество констатации на грешки.

Структура на проекта

Наистина е важно да решите как трябва да изглежда структурата на вашия проект, преди да започнете проекта (има смисъл). Особено когато работите в екип. Според мен най-добрите практики за това трябва да бъдат документирани някъде, дори ако са само във файла readme. Има още неща, за които съм се съмнявал известно време:

  • Да наименувате файла Dashboard.js или index.js и просто да го поставите в папка за компоненти/Dashboard?
  • Имате логическа файлова структура като src/components src/container src/config или по-поведенческа, като src/usercontrols src/adminelements src/utils
  • Когато пишете тестове: E2E тестовете обикновено се поставят в папката root/e2e на проекта или подобна. Но какво ще кажете за теста за модул и интеграция? Поставете ги в папка __test__ под src/ или в йерархията на тествания елемент?

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

Среда за тестване

Като цяло повече тестове ви дават по-голяма увереност, че вашата система има по-малко грешки. Има различни нива на тестване, като се започне от модулни и интеграционни тестове, до End-to-End (E2E) и тестове за приемане. Написах статия за E2E тестване в React Native. Все пак бих искал да споделя част от моя опит в тестването тук.

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

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

Повечето от грешките не могат да бъдат предотвратени, разбира се. Да приемем, че намалявате версията на компилация на minSDK за вашето приложение за Android, тъй като нова библиотека изисква/препоръчва това. Но това, което сте забравили е, че вашето приложение трябва да има конкретна версия на minSDK, което води до компилация, която няма да работи на всяко устройство. Това може да бъде предотвратено, като оставите вашите промени да бъдат прегледани от различен човек или дори заедно (партньорски прегледи). Заявките за изтегляне са много често срещана техника тук.

Непрекъсната интеграция и непрекъсната доставка

През повечето време разработчиците трябва да изпълняват повтарящи се задачи за издание. Това често включва неща като провеждане на тестове/линтери, групиране, тестове за приемане и качване. Без съмнение това отнема време и нерви, но може да бъде сведено до минимум чрез използване на външни CI/CD услуги. Със сигурност можете да получите DevOps инженер, който да ви настрои това, но често това изисква много време и опит. Изключение би било, ако работите с поверителни данни (клиентите ви имат доверие) и следователно трябва да го направите сами.

Redux

Когато проектът стане по-голям, можете (трябва) да добавите библиотека, която позволява еднопосочен поток от данни за вашето приложение. Един много известен модел за това е Flux. Тъй като това е само модел, можете да избирате от различни реализации на библиотека като Redux или MobX. Аз лично работех само с Redux в миналото и наистина е страхотно, след като свикнете с основните концепции.

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

Освен това тестването ще стане по-лесно с redux в сравнение със самостоятелното управление на състоянието. Това е така, защото има определени процеси за промяна на състоянието, в смисъл, че само специфични методи (действия) могат да променят състоянието. Състоянието е част от вашия редукс магазин и може да бъде разделено на различни части. Да приемем, че искате да имате разделени приложения/настройки и приложение/потребителски профил. Това има смисъл, особено след като един редуктор има достъп само до това конкретно подсъстояние.

Redux междинен софтуер

Мидълуерите на Redux са конфигурирани „между“ вашето приложение и магазина на redux, за да правят неща. Едно нещо може да бъде да запазите части от вашия магазин, така че приложението ви да може да се използва офлайн. Друго нещо, което определено има смисъл, е мидълуер за регистратор (redux-logger). С това можете да видите кое действие има какво въздействие върху вашия магазин, включително времеви отпечатъци и разлики в състоянието преди/след като действието е било изпратено.

Сега, когато управлението на състоянието на приложението е обработено, какво ще кажете за обработката на странични ефекти, като просто извличане на потребителския профил, след като са били успешно удостоверени със сървъра? Това може да се направи с помощта на редукс междинен софтуер като redux-saga или redux-thunk. Накратко, in ви дава начин за извличане на данни, преглеждане на данни от магазина за намаляване и задействане на действия за намаляване, за да промените магазина/състоянието на вашето приложение. Разбира се, всичко това може да се направи и в компонентите на приложението, но рано или късно вашият код ще се превърне в бъркотия и вече няма да може да се тества.

За да заключим, че: не можете (и не трябва) да правите повиквания към сървър или подобен в редуциращ редуктор. Redux действие се изпраща от redux редуктор, който след това прави промени в своето хранилище, без да чака секунди за разрешаване на заявка от сървъра. За това е мидълуерът.

Асинхронен/изчакване

Едно нещо, което много разработчици на JavaScript харесват, е синтаксисът async/await, който значително подобрява четливостта на изразите Promise.then(). Особено когато имате вложени оператори then(), може да бъде много полезно да използвате async/await. Не искам да обяснявам как работи async/await, има много страхотни статии, които правят това.

const fetchFood = new Promise((resolve, reject) => { 
  setTimeout(() => resolve('food'), 1000); 
});
const fetchWater = new Promise((resolve, reject) => { 
  setTimeout(() => resolve('water'), 300); 
});
const fetchBread = new Promise((resolve, reject) => { 
  setTimeout(() => resolve('bread'), 500); 
});
const main = async () => {
  console.time('time');
  const food = await fetchFood;
  const water = await fetchWater;
  const bread = await fetchBread;
  console.timeEnd('time'); // ~1000ms
  
  // equivalent
  // const [food, water, bread] = await Promise.all([fetchFood,   fetchWater, fetchBread]);
}
main();

Интересното тук е, че разрешаването на тези три обещания отнема само около 1 секунда, най-високото време за изчакване. Разбира се, това не е свързано с async/await, а с Promises. Това, което научих наскоро е, че можете да използвате Promise.all, което се разрешава, когато всички преминали обещания са разрешени.

Оператор за разпространение на почивка

Останалият оператор за разпространение ви позволява да разширявате обекти и масиви или по-просто: да ги свързвате. Операцията ще създаде нов обект или масив:

const one = [1, 2];
const two = [3, 4];
console.log([...one, ...two]); // [ 1, 2, 3, 4 ]
console.log([...one, ...two, 5, 6]); // [ 1, 2, 3, 4, 5, 6 ]
console.log([...one] === one); // false

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

const someObject = {
  magicValue: 42,
};
const hugeObject = {
  ...someObject,
  someValue: 37,
  deepObject: {
    foo: [42, 43],
  }
}
const overridenObject = {
  ...hugeObject,
  someValue: 24,
  deepObject: {
    someArray: [91, 21], // foo[] will be overridden
  }
}
const notOverridenObject = {
  ...hugeObject,
  anotherValue: 37,
  deepObject: {
    ...hugeObject.deepObject, // extend nested object
    someArray: [42, 43],
  }
}

Това не трябва да се бърка с останалите параметри, които имат за цел да съберат всички останали параметри, предадени на функция в масив:

const someSumFunction = (...args) => {
  return args.reduce((sum, value) => sum + value)
}
console.log(someSumFunction(1, 2, 3));

Този метод просто взема параметри, които ще бъдат трансформирани в масив и връща сумата от неговите стойности.

Изпълнение на React

React има жизнен цикъл на компонента, което е много важно да разберете, ако искате да извлечете максимална производителност от вашето приложение. React native трябва да вземе предвид някои специални неща, защото очевидно трябва да комуникира с основната страна (iOS или Android) чрез мост за съобщения, така че всичко е някак сериализирано от JavaScript в JavaScript Object Notation (JSON) и интерпретирано от родна страна. Но няма да навлизам в подробности тук.

Жизненият цикъл на компонента на реакция е много „добре документиран тук“ и описва различните събития, които се случват около DOM и компонент.

Най-важният метод е render(), който прави точно това, което се казва. Извиква се, когато подпорите или състоянието са се променили. Така че само оценява (не променя) стойностите на this.props и this.state и връща елемент, който да се използва в DOM.

„Тази статия описва“ много неща, които трябва да знаете за производителността на React (и Redux). Едно от най-често срещаните неща, с които се сблъсках, бяха методите на обвързване. React използва shallowCompare (в метода си render()). Това означава, че всеки ключ се проверява за строго равенство. Ако следвате това, ако дори само ref на обект се промени, обектите не са равни. Такъв е случаят с функциите и масива, така че те създават нови препратки всеки път:

const oneFunction = someInput => { return someInput; }
const theSameFunction = theSameInput => { return theSameInput; }
console.log(oneFunction === theSameFunction); // false
console.log([42, 43] === [42, 43]); // false

Това, което можете да видите е, че функциите и масивите никога не са „равни“, тъй като винаги ще има създаден нов, така че „вътрешната препратка“ се различава. И тук няма значение дали правите стриктна проверка за равенство, както направих аз, или слабата (==). Това е така, защото ние не „понижаваме“ нито един тип, както бихме направили, ако проверим например (42 == ’42’). Immutable.js може да бъде много полезен в тези случаи.

Всъщност причината зад това „създава нов обект“ е, че трябва да внимавате, когато правите това. Ако създавате нови функции всеки път, когато се извиква методът render() (което според мен потенциално може да се случи 60 пъти в секунда), това може да убие производителността. Ето защо свързвате функции към класа/компонента в конструктора, така че това няма да се случи. Ако се опитате да получите достъп до състояние или реквизити от функция, която не е обвързана с this, това ще доведе до грешка (очевидно защото this е неизвестно).

Това също се брои за абонаменти/наблюдения за промени в състоянието при използване на redux. Да кажем, че всеки път, когато състоянието на вашето приложение се промени, вие филтрирате, картографирате и намалявате масив от това състояние. Всеки път, когато правите това, създавате нов масив. Особено ако този масив не се е променил, вие създавате ненужна работа, което води до работа за събирача на отпадъци, което води до евентуални течове на памет, лоша производителност и т.н.

В допълнение към предотвратяването на създаването на нови обекти или извиквания на функции в метода render(), можете също да предотвратите извикването на този метод, като използвате shouldComponentUpdate. Друго важно нещо е разбирането на метода setState. Най-важното е, че не променя незабавно състоянието на компонента, а по-скоро дава заявка към него - която може да бъде отложена, групирана или каквото и да е от React. Все още можете да реагирате на промените във вашето състояние, като използвате обратното извикване setState или componentDidUpdate.

React Native и производителност

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

Друго нещо, от което хората могат да се оплакват, е производителността в React Native. Приложения като игри или просто изчислително интензивни може да не са най-производителните. Причината за това е, че React Native има мост за опашка за съобщения, който се използва за всякакъв вид взаимодействие между нативната и JavaScript страна. Също така се препоръчва първо да разработите вашето приложение за Android, тъй като iOS е най-вече по-лесно от гледна точка на интегриране на модули и производителност.

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

Първоначално публикувано в mariusreimer.com на 10 март 2019 г.