Poruszanie się po ekosystemie React.js
Opublikowany: 2022-03-11Szybkość innowacji w JavaScript Land jest tak duża, że niektórzy ludzie myślą, że jest to nieproduktywne. W ciągu kilku miesięcy biblioteka może przejść od wczesnej zabawki, poprzez nowoczesną sztukę, do przestarzałości. Umiejętność zidentyfikowania narzędzia, które pozostanie aktualne przez co najmniej kolejny rok, sama w sobie staje się sztuką.
Kiedy React.js został wydany dwa lata temu, dopiero uczyłem się Angulara i szybko odrzuciłem Reacta jako jakąś niejasną, ale kolejną bibliotekę szablonów. W ciągu tych dwóch lat Angular naprawdę zyskał przyczółek wśród programistów JavaScript i stał się niemal synonimem współczesnego rozwoju JS. Zacząłem nawet widzieć Angulara w bardzo konserwatywnych środowiskach korporacyjnych i uznałem, że jego świetlana przyszłość jest oczywista.
Ale nagle wydarzyła się dziwna rzecz. Wygląda na to, że Angular stał się ofiarą efektu Osborne'a, czyli „śmierci przez zapowiedź”. Zespół ogłosił, że po pierwsze Angular 2 będzie zupełnie inny, bez wyraźnej ścieżki migracji z Angular 1, a po drugie, że Angular 2 nie będzie dostępny przez następny rok. Co to mówi komuś, kto chce rozpocząć nowy projekt internetowy? Czy chcesz napisać swój nowy projekt we frameworku, który będzie przestarzały wraz z wydaniem nowej wersji?
Ten niepokój wśród deweloperów trafił w ręce Reacta, który starał się zaistnieć w społeczności. Ale React zawsze reklamował się jako „V” w „MVC”. Wywołało to pewną frustrację wśród twórców stron internetowych, którzy są przyzwyczajeni do pracy z kompletnymi frameworkami. Jak uzupełnić brakujące elementy? Czy powinienem napisać własną? Czy powinienem po prostu użyć istniejącej biblioteki? Jeśli tak, to jaki?
Rzeczywiście, Facebook (twórcy React.js) miał jeszcze jednego asa w dziurze: przepływ pracy Flux, który obiecał wypełnić brakujące funkcje „M” i „C”. Aby było jeszcze ciekawiej, Facebook stwierdził, że Flux jest „wzorcem”, a nie frameworkiem, a ich implementacja Fluxa jest tylko jednym z przykładów wzorca. Zgodnie z ich słowem, ich implementacja była naprawdę uproszczona i wymagała napisania wielu wyczerpujących, powtarzalnych szablonów, aby wszystko działało.
Na ratunek przyszła społeczność open source, a rok później mamy dziesiątki bibliotek Fluxa, a nawet kilka metaprojektów, których celem jest ich porównywanie. To coś dobrego; Facebook zamiast wypuścić jakieś gotowe ramy korporacyjne, zdołał wzbudzić zainteresowanie społeczności i zachęcić ludzi do wymyślania własnych rozwiązań.
Jest jeden interesujący efekt uboczny takiego podejścia: kiedy musisz połączyć wiele różnych bibliotek, aby uzyskać kompletny framework, skutecznie unikasz uzależnienia od dostawcy, a innowacje, które pojawiają się podczas tworzenia własnego frameworka, można łatwo ponownie wykorzystać gdzie indziej.
Właśnie dlatego nowe rzeczy wokół Reacta są tak interesujące; większość z nich można łatwo ponownie wykorzystać w innych środowiskach JavaScript. Nawet jeśli nie planujesz używać Reacta, spojrzenie na jego ekosystem jest inspirujące. Możesz uprościć swój system kompilacji za pomocą potężnego, ale stosunkowo łatwego w konfiguracji pakietu WebPack z modułami, lub zacząć pisać ECMAScript 6, a nawet ECMAScript 7 już dziś z kompilatorem Babel.
W tym artykule omówię niektóre z dostępnych interesujących funkcji i bibliotek. Przyjrzyjmy się więc ekosystemowi React!
Budowanie systemu
System kompilacji jest prawdopodobnie pierwszą rzeczą, na którą powinieneś zwrócić uwagę podczas tworzenia nowej aplikacji internetowej. System budowania jest nie tylko narzędziem do uruchamiania skryptów, ale w świecie JavaScript zazwyczaj kształtuje ogólną strukturę Twojej aplikacji. Najważniejsze zadania, które musi wykonać system kompilacji, to:
- Zarządzanie zależnościami zewnętrznymi i wewnętrznymi
- Uruchamianie kompilatorów i preprocesorów
- Optymalizacja zasobów do produkcji
- Uruchamianie programistycznego serwera WWW, obserwatora plików i reloadera przeglądarki
W ostatnich latach przepływ pracy Yeoman z Bowerem i Gruntem był przedstawiany jako święta trójca współczesnego programowania frontendowego, rozwiązująca problemy odpowiednio generowania schematów, zarządzania pakietami i wykonywania wspólnych zadań, przy czym bardziej progresywni ludzie przeszli ostatnio z Grunt na Gulp.
W środowisku React możesz spokojnie o nich zapomnieć. Nie, że nie możesz ich użyć, ale są szanse, że po prostu ujdzie ci to na sucho używając Webpack i starego dobrego NPM. Jak to możliwe? Webpack to pakiet modułów, który implementuje składnię modułu CommonJS, powszechną w świecie Node.js, również w przeglądarce. To faktycznie upraszcza sprawę, ponieważ nie musisz uczyć się jeszcze jednego menedżera pakietów dla interfejsu; po prostu używasz NPM i dzielisz zależności między serwerem a frontendem. Nie musisz też zajmować się problemem ładowania plików JS we właściwej kolejności, ponieważ jest to wywnioskowane z importów zależności określonych w każdym pliku, a cały rój jest poprawnie połączony z jednym ładowalnym skryptem.
Aby było jeszcze bardziej atrakcyjnie, Webpack, w przeciwieństwie do swojego starszego kuzyna Browserify, może obsługiwać również inne typy zasobów. Na przykład za pomocą programów ładujących można przekształcić dowolny plik zasobów w funkcję JavaScript, która wstawia lub ładuje plik odniesienia. Zapomnij więc o ręcznym wstępnym przetwarzaniu i odwoływaniu się do zasobów z HTML. Wystarczy require
plików CSS/SASS/LESS z JavaScript, a Webpack zajmie się resztą za pomocą prostego pliku konfiguracyjnego. Webpack zawiera również programistyczny serwer WWW i obserwator plików. Dodatkowo możesz użyć klucza "scripts"
w package.json
do zdefiniowania pojedynczych wierszy powłoki:
{ "name": "react-example-filmdb", "version": "0.0.1", "description": "Isomorphic React + Flux film database example", "main": "server/index.js", "scripts": { "build": "./node_modules/.bin/webpack --progress --stats --config ./webpack/prod.config.js", "dev": "node --harmony ./webpack/dev-server.js", "prod": "NODE_ENV=production node server/index.js", "test": "./node_modules/.bin/karma start --single-run", "postinstall": "npm run build" } ... }
I to wszystko, czego potrzebujesz, aby zastąpić Gulpa i Bowera. Oczywiście nadal możesz używać Yeoman do generowania szablonów aplikacji. Nie zniechęcaj się, gdy nie ma generatora Yeoman dla rzeczy, które chcesz (najnowocześniejsze biblioteki często go nie mają). Nadal możesz po prostu sklonować jakiś szablon z GitHub i zhakować.
ECMAScript of Tomorrow, Today
Tempo rozwoju języka JavaScript znacznie wzrosło w ostatnich latach, a po okresie usuwania dziwactw i stabilizowania języka, teraz widzimy nadchodzące nowe, potężne funkcje. Projekt specyfikacji ECMAScript 6 (ES6) został sfinalizowany i mimo tego nie została jeszcze oficjalnie ogłoszona, już znajduje szerokie zastosowanie. Prace nad ECMAScript 7 (ES7) są w toku, ale wiele jego funkcji zostało już przyjętych przez nowsze biblioteki.
Jak to jest możliwe? Być może uważasz, że nie możesz skorzystać z tych nowych, błyszczących funkcji JavaScript, dopóki nie będą one obsługiwane w Internet Explorerze, ale zastanów się jeszcze raz. Transpilatory ES stały się już tak wszechobecne, że możemy obejść się nawet bez odpowiedniej obsługi przeglądarek. Najlepszym transpilerem ES dostępnym obecnie jest Babel: pobierze on twój najnowszy kod ES6+ i przekształci go w waniliowy ES5, więc możesz użyć dowolnej nowej funkcji ES, gdy tylko zostanie wynaleziona (i zaimplementowana w Babel, co zwykle zdarza się dość szybko).
Najnowsze funkcje JavaScript są przydatne we wszystkich frameworkach frontendowych, a React został niedawno zaktualizowany, aby dobrze współpracował ze specyfikacjami ES6 i ES7. Te nowe funkcje powinny wyeliminować wiele bólów głowy podczas programowania w React. Rzućmy okiem na niektóre z najbardziej przydatnych dodatków i jak mogą one przynieść korzyści projektowi React. Później zobaczymy, jak korzystać z przydatnych narzędzi i bibliotek w React, wykorzystując tę ulepszoną składnię.
Klasy ES6
Programowanie obiektowe jest potężnym i powszechnie stosowanym paradygmatem, ale podejście JavaScript jest nieco egzotyczne. Większość frameworków frontendowych, czy to Backbone, Ember, Angular czy React, przyjęło w ten sposób własne zastrzeżone sposoby definiowania klas i tworzenia obiektów. Ale w ES6 mamy teraz tradycyjne klasy w JavaScript i po prostu sensowne jest ich użycie zamiast pisania własnej implementacji. Więc zamiast:
React.createClass({ displayName: 'HelloMessage', render() { return <div>Hello {this.props.name}</div>; } })
możemy pisać:
class HelloMessage extends React.Component { render() { return <div>Hello {this.props.name}</div>; } }
Aby uzyskać bardziej rozbudowany przykład, rozważ ten kod, używając starej składni:
React.createClass({ displayName: 'Counter', getDefaultProps: function(){ return {initialCount: 0}; }, getInitialState: function() { return {count: this.props.initialCount} }, propTypes: {initialCount: React.PropTypes.number}, tick() { this.setState({count: this.state.count + 1}); }, render() { return ( <div onClick={this.tick}> Clicks: {this.state.count} </div> ); } });
I porównaj do wersji ES6:
class Counter extends React.Component { static propTypes = {initialCount: React.PropTypes.number}; static defaultProps = {initialCount: 0}; constructor(props) { super(props); this.state = {count: props.initialCount}; } state = {count: this.props.initialCount}; tick() { this.setState({count: this.state.count + 1}); } render() { return ( <div onClick={this.tick.bind(this)}> Clicks: {this.state.count} </div> ); } }
W tym przypadku metody cyklu życia React getDefaultProps
i getInitialState
nie są już potrzebne. getDefaultProps
staje się statyczną zmienną klasy defaultProps
, a stan początkowy jest właśnie zdefiniowany w konstruktorze. Jedyną wadą jest to, że metody nie są już automatycznie wiązane, więc musisz używać bind
podczas wywoływania programów obsługi z JSX.
Dekoratorzy
Dekoratory są użyteczną funkcją ES7. Pozwalają na rozszerzenie zachowania funkcji lub klasy poprzez zawinięcie jej w inną funkcję. Załóżmy na przykład, że chcesz mieć tę samą procedurę obsługi zmian w niektórych składnikach, ale nie chcesz zatwierdzać antywzorca dziedziczenia. Zamiast tego możesz użyć dekoratora klas. Zdefiniujmy dekoratora w następujący sposób:
addChangeHandler: function(target) { target.prototype.changeHandler = function(key, attr, event) { var state = {}; state[key] = this.state[key] || {}; state[key][attr] = event.currentTarget.value; this.setState(state); }; return target; }
Ważną rzeczą tutaj jest to, że funkcja addChangeHandler
dodaje funkcję changeHandler
do prototypu klasy docelowej.
Aby zastosować dekorator, moglibyśmy napisać:
MyClass = changeHandler(MyClass)
lub bardziej elegancko, ze składnią ES7:
@addChangeHandler class MyClass { ... }
Jeśli chodzi o zawartość samej funkcji changeHandler
, przy braku dwukierunkowego wiązania danych w React, praca z danymi wejściowymi w React może być żmudna. Funkcja changeHandler
stara się to ułatwić. Pierwszy parametr określa key
w obiekcie stanu, który będzie służył jako obiekt danych dla wejścia. Drugim parametrem jest atrybut, do którego zostanie zapisana wartość z pola wejściowego. Te dwa parametry są ustawiane z JSX za pomocą słowa kluczowego bind
.
@addChangeHandler class LoginInput extends React.Component { constructor(props) { super(props); this.state = { login: {} }; } render() { return ( <input type='text' value={this.state.login.username} onChange={this.changeHandler.bind(this, 'login', 'username')} /> <input type='password' value={this.state.login.password} onChange={this.changeHandler.bind(this, 'login', 'password')} /> ) } }
Gdy użytkownik zmieni pole username, jego wartość jest zapisywana do this.state.login.username
, bez potrzeby definiowania kolejnych niestandardowych handlerów.
Funkcje strzałek
Dynamiczny this
kontekst JavaScript był ciągłym utrapieniem dla programistów, ponieważ, nieco nieintuicyjnie, this
kontekst funkcji zagnieżdżonej jest resetowany do globalnego, nawet wewnątrz klasy. Aby to naprawić, zwykle konieczne jest zapisanie this
w jakiejś zmiennej zewnętrznej (zwykle _this
) i użycie jej w funkcjach wewnętrznych:
class DirectorsStore { onFetch(directors) { var _this = this; this.directorsHash = {}; directors.forEach(function(x){ _this.directorsHash[x._id] = x; }) } }
Dzięki nowej składni ES6 function(x){
może zostać przepisana jako (x) => {
. Ta definicja metody „strzałki” nie tylko poprawnie wiąże this
z zewnętrznym zakresem, ale jest również znacznie krótsza, co zdecydowanie liczy się przy pisaniu dużej ilości kodu asynchronicznego.
onFetch(directors) { this.directorsHash = {}; directors.forEach((x) => { this.directorsHash[x._id] = x; }) }
Zadania destrukturyzacyjne
Przypisania destrukturyzujące, wprowadzone w ES6, umożliwiają umieszczenie obiektu złożonego po lewej stronie przypisania:
var o = {p: 42, q: true}; var {p, q} = o; console.log(p); // 42 console.log(q); // true
To fajnie, ale jak właściwie pomaga nam w React? Rozważmy następujący przykład:
function makeRequest(url, method, params) { var config = { url: url, method: method, params: params }; ... }
Dzięki destrukturyzacji możesz zaoszczędzić kilka naciśnięć klawiszy. Literał kluczy {url, method, params}
jest automatycznie przypisywany wartościom z zakresu o takich samych nazwach jak klucze. Ten idiom jest używany dość często, a wyeliminowanie powtórzeń sprawia, że kod jest mniej podatny na błędy.
function makeRequest(url, method, params) { var config = {url, method, params}; ... }
Destrukturyzacja może również pomóc w załadowaniu tylko podzbioru modułu:
const {clone, assign} = require('lodash'); function output(data, optional) { var payload = clone(data); assign(payload, optional); }
Argumenty: domyślne, reszta i spread
Argumenty funkcji mają większą moc w ES6. Na koniec możesz ustawić domyślny argument:
function http(endpoint, method='GET') { console.log(method) ... } http('/api') // GET
Masz dość kłopotania się z nieporęcznymi arguments
? W nowej specyfikacji możesz uzyskać resztę argumentów w postaci tablicy:
function networkAction(context, method, ...rest) { // rest is an array return method.apply(context, rest); }
A jeśli nie lubisz wywoływać funkcji apply()
, możesz po prostu rozłożyć tablicę na argumenty funkcji:
myArguments = ['foo', 'bar', 123]; myFunction(...myArguments);
Generatory i funkcje asynchroniczne
ES6 wprowadził generatory JavaScript. Generator to w zasadzie funkcja JavaScript, której wykonanie można wstrzymać, a następnie wznowić później, pamiętając jej stan. Za każdym razem, gdy napotkane zostanie słowo kluczowe yield
, wykonanie jest wstrzymywane, a argument yield
jest przekazywany z powrotem do obiektu wywołującego:
function* sequence(from, to) { console.log('Ready!'); while(from <= to) { yield from++; } }
Oto przykład działania tego generatora:
> var cursor = sequence(1,3) Ready! > cursor.next() { value: 1, done: false } > cursor.next() { value: 2, done: false } > cursor.next() { value: 3, done: false } > cursor.next() { value: undefined, done: true }
Kiedy wywołujemy funkcję generatora, wykonuje się ona do pierwszego yield
, a następnie zatrzymuje się. Po wywołaniu next()
zwraca pierwszą wartość i wznawia wykonywanie. Każdy yield
zwraca inną wartość, ale po trzecim wywołaniu funkcja generatora kończy działanie, a każde kolejne wywołanie next()
zwróci { value: undefined, done: true }
.
Oczywiście celem generatorów nie jest tworzenie skomplikowanych ciągów liczbowych. Ekscytującą częścią jest ich zdolność do zatrzymywania i wznawiania wykonywania funkcji, która może być wykorzystana do sterowania asynchronicznym przepływem programu i ostatecznie do pozbycia się tych nieznośnych funkcji zwrotnych.
Aby zademonstrować ten pomysł, najpierw potrzebujemy funkcji asynchronicznej. Zwykle mielibyśmy jakąś operację we/wy, ale dla uproszczenia użyjmy po prostu setTimeout
i zwróćmy obietnicę. (Zauważ, że ES6 wprowadził również natywne obietnice do JavaScript.)
function asyncDouble(x) { var deferred = Promise.defer(); setTimeout(function(){ deferred.resolve(x*2); }, 1000); return deferred.promise; }
Następnie potrzebujemy funkcji konsumenta:
function consumer(generator){ var cursor = generator(); var value; function loop() { var data = cursor.next(value); if (data.done) { return; } else { data.value.then(x => { value = x; loop(); }) } } loop(); }
Ta funkcja przyjmuje dowolny generator jako argument i wywołuje na nim next()
, dopóki istnieją wartości do yield
. W tym przypadku otrzymane wartości są obietnicami, a więc konieczne jest poczekanie na rozwiązanie obietnic i użycie rekursji z loop()
, aby uzyskać pętlę między zagnieżdżonymi funkcjami.
Zwracana wartość jest rozwiązywana w procedurze obsługi then()
i przekazywana do value
, która jest zdefiniowana w zewnętrznym zakresie i która zostanie przekazana do wywołania next(value)
. To wywołanie powoduje, że wartość jest wynikiem odpowiedniego wyrażenia wydajności. Oznacza to, że możemy teraz pisać asynchronicznie bez żadnych wywołań zwrotnych:
function* myGenerator(){ const data1 = yield asyncDouble(1); console.log(`Double 1 = ${data1}`); const data2 = yield asyncDouble(2); console.log(`Double 2 = ${data2}`); const data3 = yield asyncDouble(3); console.log(`Double 3 = ${data3}`); } consumer(myGenerator);
Generator myGenerator
zostanie wstrzymany przy każdym yield
, czekając na dostarczenie przez konsumenta rozwiązanej obietnicy. I rzeczywiście, obliczone liczby pojawią się w konsoli w jednosekundowych odstępach.
Double 1 = 2 Double 2 = 4 Double 3 = 6
To pokazuje podstawową koncepcję, jednak nie zalecam używania tego kodu w środowisku produkcyjnym. Zamiast tego wybierz dobrze przetestowaną bibliotekę, taką jak co. Umożliwi to łatwe pisanie kodu asynchronicznego z wydajnością, w tym obsługą błędów:
co(function *(){ var a = yield Promise.resolve(1); console.log(a); var b = yield Promise.resolve(2); console.log(b); var c = yield Promise.resolve(3); console.log(c); }).catch(function(err){ console.error(err.stack); });
Tak więc ten przykład pokazuje, jak pisać kod asynchroniczny bez wywołań zwrotnych przy użyciu generatorów ES6. ES7 idzie o krok dalej w tym podejściu, wprowadzając słowa kluczowe async
i await
oraz całkowicie eliminując potrzebę posiadania biblioteki generatora. Dzięki tej możliwości poprzedni przykład wyglądałby tak:
async function (){ try { var a = await Promise.resolve(1); console.log(a); var b = await Promise.resolve(2); console.log(b); var c = await Promise.resolve(3); console.log(c); } catch (err) { console.error(err.stack); } };
Moim zdaniem eliminuje to ból związany z pracą z kodem asynchronicznym w JavaScript. Nie tylko w React, ale i wszędzie indziej.
Generatory są nie tylko bardziej zwięzłe i proste, ale także pozwalają nam korzystać z technik, które byłyby bardzo trudne do zaimplementowania za pomocą wywołań zwrotnych. Wybitnym przykładem dobroci generatora jest biblioteka oprogramowania pośredniego koa dla Node.js. Ma na celu zastąpienie Express, a w tym celu ma jedną zabójczą cechę: łańcuch oprogramowania pośredniczącego przepływa nie tylko w dół (z żądaniem klienta), ale także w górę , umożliwiając dalszą modyfikację odpowiedzi serwera. Rozważmy następujący przykład serwera koa:
// Response time logger middleware app.use(function *(next){ // Downstream var start = new Date; yield next; // Upstream this.body += ' World'; var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); }); // Response handler app.use(function *(){ this.body = 'Hello'; }); app.listen(3000);

yield
oprogramowania pośredniego odpowiedzi jest przesyłana w dół do modułu obsługi odpowiedzi, który ustawia treść odpowiedzi, a w przepływie poprzedzającym (po wyrażeniu yield
) dozwolona jest dalsza modyfikacja this.body
, a także inne funkcje, takie jak rejestrowanie czasu, co jest możliwe ponieważ upstream i downstream mają ten sam kontekst funkcji. Jest to znacznie potężniejsze niż Express, w którym próba osiągnięcia tego samego zakończyłaby się tak:
var start; // Downstream middleware app.use(function(req, res, next) { start = new Date; next(); // Already returned, cannot continue here }); // Response app.use(function (req, res, next){ res.send('Hello World') next(); }); // Upstream middleware app.use(function(req, res, next) { // res already sent, cannot modify var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); next(); }); app.listen(3000);
Prawdopodobnie już wiesz, co jest tutaj nie tak; użycie „globalnej” zmiennej start
spowoduje wyścig, zwracając nonsens z równoczesnymi żądaniami. Rozwiązaniem jest pewne nieoczywiste obejście i możesz zapomnieć o modyfikowaniu odpowiedzi w strumieniu upstream.
Ponadto, używając koa, otrzymasz asynchroniczny przepływ pracy generatora za darmo:
app.use(function *(){ try { const part1 = yield fs.readFile(this.request.query.file1, 'utf8'); const part2 = yield fs.readFile(this.request.query.file2, 'utf8'); this.body = part1 + part2; } catch (err) { this.status = 404 this.body = err; } }); app.listen(3000);
Możesz sobie wyobrazić obietnice i oddzwonienia związane z odtworzeniem tego małego przykładu w Express.
Jak cała ta rozmowa Node.js ma się do Reacta? Cóż, Node jest pierwszym wyborem przy rozważaniu odpowiedniego zaplecza dla Reacta. Ponieważ Node jest również napisany w JavaScript, obsługuje współdzielenie kodu między backendem i frontendem, co pozwala nam budować izomorficzne aplikacje internetowe React. Ale o tym później.
Biblioteka strumieni
React świetnie radzi sobie z tworzeniem komponentów widoków, które można komponować, ale potrzebujemy sposobu na zarządzanie danymi i stanami w całej aplikacji. Niemal powszechnie uznano, że React najlepiej uzupełnia architektura aplikacji Flux. Jeśli jesteś zupełnie nowy w Fluxie, polecam szybkie odświeżenie.
To, co nie zostało tak powszechnie uzgodnione, to to, którą z wielu implementacji Fluxa wybrać. Facebook Flux byłby oczywistym wyborem, ale dla większości ludzi jest zbyt gadatliwy. Alternatywne implementacje skupiają się głównie na zmniejszeniu ilości wymaganego wzorca dzięki podejściu opartemu na konwencji nad konfiguracją, a także na niektórych wygodnych funkcjach dla komponentów wyższego rzędu, renderowaniu po stronie serwera i tak dalej. Niektóre z czołowych konkurentów, z różnymi wskaźnikami popularności, można zobaczyć tutaj. Sprawdziłem Alt, Reflux, Flummox, Fluxxor i Marty.js.
Mój sposób wyboru odpowiedniej biblioteki nie jest w żaden sposób obiektywny, ale i tak może pomóc. Fluxxor był jedną z pierwszych bibliotek, które sprawdziłem, ale teraz wygląda trochę nieaktualnie. Marty.js jest interesujący i ma wiele funkcji, ale nadal zawiera dużo szablonów, a niektóre funkcje wydają się zbędne. Reflux wygląda świetnie i ma pewną przyczepność, ale jest trochę trudny dla początkujących, a także brakuje mu odpowiedniej dokumentacji. Flummox i Alt są bardzo podobne, ale Alt wydaje się mieć jeszcze mniej schematów, bardzo aktywny rozwój, aktualną dokumentację i pomocną społeczność Slacka. Dlatego wybrałem Alt.
Alt Flux
Dzięki Alt przepływ pracy Flux staje się znacznie prostszy, nie tracąc nic ze swojej mocy. Dokumentacja Flux Facebooka mówi wiele o dyspozytorze, ale możemy to zignorować, ponieważ w Alt dyspozytor jest niejawnie powiązany z akcjami zgodnie z konwencją i zwykle nie wymaga żadnego niestandardowego kodu. To pozostawia nam tylko magazyny , akcje i komponenty . Te trzy warstwy mogą być używane w taki sposób, aby ładnie odwzorowywały się w modelu myślowym MVC : Sklepy to Modele , akcje to Kontrolery , a komponenty to Widoki . Główną różnicą jest jednokierunkowy przepływ danych centralny dla wzorca Flux, co oznacza, że kontrolery (akcje) nie mogą bezpośrednio modyfikować widoków (komponentów), ale zamiast tego mogą jedynie wyzwalać modyfikacje modelu (magazynu), z którymi widoki są biernie powiązane. To była już najlepsza praktyka dla niektórych oświeconych programistów Angulara.
Przepływ pracy wygląda następująco:
- Komponenty inicjują akcje.
- Sklepy nasłuchują działań i aktualizują dane.
- Komponenty są powiązane ze sklepami i renderują się, gdy dane są aktualizowane.
działania
W przypadku korzystania z biblioteki Alt Flux, akcje zazwyczaj występują w dwóch odmianach: automatycznym i ręcznym. Akcje automatyczne tworzone są za pomocą funkcji generateActions
i trafiają bezpośrednio do dyspozytora. Metody ręczne są zdefiniowane jako metody klas akcji i mogą przejść do dyspozytora z dodatkowym ładunkiem. Najczęstszym przypadkiem użycia automatycznych akcji jest powiadomienie sklepów o jakimś zdarzeniu w aplikacji. Ręczne działania to między innymi preferowany sposób radzenia sobie z interakcjami z serwerem.
Tak więc wywołania REST API należą do akcji. Cały przepływ pracy wygląda następująco:
- Komponent wyzwala akcję.
- Twórca akcji uruchamia asynchroniczne żądanie serwera, a wynik trafia do dyspozytora jako ładunek.
- Sklep nasłuchuje akcji, odpowiedni program obsługi akcji otrzymuje wynik jako argument, a sklep odpowiednio aktualizuje swój stan.
W przypadku żądań AJAX możemy skorzystać z biblioteki axios, która między innymi bezproblemowo radzi sobie z danymi i nagłówkami JSON. Zamiast obietnic lub wywołań zwrotnych możemy użyć wzorca ES7 async
/ await
. Jeśli status odpowiedzi POST
jest inny niż 2XX, zgłaszany jest błąd i wysyłamy albo zwrócone dane, albo otrzymany błąd.
Spójrzmy na stronę logowania dla prostego przykładu przepływu pracy Alt. Akcja wylogowania nie wymaga niczego specjalnego, wystarczy powiadomić sklep, abyśmy mogli ją wygenerować automatycznie. Akcja logowania jest ręczna i oczekuje danych logowania jako parametru do twórcy akcji. Po otrzymaniu odpowiedzi z serwera albo wysyłamy dane o powodzeniu, albo, jeśli zostanie zgłoszony błąd, wysyłamy otrzymany błąd.
class LoginActions { constructor() { // Automatic action this.generateActions('logout'); } // Manual action async login(data) { try { const response = await axios.post('/auth/login', data); this.dispatch({ok: true, user: response.data}); } catch (err) { console.error(err); this.dispatch({ok: false, error: err.data}); } } } module.exports = (alt.createActions(LoginActions));
Sklepy
Sklep Flux służy dwóm celom: ma programy obsługi akcji i przenosi stan. Kontynuujmy nasz przykład strony logowania, aby zobaczyć, jak to działa.
LoginStore
, z dwoma atrybutami stanu: user
, dla aktualnie zalogowanego użytkownika i error
, dla bieżącego błędu związanego z logowaniem. W duchu ograniczenia schematu Alt pozwala nam powiązać wszystkie akcje z jednej klasy za pomocą jednej funkcji bindActions
.
class LoginStore { constructor() { this.bindActions(LoginActions); this.user = null; this.error = null; } ...
Nazwy programów obsługi są zdefiniowane zgodnie z konwencją, poprzedzając on
nazwę akcji. Tak więc akcja login
jest obsługiwana przez onLogin
i tak dalej. Zauważ, że pierwsza litera nazwy akcji będzie pisana wielką literą w stylu camelCase. W naszym LoginStore
mamy następujące handlery, wywoływane przez odpowiednie akcje:
... onLogin(data) { if (data.ok) { this.user = data.user; this.error = null; router.transitionTo('home'); } else { this.user = null; this.error = data.error } } onLogout() { this.user = null; this.error = null; } }
składniki
Zwykłym sposobem wiązania sklepów z komponentami jest użycie pewnego rodzaju mieszanki React. Ale ponieważ mixiny wychodzą z mody, musi być jakiś inny sposób. Jednym z nowych podejść jest wykorzystanie komponentów wyższego rzędu. Bierzemy nasz komponent i umieszczamy go w opakowaniu komponentu, który zajmie się odsłuchiwaniem sklepów i wywołaniem ponownego renderowania. Nasz komponent otrzyma stan sklepu w props
. Takie podejście jest również pomocne w uporządkowaniu naszego kodu w inteligentne i głupie komponenty, które stały się ostatnio modne. W przypadku Alt opakowanie komponentów jest implementowane przez AltContainer
:
export default class Login extends React.Component { render() { return ( <AltContainer stores={{LoginStore: LoginStore}}> <LoginPage/> </AltContainer> )} }
Nasz komponent LoginPage
korzysta również z wprowadzonego wcześniej dekoratora changeHandler
. Dane z LoginStore
służą do wyświetlania błędów w przypadku nieudanego logowania, a ponownym renderowaniem zajmuje się AltContainer
. Kliknięcie przycisku logowania powoduje wykonanie akcji login
, kończąc przepływ pracy Alt flux:
@changeHandler export default class LoginPage extends React.Component { constructor(props) { super(props); this.state = { loginForm: {} }; } login() { LoginActions.login(this.state.loginForm) } render() { return ( <Alert>{{this.props.LoginStore.error}}</Alert> <Input label='Username' type='text' value={this.state.login.username} onChange={this.changeHandler.bind(this, 'loginForm', 'username')} /> <Input label='Password' type='password' value={this.state.login.password} onChange={this.changeHandler.bind(this, 'loginForm', 'password')} /> <Button onClick={this.login.bind(this)}>Login</Button> )} }
Renderowanie izomorficzne
Izomorficzne aplikacje internetowe są obecnie gorącym tematem, ponieważ rozwiązują niektóre z największych zadań tradycyjnych aplikacji jednostronicowych. W tych aplikacjach znaczniki są tworzone dynamicznie przez JavaScript w przeglądarce. W rezultacie treść nie jest dostępna dla klientów z wyłączonym JavaScript, w szczególności dla robotów indeksujących wyszukiwarki. Oznacza to, że Twoja strona internetowa nie jest indeksowana i nie pojawia się w wynikach wyszukiwania. Istnieją sposoby na obejście tego, ale są one dalekie od optymalnych. Podejście izomorficzne próbuje rozwiązać ten problem, wstępnie renderując żądany adres URL aplikacji jednostronicowej na serwerze. Dzięki Node.js masz JavaScript na serwerze, co oznacza, że React może również działać po stronie serwera. To nie powinno być zbyt trudne, prawda?
Jedną z przeszkód jest to, że niektóre biblioteki Fluxa, szczególnie te, które używają singletonów, mają problemy z renderowaniem po stronie serwera. Gdy masz pojedyncze sklepy Flux i wiele jednoczesnych żądań do serwera, dane zostaną pomieszane. Niektóre biblioteki rozwiązują ten problem, używając instancji Flux, ale ma to inne wady, w szczególności konieczność przekazywania tych instancji w kodzie. Alt oferuje również instancje Flux, ale rozwiązał również problem renderowania po stronie serwera za pomocą singletonów; opróżnia magazyny po każdym żądaniu, dzięki czemu każde współbieżne żądanie zaczyna się z czystym kontem.
Rdzeń funkcji renderowania po stronie serwera zapewnia React.renderToString
. Na serwerze działa również cała aplikacja front-endowa Reacta. W ten sposób nie musimy czekać, aż JavaScript po stronie klienta utworzy znacznik; jest on wstępnie zbudowany na serwerze dla adresu URL, do którego uzyskuje się dostęp, i wysyłany do przeglądarki jako HTML. Kiedy klient JavaScript jest uruchamiany, wraca do miejsca, w którym serwer został przerwany. Aby to wesprzeć, możemy użyć biblioteki Iso, która ma być używana z Alt.
Najpierw inicjujemy Flux na serwerze za pomocą alt.bootstrap
. Możliwe jest wstępne wypełnienie magazynów Flux danymi do renderowania. Niezbędne jest również podjęcie decyzji, który komponent renderować dla którego adresu URL, co jest funkcjonalnością Router
po stronie klienta. Używamy singletonowej wersji alt
, więc po każdym renderowaniu musimy alt.flush()
sklepy, aby były czyste dla kolejnego żądania. Za pomocą dodatku iso
stan Fluxa jest serializowany do znacznika HTML, dzięki czemu klient wie, gdzie pobrać:
// We use react-router to run the URL that is provided in routes.jsx var getHandler = function(routes, url) { var deferred = Promise.defer(); Router.run(routes, url, function (Handler) { deferred.resolve(Handler); }); return deferred.promise; }; app.use(function *(next) { yield next; // We seed our stores with data alt.bootstrap(JSON.stringify(this.locals.data || {})); var iso = new Iso(); const handler = yield getHandler(reactRoutes, this.request.url); const node = React.renderToString(React.createElement(handler)); iso.add(node, alt.flush()); this.render('layout', {html: iso.render()}); });
Po stronie klienta pobieramy stan serwera i uruchamiamy alt
z danymi. Następnie uruchamiamy Router
i React.render
na docelowym kontenerze, który w razie potrzeby zaktualizuje znaczniki generowane przez serwer.
Iso.bootstrap(function (state, _, container) { // Bootstrap the state from the server alt.bootstrap(state) Router.run(routes, Router.HistoryLocation, function (Handler, req) { let node = React.createElement(Handler) React.render(node, container) }) })
Śliczny!
Przydatne biblioteki front-end
Przewodnik po ekosystemie React nie byłby kompletny bez wymienienia kilku bibliotek front-end, które szczególnie dobrze współpracują z Reactem. Te biblioteki obsługują najczęstsze zadania występujące w prawie każdej aplikacji internetowej: układy i kontenery CSS, stylizowane formularze i przyciski, walidacje, wybieranie dat i tak dalej. Nie ma sensu wymyślać koła na nowo, skoro te problemy zostały już rozwiązane.
React-Bootstrap
Twitter's Bootstrap framework has become commonplace since it is of immense help to every web developer who does not want to spend a ton of time working in CSS. In the prototyping phase especially, Bootstrap is indispensable. To leverage the power of bootstrap in a React application, it's good to use it with React-Bootstrap, which comes with nice React syntax and re-implements Bootstrap's jQuery plugins using native React components. The resulting code is terse and easy to understand:
<Navbar brand='React-Bootstrap'> <Nav> <NavItem eventKey={1} href='#'>Link</NavItem> <NavItem eventKey={2} href='#'>Link</NavItem> <DropdownButton eventKey={3} title='Dropdown'> <MenuItem eventKey='1'>Action</MenuItem> <MenuItem eventKey='2'>Another action</MenuItem> <MenuItem eventKey='3'>Something else here</MenuItem> <MenuItem divider /> <MenuItem eventKey='4'>Separated link</MenuItem> </DropdownButton> </Nav> </Navbar>
Personally, I cannot escape the feeling that this is what HTML should always have been like.
If you want to use Bootstrap source with Webpack, consider using less-loader or bootstrap-sass-loader, depending on the preprocessor you prefer. It will allow you to easily customize which Bootstrap components to include, and also allow the usage of LESS or SASS global variables in your CSS code.
React Router
React Router has become the de-facto standard for routing in React. It allows nested routing, support for redirections, plays nicely with isomorphic rendering, and has a simple JSX-based syntax:
<Router history={new BrowserHistory}> <Route path="/" component={App}> <Route path="about" name="about" component={About}/> <Route path="users" name="users" component={Users} indexComponent={RecentUsers}> <Route path="/user/:userId" name="user" component={User}/> </Route> <Route path="*" component={NoMatch}/> </Route> </Router>
React Router also provides a Link
component that you can use for navigation in your application, specifying only the route name:
<nav> <Link to="about">About</Link> <Link to="users">Users</Link> </nav>
There is even a library for integration with React-Bootstrap, so if you are using Bootstrap's components and don't feel like setting the active
class on them manually all the time, you can use react-router-bootstrap and write code like this:
<Nav> <NavItemLink to="about">About</NavItemLink> <NavItemLink to="users">Users</NavItemLink> </Nav>
No additional setup is necessary. Active links will take care of themselves.
Formsy-React
Working with forms can be tedious, so let's get some help from the formsy-react library, which will help us manage validations and data models. The Formsy-React library, strangely enough, does not include the actual form inputs because users are encouraged to write their own (which is great). But if you are content with the common ones, just use formsy-react-components. Bootstrap classes are included:
import Formsy from 'formsy-react'; import {Input} from 'formsy-react-components'; export default class FormsyForm extends React.Component { enableButton() { this.setState({canSubmit: true}); } disableButton() { this.setState({canSubmit: true}); } submit(model) { FormActions.saveEmail(model.email); } render() { return ( <Formsy.Form onValidSubmit={this.submit} onValid={this.enableButton} onInvalid={this.disableButton}> <Input name="email" validations="isEmail" validationError="This is not a valid email" required/> <button type="submit" disabled={!this.state.canSubmit}>Submit</button> </Formsy.Form> )} }
Calendar and Typeahead
Calendar and typeahead are icing on the cake of every UI toolkit. Sadly, these two components were removed from Bootstrap 3, probably because they are too specialized for a general purpose CSS framework. Luckily, I've been able to find worthy replacements in react-pikaday and react-select. I've tested more than 10 libraries, and these two came out as the best. They are dead easy to use, as well:
import Pikaday from 'react-pikaday'; import Select from 'react-select'; export default class CalendarAndTypeahead extends React.Component { constructor(props){ super(props); this.options = [ { value: 'one', label: 'One' }, { value: 'two', label: 'Two' } ]; } dateChange(date) { this.setState({date: date}); }, selectChange(selected) { this.setState({selected: selected}); }, render() { return ( <Pikaday value={this.state.date} onChange={this.dateChange} /> <Select name="form-field-name" value={this.state.selected} options={this.options} onChange={selectChange} /> )} }
Conclusion - React.JS
In this article I've presented libraries and techniques that I consider some of the most productive in current web development. Some of them are React-specific, but due to React's open nature, many of them are usable in other environments as well. Technological progress is sometimes hindered by fear of the newest stuff, so I hope this article will help to dissipate doubts concerning React, Flux and the newest features in ECMAScript.
If you are interested, you can take a look at my example application built with these technologies. The source code is available on GitHub.
Dziękuje za przeczytanie!