Navigarea în ecosistemul React.js
Publicat: 2022-03-11Viteza de inovare în JavaScript Land este atât de mare, încât unii oameni chiar cred că este contraproductivă. O bibliotecă poate trece de la o jucărie pentru adoptarea timpurie la cea de ultimă generație, la uzură în decurs de câteva luni. A fi capabil să identifici un instrument care va rămâne relevant pentru cel puțin încă un an devine o artă în sine.
Când React.js a fost lansat în urmă cu doi ani, tocmai învățam Angular și l-am respins rapid pe React ca fiind o bibliotecă obscură și încă o altă bibliotecă cu șabloane. În acești doi ani, Angular și-a câștigat cu adevărat un loc în rândul dezvoltatorilor JavaScript și aproape a devenit sinonim cu dezvoltarea modernă JS. Am început să văd Angular chiar și în medii corporative foarte conservatoare și am luat de la sine înțeles viitorul său strălucit.
Dar brusc, s-a întâmplat un lucru ciudat. Se pare că Angular a devenit o victimă a efectului Osborne, sau „moarte prin pre-anunț”. Echipa a anunțat că, în primul rând, Angular 2 va fi complet diferit, fără o cale clară de migrare de la Angular 1 și, în al doilea rând, că Angular 2 nu va fi disponibil pentru încă un an și ceva. Ce spune asta cuiva care vrea să înceapă un nou proiect web? Doriți să vă scrieți noul proiect într-un cadru care va deveni învechit de o nouă versiune?
Această anxietate în rândul dezvoltatorilor a jucat în mâinile lui React, care căuta să se impună în comunitate. Dar React s-a prezentat întotdeauna ca „V” în „MVC”. Acest lucru a provocat o anumită frustrare în rândul dezvoltatorilor web, care sunt obișnuiți să lucreze cu cadre complete. Cum completez piesele lipsă? Ar trebui să scriu pe al meu? Ar trebui să folosesc doar o bibliotecă existentă? Dacă da, care?
Destul de sigur, Facebook (creatorii lui React.js) a mai avut un as în gol: fluxul de lucru Flux, care promitea să completeze funcțiile „M” și „C” lipsă. Pentru a face lucrurile și mai interesante, Facebook a declarat că Flux este un „model”, nu un cadru, iar implementarea lor a Flux este doar un exemplu al modelului. Fidel cuvântului lor, implementarea lor a fost cu adevărat simplistă și a implicat scrierea multor planșe repetate, pentru a pune lucrurile în mișcare.
Comunitatea open source a venit în ajutor și, un an mai târziu, avem zeci de biblioteci Flux și chiar câteva meta-proiecte menite să le compare. Ăsta este un lucru bun; în loc să lanseze un cadru corporativ gata făcut, Facebook a reușit să trezească interesul în comunitate și a încurajat oamenii să vină cu propriile soluții.
Există un efect secundar interesant al acestei abordări: atunci când trebuie să combinați o mulțime de biblioteci diferite pentru a obține cadrul complet, scăpați efectiv de blocarea furnizorului, iar inovațiile care apar în timpul construcției propriului cadru pot fi reutilizate cu ușurință. în altă parte.
De aceea, lucrurile noi din jurul React sunt atât de interesante; cea mai mare parte poate fi reutilizată cu ușurință în alte medii JavaScript. Chiar dacă nu intenționați să utilizați React, aruncați o privire asupra ecosistemului său este inspirațional. Poate doriți să vă simplificați sistemul de construcție utilizând pachetul de module puternic, dar relativ ușor de configurat, Webpack sau începeți să scrieți ECMAScript 6 și chiar ECMAScript 7 astăzi cu compilatorul Babel.
În acest articol, voi trece prin câteva dintre funcțiile și bibliotecile interesante disponibile. Deci, haideți să explorăm ecosistemul React!
Build System
Sistemul de compilare este, fără îndoială, primul lucru de care ar trebui să vă pese atunci când creați o nouă aplicație web. Sistemul de compilare nu este doar un instrument pentru rularea scripturilor, dar în lumea JavaScript, de obicei modelează structura generală a aplicației dvs. Cele mai importante sarcini pe care trebuie să le acopere un sistem de construcție sunt următoarele:
- Gestionarea dependențelor externe și interne
- Rularea compilatoarelor și preprocesoarelor
- Optimizarea activelor pentru producție
- Rularea serverului web de dezvoltare, urmăritorul de fișiere și reîncărcarea browserului
În ultimii ani, fluxul de lucru Yeoman cu Bower și Grunt a fost prezentat ca sfânta trinitate a dezvoltării frontend moderne, rezolvând problemele de generare a standardelor, gestionarea pachetelor și, respectiv, rularea sarcinii comune, oamenii mai progresivi trecând recent de la Grunt la Gulp.
În mediul React, puteți uita în siguranță de acestea. Nu că nu le-ați putea folosi, dar sunt șanse să puteți scăpa folosind Webpack și vechiul NPM. Cum este posibil? Webpack este un bundler de module, care implementează sintaxa modulului CommonJS, comună în lumea Node.js, și în browser. De fapt, face lucrurile mai simple, deoarece nu trebuie să înveți încă un alt manager de pachete pentru front-end; utilizați doar NPM și partajați dependențe între server și front-end. De asemenea, nu trebuie să vă ocupați de problema încărcării fișierelor JS în ordinea corectă, deoarece este dedusă din importurile de dependență specificate în fiecare fișier, iar întregul roi este corect concatenat la un script încărcat.
Pentru a face lucrurile și mai atrăgătoare, Webpack, spre deosebire de vărul său mai vechi Browserify, poate gestiona și alte tipuri de active. De exemplu, cu încărcătoare, puteți transforma orice fișier de material într-o funcție JavaScript care fie aliniază, fie încarcă fișierul referit. Deci, uitați de preprocesarea manuală și referirea activelor din HTML. Cereți doar fișierele dvs. CSS/SASS/LESS din JavaScript, iar require
se ocupă de restul cu un fișier de configurare simplu. Webpack include, de asemenea, un server web de dezvoltare și un monitor de fișiere. În plus, puteți utiliza cheia "scripts"
din package.json
pentru a defini liniile de shell:
{ "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 cam atât ai nevoie pentru a înlocui Gulp și Bower. Desigur, puteți utiliza în continuare Yeoman pentru a genera aplicații boilerplate. Nu vă descurajați când nu există generator Yeoman pentru lucrurile pe care le doriți (cele mai de ultimă oră biblioteci de multe ori nu au unul). Încă puteți doar să clonați niște boilerplate din GitHub și să piratați.
ECMAScript de mâine, azi
Ritmul dezvoltării limbajului JavaScript a crescut substanțial în ultimii ani și, după o perioadă de înlăturare a neconformităților și de stabilizare a limbajului, acum vedem că apar funcții noi puternice. Proiectul de specificație ECMAScript 6 (ES6) a fost finalizat și chiar dacă nu a fost încă oficializat, acesta găsește deja o adoptare pe scară largă. Lucrările la ECMAScript 7 (ES7) sunt în curs de desfășurare, dar multe dintre caracteristicile sale sunt deja adoptate de bibliotecile mai de ultimă oră.
Cum este posibil acest lucru? Poate credeți că nu puteți profita de aceste noi funcții JavaScript strălucitoare până când nu sunt acceptate în Internet Explorer, dar gândiți-vă din nou. Transpilere ES au devenit deja atât de omniprezente încât ne putem descurca chiar și fără suport adecvat pentru browser. Cel mai bun transpiler ES disponibil în acest moment este Babel: va lua cel mai nou cod ES6+ și îl va transforma în vanilla ES5, așa că puteți utiliza orice caracteristică ES nouă de îndată ce este inventată (și implementată în Babel, ceea ce se întâmplă de obicei destul de mult). repede).
Cele mai noi caracteristici JavaScript sunt utile în toate cadrele frontale, iar React a fost recent actualizat pentru a funcționa bine cu specificațiile ES6 și ES7. Aceste noi funcții ar trebui să elimine multe dureri de cap atunci când se dezvoltă cu React. Să aruncăm o privire la unele dintre cele mai utile completări și la modul în care pot beneficia un proiect React. Mai târziu, vom vedea cum să folosim câteva instrumente și biblioteci utile cu React în timp ce valorificăm această sintaxă îmbunătățită.
Clasele ES6
Programarea orientată pe obiecte este o paradigmă puternică și adoptată pe scară largă, dar interpretarea JavaScript-ului este puțin exotică. Majoritatea cadrelor frontale, fie că este vorba de Backbone, Ember, Angular sau React, și-au adoptat, astfel, propriile modalități de definire a claselor și de a crea obiecte. Dar cu ES6, avem acum clase tradiționale în JavaScript și pur și simplu are sens să le folosim în loc să scriem propria noastră implementare. Deci, în loc de:
React.createClass({ displayName: 'HelloMessage', render() { return <div>Hello {this.props.name}</div>; } })
putem scrie:
class HelloMessage extends React.Component { render() { return <div>Hello {this.props.name}</div>; } }
Pentru un exemplu mai elaborat, luați în considerare acest cod, folosind vechea sintaxă:
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 comparați cu versiunea 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> ); } }
Aici, metodele ciclului de viață React getDefaultProps
și getInitialState
nu mai sunt necesare. getDefaultProps
devine variabila de clasă statică defaultProps
, iar starea inițială este doar definită în constructor. Singurul dezavantaj este că metodele nu mai sunt legate automat, așa că trebuie să utilizați bind
atunci când apelați handlere de la JSX.
Decoratori
Decoratorii sunt o caracteristică utilă de la ES7. Ele vă permit să măriți comportamentul unei funcții sau clase prin includerea acesteia în altă funcție. De exemplu, să presupunem că doriți să aveți același handler de modificare pe unele dintre componentele dvs., dar nu doriți să vă implicați în antimodelul de moștenire. Puteți folosi în schimb un decorator de clasă. Să definim decoratorul după cum urmează:
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; }
Lucrul important aici este că funcția addChangeHandler
adaugă funcția changeHandler
la prototipul clasei țintă.
Pentru a aplica decoratorul, am putea scrie:
MyClass = changeHandler(MyClass)
sau mai elegant, cu sintaxa ES7:
@addChangeHandler class MyClass { ... }
În ceea ce privește conținutul funcției changeHandler
în sine, cu absența React a legăturii de date în două sensuri, lucrul cu intrări în React poate fi plictisitor. Funcția changeHandler
încearcă să faciliteze acest lucru. Primul parametru specifică o key
pe obiectul de stare, care va servi ca obiect de date pentru intrare. Al doilea parametru este atributul, în care se va salva valoarea din câmpul de intrare. Acești doi parametri sunt setați din JSX folosind cuvântul cheie 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')} /> ) } }
Când utilizatorul modifică câmpul nume de utilizator, valoarea acestuia este salvată în this.state.login.username
, fără a fi nevoie de definirea mai multor handlere personalizate.
Funcții săgeți
Dinamica JavaScript, this
context a fost o durere constantă pentru dezvoltatori, deoarece, oarecum neintuitiv, this
context al unei funcții imbricate este resetat la global, chiar și în interiorul unei clase. Pentru a remedia acest lucru, este de obicei necesar să salvați this
într-o variabilă de domeniu exterioară (de obicei _this
) și să o utilizați în funcțiile interioare:
class DirectorsStore { onFetch(directors) { var _this = this; this.directorsHash = {}; directors.forEach(function(x){ _this.directorsHash[x._id] = x; }) } }
Cu noua sintaxă ES6, function(x){
poate fi rescrisă ca (x) => {
. Această definiție a metodei „săgeată” nu numai că leagă corect this
de domeniul exterior, dar este și considerabil mai scurtă, ceea ce cu siguranță contează atunci când scrieți o mulțime de cod asincron.
onFetch(directors) { this.directorsHash = {}; directors.forEach((x) => { this.directorsHash[x._id] = x; }) }
Destructurarea sarcinilor
Atribuțiile de destructurare, introduse în ES6, vă permit să aveți un obiect compus în partea stângă a unei sarcini:
var o = {p: 42, q: true}; var {p, q} = o; console.log(p); // 42 console.log(q); // true
Este frumos, dar cum ne ajută de fapt în React? Luați în considerare următorul exemplu:
function makeRequest(url, method, params) { var config = { url: url, method: method, params: params }; ... }
Cu destructurare, puteți salva câteva apăsări de taste. Literalul cheilor {url, method, params}
i se atribuie automat valori din domeniul de aplicare cu aceleași nume ca și cheilor. Această expresie este folosită destul de des, iar eliminarea repetiției face ca codul să fie mai puțin predispus la erori.
function makeRequest(url, method, params) { var config = {url, method, params}; ... }
Destructurarea vă poate ajuta, de asemenea, să încărcați doar un subset al unui modul:
const {clone, assign} = require('lodash'); function output(data, optional) { var payload = clone(data); assign(payload, optional); }
Argumente: Implicit, Rest și Spread
Argumentele funcției sunt mai puternice în ES6. În cele din urmă, puteți seta argumentul implicit :
function http(endpoint, method='GET') { console.log(method) ... } http('/api') // GET
Te-ai săturat să te încurci cu obiectul arguments
grele? Cu noua specificație, puteți obține restul argumentelor ca o matrice:
function networkAction(context, method, ...rest) { // rest is an array return method.apply(context, rest); }
Și dacă nu vă place să apelați apply()
, puteți doar să răspândiți o matrice în argumente ale funcției:
myArguments = ['foo', 'bar', 123]; myFunction(...myArguments);
Generatoare și funcții asincrone
ES6 a introdus generatoare JavaScript. Un generator este practic o funcție JavaScript a cărei execuție poate fi întreruptă și apoi reluată mai târziu, amintindu-și starea. De fiecare dată când este întâlnit cuvântul cheie yield
, execuția este întreruptă, iar argumentul yield
este transmis înapoi obiectului apelant:
function* sequence(from, to) { console.log('Ready!'); while(from <= to) { yield from++; } }
Iată un exemplu al acestui generator în acțiune:
> 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 }
Când apelăm funcția generator, aceasta se execută până la primul yield
și apoi se oprește. După ce apelăm next()
, returnează prima valoare și reia execuția. Fiecare yield
returnează o altă valoare, dar după al treilea apel, funcția generator se termină, iar fiecare apel ulterioar la next()
va returna { value: undefined, done: true }
.
Desigur, scopul generatorilor nu este de a crea secvențe numerice elaborate. Partea interesantă este capacitatea lor de a opri și relua execuția funcției, care poate fi folosită pentru a controla fluxul asincron al programului și, în cele din urmă, a scăpa de acele funcții de apel invers.
Pentru a demonstra această idee, avem nevoie mai întâi de o funcție asincronă. De obicei, am avea o operație I/O, dar pentru simplitate, să folosim setTimeout
și să returnăm o promisiune. (Rețineți că ES6 a introdus și promisiuni native în JavaScript.)
function asyncDouble(x) { var deferred = Promise.defer(); setTimeout(function(){ deferred.resolve(x*2); }, 1000); return deferred.promise; }
În continuare, avem nevoie de o funcție de consum:
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(); }
Această funcție ia orice generator ca argument și continuă să apeleze next()
pe el atâta timp cât există valori de yield
. În acest caz, valorile obținute sunt promisiuni și, prin urmare, este necesar să așteptați ca promisiunile să se rezolve și să utilizați recursiunea cu loop()
pentru a realiza bucla între funcțiile imbricate.
Valoarea returnată este rezolvată în handlerul then()
și transmisă la value
, care este definită în domeniul exterior și care va fi trecută în next(value)
. Acest apel face ca valoarea un rezultat al expresiei de randament corespunzătoare. Aceasta înseamnă că acum putem scrie asincron, fără apeluri inverse:
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);
Generatorul myGenerator
va fi întrerupt la fiecare yield
, așteptând ca consumatorul să-și dea promisiunea rezolvată. Și într-adevăr, vom vedea că numerele calculate apar în consolă la intervale de o secundă.
Double 1 = 2 Double 2 = 4 Double 3 = 6
Acest lucru demonstrează conceptul de bază, cu toate acestea, nu vă recomand să utilizați acest cod în producție. În schimb, alegeți o bibliotecă bine testată, cum ar fi co. Acest lucru vă va permite să scrieți cu ușurință cod asincron cu randamente, inclusiv gestionarea erorilor:
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); });
Deci, acest exemplu arată cum să scrieți cod asincron fără apeluri inverse folosind generatoare ES6. ES7 duce această abordare cu un pas mai departe prin introducerea cuvintelor cheie async
și await
și eliminând cu totul necesitatea unei biblioteci generatoare. Cu această capacitate, exemplul anterior ar arăta astfel:
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); } };
În opinia mea, acest lucru elimină durerea de a lucra cu cod asincron în JavaScript. Nu numai în React, ci și peste tot în altă parte.
Generatoarele nu sunt doar mai concise și mai simple, dar ne permit și să folosim tehnici care ar fi foarte greu de implementat cu apeluri inverse. Un exemplu proeminent de bunătate a generatorului este biblioteca middleware koa pentru Node.js. Își propune să înlocuiască Express și, în acest scop, vine cu o caracteristică ucigașă: lanțul de middleware circulă nu numai în aval (cu cererea clientului), ci și în amonte , permițând modificarea ulterioară a răspunsului serverului. Luați în considerare următorul exemplu de server 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
middleware-ului de răspuns este în aval în handlerul de răspuns, care setează corpul răspunsului, iar în fluxul din amonte (după expresia yield
), este permisă modificarea ulterioară a this.body
, precum și alte funcții precum înregistrarea timpului, ceea ce este posibil. deoarece în amonte și în aval împărtășesc același context de funcție. Acesta este mult mai puternic decât Express, în care o încercare de a realiza același lucru s-ar termina astfel:
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);
Probabil că puteți observa deja ce este în neregulă aici; utilizarea unei variabile de start
„globală” va avea ca rezultat o condiție de cursă, returnând prostii cu cereri concurente. Soluția este o soluție care nu este evidentă și puteți uita de modificarea răspunsului în fluxul din amonte.
De asemenea, când utilizați koa, veți obține gratuit fluxul de lucru asincron al generatorului:
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);
Vă puteți imagina promisiunile și apelurile inverse implicate în recrearea acestui mic exemplu în Express.
Cum se leagă toată această discuție Node.js cu React? Ei bine, Node este prima alegere atunci când se ia în considerare un back-end potrivit pentru React. Deoarece Node este scris și în JavaScript, acceptă partajarea codului între back-end și front-end, permițându-ne să construim aplicații web React izomorfe. Dar, mai multe despre asta mai târziu.
Biblioteca Flux
React este grozav la crearea componentelor de vizualizare componabile, dar avem nevoie de o modalitate de a gestiona datele și starea în întreaga aplicație. S-a convenit aproape universal că React este cel mai bine completat de arhitectura aplicației Flux. Dacă sunteți complet nou în Flux, vă recomand o reîmprospătare rapidă.
Ceea ce nu a fost atât de universal agreat este care dintre numeroasele implementări Flux să alegeți. Facebook Flux ar fi alegerea evidentă, dar pentru majoritatea oamenilor este prea verbos. Implementările alternative se concentrează în principal pe reducerea cantității de boilerplate necesare cu o abordare convențională peste configurație și, de asemenea, cu unele funcții convenabile pentru componente de ordin superior, randare pe server și așa mai departe. Unii dintre candidații de top, cu diverse valori de popularitate, pot fi văzute aici. M-am uitat la Alt, Reflux, Flummox, Fluxxor și Marty.js.
Modul meu de a alege biblioteca potrivită nu este în niciun caz obiectiv, dar ar putea fi de ajutor oricum. Fluxxor a fost una dintre primele biblioteci pe care le-am verificat, dar acum pare puțin învechit. Marty.js este interesant, și are o mulțime de caracteristici, dar implică totuși o mulțime de elemente, iar unele dintre funcții par de prisos. Refluxul arată grozav și are o anumită tracțiune, dar se simte puțin greu pentru începători și, de asemenea, nu are documentația adecvată. Flummox și Alt sunt foarte asemănătoare, dar Alt pare să aibă și mai puține standarde, o dezvoltare foarte activă, o documentație actualizată și o comunitate Slack utilă. Prin urmare, am ales Alt.
Flux alternativ
Cu Alt, fluxul de lucru Flux devine mult mai simplu, fără a-și pierde puterea. Documentația Facebook Flux spune multe despre dispecer, dar suntem liberi să ignorăm asta deoarece, în Alt, dispecerul este implicit legat de acțiuni prin convenție și, de obicei, nu necesită niciun cod personalizat. Acest lucru ne lasă doar cu magazine , acțiuni și componente . Aceste trei straturi pot fi utilizate în așa fel încât să se mapeze frumos în modelul de gândire MVC : magazinele sunt modele , acțiunile sunt controlere și componentele sunt vizualizări . Principala diferență este fluxul de date unidirecțional central pentru modelul Flux, ceea ce înseamnă că controlorii (acțiunile) nu pot modifica direct vizualizările (componentele), ci, în schimb, pot declanșa doar modificări de model (magazin), la care vizualizările sunt legate pasiv. Aceasta a fost deja cea mai bună practică pentru unii dezvoltatori Angular iluminați.
Fluxul de lucru este următorul:
- Componentele inițiază acțiuni.
- Magazinele ascultă acțiunile și actualizează datele.
- Componentele sunt legate de magazine și se redau din nou atunci când datele sunt actualizate.
Acțiuni
Când utilizați biblioteca Alt Flux, acțiunile vin în general în două variante: automate și manuale. Acțiunile automate sunt create folosind funcția generateActions
și merg direct la dispecer. Metodele manuale sunt definite ca metode ale claselor dvs. de acțiune și pot merge la dispecer cu o sarcină utilă suplimentară. Cel mai frecvent caz de utilizare al acțiunilor automate este notificarea magazinelor despre un eveniment din aplicație. Acțiunile manuale sunt, printre altele, modalitatea preferată de a trata interacțiunile cu serverul.
Deci apelurile REST API aparțin acțiunilor. Fluxul de lucru complet este următorul:
- Componenta declanșează o acțiune.
- Creatorul acțiunii rulează o cerere de server asincronă, iar rezultatul ajunge la dispecer ca sarcină utilă.
- Magazinul ascultă acțiunea, handlerul de acțiune corespunzător primește rezultatul ca argument, iar magazinul își actualizează starea în consecință.
Pentru cererile AJAX, putem folosi biblioteca axios, care, printre altele, se ocupă cu datele și anteturile JSON fără probleme. În loc de promisiuni sau apeluri inverse, putem folosi modelul ES7 async
/ await
. Dacă starea răspunsului POST
nu este 2XX, se afișează o eroare și trimitem fie datele returnate, fie o eroare primită.
Să ne uităm la o pagină de conectare pentru un exemplu simplu de flux de lucru Alt. Acțiunea de deconectare nu trebuie să facă nimic special, doar anunță magazinul, astfel încât să o putem genera automat. Acțiunea de conectare este manuală și așteaptă datele de conectare ca parametru pentru creatorul acțiunii. După ce primim un răspuns de la server, fie trimitem datele de succes, fie, dacă apare o eroare, trimitem eroarea primită.
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));
Magazine
Magazinul Flux servește două scopuri: are handler de acțiune și poartă stare. Să continuăm exemplul paginii noastre de conectare pentru a vedea cum funcționează.
Să creăm LoginStore
, cu două atribute de stare: user
, pentru utilizatorul conectat curent și error
, pentru eroarea curentă legată de autentificare. În spiritul reducerii boilerplate, Alt ne permite să ne legăm la toate acțiunile dintr-o clasă cu o singură funcție bindActions
.
class LoginStore { constructor() { this.bindActions(LoginActions); this.user = null; this.error = null; } ...
Numele de gestionare sunt definite prin convenție, precedând on
acțiunii corespunzătoare. Deci, acțiunea de login
este gestionată de onLogin
și așa mai departe. Rețineți că prima literă a numelui acțiunii va fi scrisă cu majuscule în stilul camelCase. În LoginStore
, avem următorii handlere, apelați de acțiunile corespunzătoare:
... 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; } }
Componente
Modul obișnuit de a lega depozitele de componente este utilizarea unui fel de mixin React. Dar, din moment ce mixin-urile sunt demodate, trebuie să existe o altă cale. Una dintre noile abordări este utilizarea componentelor de ordin superior. Ne luăm componenta și o punem într-o componentă wrapper, care se va ocupa de ascultarea magazinelor și de apelarea re-rendarii. Componenta noastră va primi starea magazinului în props
. Această abordare este, de asemenea, utilă pentru a ne organiza codul în componente inteligente și stupide, care au devenit la modă în ultima vreme. Pentru Alt, wrapper-ul componentelor este implementat de AltContainer
:
export default class Login extends React.Component { render() { return ( <AltContainer stores={{LoginStore: LoginStore}}> <LoginPage/> </AltContainer> )} }
Componenta noastră LoginPage
folosește și decoratorul changeHandler
introdus mai devreme. Datele din LoginStore
sunt folosite pentru a afișa erori în cazul unei autentificări nereușite, iar redarea este asigurată de AltContainer
. Făcând clic pe butonul de conectare se execută acțiunea de login
, completând fluxul de lucru 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> )} }
Redare izomorfă
Aplicațiile web izomorfe sunt un subiect fierbinte în aceste zile, deoarece rezolvă unele dintre cele mai mari sarcini ale aplicațiilor tradiționale cu o singură pagină. În aceste aplicații, marcajul este creat dinamic de JavaScript în browser. Rezultatul este că conținutul nu este disponibil clienților cu JavaScript dezactivat, în special, crawlerele web ale motoarelor de căutare. Aceasta înseamnă că pagina dvs. web nu este indexată și nu apare în rezultatele căutării. Există modalități de a rezolva acest lucru, dar sunt departe de a fi optime. Abordarea izomorfă încearcă să rezolve această problemă prin pre-rendarea adresei URL solicitate a unei aplicații cu o singură pagină pe server. Cu Node.js, aveți JavaScript pe server, ceea ce înseamnă că React poate rula și pe server. Nu ar trebui să fie prea greu, nu?
Un obstacol este că unele biblioteci Flux, în special cele care folosesc singleton-uri, au dificultăți cu randarea pe server. Când aveți magazine Flux singleton și solicitări multiple concurente către serverul dvs., datele se vor amesteca. Unele biblioteci rezolvă acest lucru folosind instanțe Flux, dar acest lucru vine cu alte dezavantaje, în special necesitatea de a trece acele instanțe în cod. Alt oferă și instanțe Flux, dar a rezolvat și problema redării pe server cu singleton-uri; șterge magazinele după fiecare solicitare, astfel încât fiecare solicitare concomitentă să înceapă cu o listă curată.
Nucleul funcționalității de redare pe partea serverului este furnizat de React.renderToString
. Întreaga aplicație front-end React este rulată și pe server. În acest fel, nu trebuie să așteptăm ca JavaScript de la partea clientului să creeze marcajul; este pre-construit pe server pentru adresa URL accesată și trimis la browser ca HTML. Când rulează JavaScript client, reia de unde a rămas serverul. Pentru a susține acest lucru, putem folosi biblioteca Iso, care este menită să fie utilizată cu Alt.
Mai întâi, inițializam Flux pe server folosind alt.bootstrap
. Este posibil să precompleți magazinele Flux cu date pentru randare. De asemenea, este necesar să decideți ce componentă să redați pentru ce URL, care este funcționalitatea Router
-ului client. Folosim versiunea singleton a alt
, așa că după fiecare randare, trebuie să alt.flush()
magazinele pentru a le curăța pentru o altă solicitare. Folosind suplimentul iso
, starea Flux este serializată la marcajul HTML, astfel încât clientul să știe de unde să ridice:
// 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()}); });
Pe partea clientului, preluăm starea serverului și bootstrap alt
cu datele. Apoi rulăm Router
și React.render
pe containerul țintă, care va actualiza marcajul generat de server după cum este necesar.
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) }) })
Minunat!
Biblioteci utile front-end
Un ghid al ecosistemului React nu ar fi complet fără a menționa câteva biblioteci front-end care joacă deosebit de bine cu React. Aceste biblioteci abordează cele mai comune sarcini găsite în aproape fiecare aplicație web: machete și containere CSS, formulare și butoane stilate, validări, alegere a datei și așa mai departe. Nu are rost să reinventăm roata când acele probleme au fost deja rezolvate.
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.
Multumesc pentru lectura!