Rádi v MoroSystems zkoušíme nové technologie a pokud jejich použití dává smysl, zavádíme je okamžitě do praxe. Nedávno jsme udělali menší revoluci a přešli z jQuery, se kterým máme rozsáhlé dlouholeté zkušenosti, na React, konkrétně ES6, Less, Redux, Webpack a npm.
Proč jsme začali o použití Reactu vůbec uvažovat? Přiměl nás k tomu projekt pro eBay, na kterém několik let pracujeme. Klient nám zadal vývoj zcela nového a velmi rozsáhlého modulu, který je velmi náročný nejen na samotný vývoj a vývojový proces, ale také na výkon aplikace, její použitelnost z hlediska efektivity práce s rozhraním a dostupnost aplikace.
Tento nový seriál Tomových článků by měl nabízet tipy komukoli, kdo se pustí do psaní aplikace v podobných technologiích ve více než jednom či dvou lidech nebo třeba i těm, kteří o použití těchto technologií zatím jen uvažují.
První díl přináší pár základních technologických a designových poznatků, které se nám osvědčili a mohou se hodit úplně každému.
Unit Testy
Zprovoznění rychlých unit testů na npm run test:watch bylo jednou z nejlepších věcí, co mohla lidi, zvyklé na špagetový jQuery kód, potkat. Umožnila použití TDD, ke kterému zatím přistupujeme poměrně konzervativně, ale oproti stavu kdy 99% UI kódu bylo netestovatelné, je to velký pokrok.
Inspirovali jsme se přístupem A STEP-BY-STEP TDD APPROACH ON TESTING REACT COMPONENTS USING ENZYME. V našem případě jsou testovatelné nejen React komponenty, ale i akce a reducery.
import React from "react"; import {expect} from "chai"; import {shallow} from "enzyme"; import YesNoFormatter from "./YesNoFormatter"; import FontAwesome from "react-fontawesome"; describe('<yesnoformatter></yesnoformatter>', () => { it('should exists', () => { expect(YesNoFormatter).to.not.equal(null); }); it("should render FontAwesome element with class 'formatter-yesNo'", () => { const elem = shallow(<yesnoformatter></yesnoformatter>); const value = elem.find(FontAwesome); expect(value.hasClass("formatter-yesNo")).to.be.equal(true); }); it("should have default false value prop", () => { const elem = shallow(<yesnoformatter></yesnoformatter>).find(FontAwesome); const elem1 = shallow(<yesnoformatter value={false}></yesnoformatter>).find(FontAwesome); expect(elem.prop("name")).to.be.equal(elem1.prop("name")); }); it("should render 'check' FontAwesome when value is true", () => { const elem = shallow(<yesnoformatter value={true}></yesnoformatter>); const value = elem.find(FontAwesome); expect(value.prop("name")).to.be.equal("check"); }); it("should render 'times' FontAwesome when value is false", () => { const elem = shallow(<yesnoformatter value={false}></yesnoformatter>); const value = elem.find(FontAwesome); expect(value.prop("name")).to.be.equal("times"); }); });
Výpis testu pak vypadá takto:
<yesnoformatter></yesnoformatter> √ should exists √ should render FontAwesome element with class 'formatter-yesNo' √ should have default false value prop √ should render 'check' FontAwesome when value is true √ should render 'times' FontAwesome when value is false
Less.css
Použití nějakého CSS preprocesoru, v naše případě Less, lze ale použít i Sass, je už v dnešní době na větších projektech standardem. Skvělým příkladem je zanořování pravidel. Dva hlavní důvody, pro které Less používáme, jsou kontrola nad CSS namespace, a proměnné (především barvy).
Kontrola namespace
V každém větším projektu časem přijde CSS do stavu, kdy v tisíc-řádkovém souboru není možné reálně nic najít, a tím pádem neustále dál roste a roste. Pro tento účel jsme kód strukturovali tak, aby existovalo jen několik málo míst, kde se definuje, na jakou konkrétní třídu se má něco použít od toho, jak to má vypadat.
Uvedu konkrétní příklad: řekněme že máme soubor NoneFormatter.less, ve kterém je obsažena definice toho, jak má daná komponenta vypadat.
.none-formatter(){ color: gray; font-weight: normal; font-size: 12px; }
To samotné ale nestačí k tomu, aby se to někde projevilo. K tomu potřebujeme tento mixin navázat na nějaký selektor. Od toho je zde pak soubor components/index.less, kde jsou všechny tyto definice reálně použity.
@import (reference) "NoneFormatter"; @import (reference) "UserFormatter"; @import (reference) "UserListFormatter"; @import (reference) "YesNoFormatter"; .userlist { .userlist(); } .formatter-none { .none-formatter(); } .formatter-user { .user-formatter(); } .formatter-yesNo { .YesNoFormatter(); }
Důležité je, že v případě potřeby zjistit, kde je nějaký styl použit nebo jaké všechny třídy jsou obsazeny, stačí pohled do jednoho souboru (nebo do několika málo souborů). Za povšimnutí také stojí použití volby reference pro import, která zajišťuje, že z includovaných souborů se nic nedostane do globálního namespace. Includují se pouze mixiny které je nutno ručně použít
Další přidanou hodnotou je to, že pokud se někdy rozhodneme vytvořit prvek, který vypadá jako userlist z příkladu výše, stačí na patřičný prvek aplikovat už hotový mixin a máme prakticky hotovo.
Proměnné
Různé úpravy barev, velikostí a odsazení jsou při ladění aplikace na denním pořádku a je tedy ideální mít je na jednom místě. Proto používáme a rozšiřujeme přístup od Bootstrapu, kdy klíčové barvy, velikosti a podobně nedáváme do stylů napevno, ale referencujeme je přes proměnné.
@import "../../main/less/colors"; .none-formatter(){ color: lighten(@gray-light, 25%); font-weight: normal; font-size: @font-size-base; }
//***************DEFAULTS @import "~bootstrap/less/variables"; //***************OVERRIDED @border-radius-base: 2px; @border-radius-large: @border-radius-base; @border-radius-small: 2px; @input-border: #ddd; @text-color: @gray; @headings-color: @gray-dark; @brand-info: #FCFCFC; @brand-primary: #296CA3; @state-info-text: #31708f; @state-info-bg: lighten(#d9edf7, 7%); @font-size-large: 18px; //***************CUSTOM //Backgrounds @sparc-dark-panel-background: #ddd; @sparc-basic-panel-background: #FFFFFF; @sparc-light-panel-background: #F5F5F5;
Tímto přístupem jsme schopni změnit primární barvu aplikace (call to action prvky, aktivní barvy prvků, různá podbarvení, odkazy, …), @brand-primary, během několika sekund.
To vše ale jedině za předpokladu, že komponenty jsou napsány tak, aby s tímto přístupem spolupracovaly. To vyžaduje poměrně striktní code review pro lidi, kteří nejsou zvyklí CSS styly příliš řešit.
PageLayout komponenty
Jak často slyšíte požadavek typu: „Chtěl bych novou stránku, aby vypadala stejně jako ten dashboard co tam máme…“.
Většinou to znamená zkopírovat html stránky, nějaké ty css styly, napojit to na jiná data atd… Jenže ve skutečnosti chce programátor napsat asi toto: „Použij standardní layout pro Dashboard, do záhlaví dej tenhle text, vlevo dej komponentu pro filtr, doprostřed tabulku a do pravého panelu nic dávat nebudeme“.
Čemuž odpovídá zhruba tento kus kódu, který se postará o všechny styly, formátování atd. Co si na patřičná místo vloží programátor, to už je jeho věc.
import {DashboardLayout} from "../../../layout"; import TableContainer from "./containers/TableContainer"; import FilterContainer from './containers/FilterContainer'; const Component = () => { const layoutParams = { topSection: <h1>My static header</h1>, leftSection: <filtercontainer></filtercontainer>, mainContentSection: <tablecontainer></tablecontainer>, rightSection: null //for demonstration, not needed to specify }; return (<dashboardlayout {...layoutParams}></dashboardlayout>); };
Nejedná se o žádnou převratnou novinku, je to standardní přístup z komponentových frameworků (Spring, Vaadin, …). Byl ale v průběhu vývoje webových technologií trochu pozapomenut.
Formatter komponenty
Dalším typem komponenty, kterou určitě chcete mít v aplikaci je něco, co nazýváme Formatter. Stejně jako v aplikaci existuje standardní cesta, jakou zadávat např datumy nebo text, měl by existovat i způsob, jakým standardně zobrazovat (formátovat) určitý typ informace (string, datum, uživatel, boolean). Tyto formattery jsou pak použity všude, kde daný typ informace zobrazujeme, ať už natvrdo v kódu nebo dynamicky dle nějakého informace z backendu (sloupeček v tabulce).
Kromě očividných výhod typu, že změna zobrazení se provádí na jednom místě (DRY), to vede i k dobrého UX patternu – jednotný styl prezentace informací tlačen zespoda směrem od týmu („Vážně chceš tady pro to místo naprogramovat speciální formátování? To zabere dost času. Kdybychom ale použili to co máme všude jinde…„).
Reálný příklad z dnešního dne z projektu je změna YesNo formatteru, kdy během 5 minut celá aplikace na všech místech prokoukla tím že se přidala ikonka místo textu.
//original const YesNoFormatter = ({value}) => ( <span className="formatter-yesNo">{value ? "Yes" : "No"}</span> ); //new const YesNoFormatter = ({value}) => ( <fontawesome className="formatter-yesNo" name={value ? "check" : "times"}></fontawesome> );
Na co se můžete těšit v dalším díle
Když jsem se ptal Toma na to, o čem bude psát v dalším díle, abych to zde uvedl jako teaser, odpověděl mi:
„To právě nevím, jinak bych to napsal už teď :-D. Určitě bude do dalšího dílů možnost psát o nějaké infrastruktuře. Případně můžem pak některé části kódu dát k dispozici, ale to jsem nechtěl slibovat/psát předčasně, než se bude vědět přesně co a jak. A určitě budem moct popisovat strukturu projektu, tu jsem necpal sem bo jsem si to ještě netroufal to popsat.“.
Tak si z toho vyberte, zůstaňte naladěni a těšte se na příště :-)
Tomáš Jílek
Tomáš je fullstack Senior Developer se zaměřením na front-end a UX. Má rád JavaScript, nové technologie a pěkné, rychlé a dobře použitelné uživatelské rozhraní.
Zajímá tě front-end stejně jako Toma nebo ho chceš naučit?
Teď máš jedinečnou možnost naučit se u nás práci s Reactem od těch nejzkušenějších!
17.8.2016 at 18:35
Supr článek, teším sa na ďalší!
18.8.2016 at 05:51
Pekne napsano, jsem zvedavy na dalsi dil :) Resite uz nejak i klikaci testy, nebo zatim jen unity?
18.8.2016 at 06:58
Hoj,
klikací testy nejsou specifické pro React nebo tak, řešíme je tady tedy stejně jako na jiných projektech (akorát je tu víc asynchronicity a nemůžeš čekat na page refresh).
Řešíme je klasickým Seleniem v Javě (zajímala by tě konkrétní nastavení?), kde to píšeme víceméně PageObject patternem (http://www.assertselenium.com/automation-design-practices/page-object-pattern/)
V poslední době ale začínáme experimentovat ještě se Serenity (nástroj pro BDD http://www.thucydides.info/), což je taková nadstavba pro klikací testy na (mimo jiné) akceptační kritéria
23.8.2016 at 07:01
Ahoj,
článek je zajímavej, nicméně předpokládám že je to jen „vanilla“ ukázka jak by to asi mělo vypadat.
Testy beru jako součást projektu a v článku mi přijde jejich formát značně nepřehlednej. Pojmenovávat proměnné `elem` nebo `value` mi nepřijde zrovna vhodné.
Co se týče stylů, není vhodnější přikládat styly formou `include` přímo ke komponentě a řešit namespaces takhle ? Pomůže to trochu i tomu, že se na klienta stahuje CSS jen v případě možnosti jeho aplikace.
V ukázce komponentového layoutu je natvrdo string v „. Řešili jste už lokalizaci React komponent ?
Dík
23.8.2016 at 08:58
Ahoj,
jasně že je to jen ukázka některých základních věcí které jsem sebral, a spíš jednotlivostí než než celkového přístupu. Je možné, že časem bude dostupná i nějaký kód k použití :)
Ohledně testů máš pravdu, jsou základ. Akorát jsme měli trochu složitější fázi zvykání si na ně, a tak zavedení a zvyknutí si na testy alespoň v takovéhle podobně velký skok. Uvedl jsem to na začátek hlavně jako ujištění pro ty, co je náhodou ještě nedělají, že už by fakt už měli :)
(btw formátování kódu na blogu už řešíme)
Includovat styly per komponenta jsme taky zvažovali, ale ve výsledku to neřeší (náš) hlavní problém s css – zjistit na co se jaká třída používá a případně kde jsou nějaké namespace konflikty. Což by se dalo řešit striktní unikátností názvů className pro komponenty, ale to by zase rychle přešlo v prasení stylů když bude stylovat nějak komponentu v jiném kontextu.
Rychlost načítání v našem případě také není relevantní, všechny styly bundlujeme přes webpack do jednoho css (časem to možná rozdělíme a těm stálejší částem nastavíme nějakou rozumnou cache).
Proto radši styly includujeme čistě přes @include v less, ale samotný Less soubor je u komponenty, takže není problém případně přejít na systém, který popisuješ.
(Komponenta se skládá z Component.js, Component.spec.js a Component.less (když potřebuje mít styly))
Lokalizaci jsme řešili a určitě se objeví v nějakém dalším dílu více popsaná :) V zásadě jsme zvolili jednoduché řešení na způsob mapStateToProps (zhruba takhle https://gist.github.com/ArcanisCz/04a6dce6a2ad66382dedcffc73edde80)
23.8.2016 at 07:04
Ty uvozovky v posledním odstavci byli tag H1 (komentářové pole má asi trošku přísný sanitizing vstupu)