Cum se amplifică aplicația dvs. front-end? Cum vă asigurați că codul pe care îl scrieți se poate menține în 6 luni de acum?

Redux a luat cu asalt lumea dezvoltării front-end în 2015 și s-a impus ca standard – chiar și dincolo de scopul React.

La compania unde lucrez, am terminat recent refactorizarea unei baze de cod React destul de mari, adăugând redux în loc de reflux.

Am făcut-o pentru că mersul înainte ar fi fost imposibil fără o aplicație bine structurată și un set bun de reguli.

Baza de coduri are mai mult de doi ani și reflux a fost acolo de la început. A trebuit să schimbăm codul care nu a fost atins în mai mult de un an și a fost destul de încurcat cu componentele React.

Pe baza muncii pe care am făcut-o la proiect, am pus cap la cap acest repo, explicând abordarea noastră în organizarea codului nostru redux.

Când aflați despre redux și rolurile acțiunilor și reducerilor, începeți cu exemple foarte simple. Majoritatea tutorialelor disponibile astăzi nu trec la nivelul următor. Dar dacă construiți ceva cu Redux care este mai complicat decât o listă de lucruri de făcut, veți avea nevoie de un mod mai inteligent de a vă scala scala de coduri în timp.

Cineva a spus asta odată denumirea lucrurilor este una dintre cele mai grele slujbe în informatică. Nu aș putea fi mai de acord. Dar structurarea folderelor și organizarea fișierelor este o secundă apropiată.

Să explorăm modul în care am abordat organizarea codului în trecut.

Funcție vs caracteristică

Există două abordări stabilite de structurare a aplicațiilor: funcția-primul și prima caracteristică.

Una din stânga de mai jos puteți vedea o structură de dosare funcțională. În partea dreaptă puteți vedea o abordare de prima caracteristică.

Scalarea aplicatiei Redux cu rate

Function-first înseamnă că directoarele dvs. de nivel superior sunt numite după scopul fișierelor din interior. Deci tu ai: containere, componente, acțiuni, reductoare, etc.

Acest lucru nu escaladează deloc. Pe măsură ce aplicația dvs. crește și adăugați mai multe funcții, adăugați fișiere în aceleași foldere. Așadar, veți avea nevoie să derulați într-un singur folder pentru a vă găsi fișierul.

Problema este și legată de cuplarea folderelor. Un singur flux prin aplicația dvs. va necesita probabil fișiere din toate folderele.

Un avantaj al acestei abordări este că izolează – în cazul nostru – React de la redux. Deci, dacă doriți să schimbați biblioteca de gestionare a stării, știți ce foldere trebuie să atingeți. Dacă modificați biblioteca de vizualizare, vă puteți păstra intact folderele redux.

Întâi caracteristică înseamnă că directoarele de nivel superior poartă numele principalelor caracteristici ale aplicației: produs, cart, sesiune.

Această abordare se dezvoltă mult mai bine, deoarece fiecare caracteristică nouă vine cu un folder nou. Dar nu aveți nicio separare între componentele React și redux. Schimbarea uneia dintre ele pe termen lung este o treabă foarte dificilă.

În plus, aveți fișiere care nu aparțin niciunei caracteristici. Veți ajunge cu un dosar uzual sau impartit, deoarece doriți să refolosiți codul în mai multe funcții din aplicația dvs.

Cel mai bun din două lumi

Deși nu intră în sfera acestui articol, vreau să ating această idee: separați întotdeauna fișierele de gestionare a statului de fișierele UI.

Gândiți-vă la aplicația dvs. pe termen lung. Imaginați-vă ce se întâmplă cu baza de cod când treceți de la Reacţiona la o altă bibliotecă. Sau gândiți-vă cum ar folosi baza de cod ReactNative în paralel cu versiunea web.

Abordarea noastră începe de la necesitatea de a izola codul React într-un singur folder – numit view – și codul redux într-un folder separat – numit redux.

Această împărțire a primului nivel ne oferă flexibilitatea de a organiza cele două părți separate ale aplicației complet diferite.

În interiorul folderului de vizualizări, preferăm o abordare funcțională în structurarea fișierelor. Acest lucru se simte foarte natural în contextul React: pagini, machete, componente, amplificatori etc.

Pentru a nu înnebuni cu numărul de fișiere dintr-un folder, este posibil să avem o divizare bazată pe caracteristici în fiecare dintre aceste foldere.

Apoi, în interiorul dosarului redux …

Introduceți re-rațe

Fiecare caracteristică a aplicației ar trebui să fie mapată pentru a separa acțiunile și reducerile, deci este logic să alegeți o abordare bazată pe caracteristici.

Originalul abordarea modulară a rațelor este o simplificare plăcută pentru redux și oferă un mod structurat de a adăuga fiecare caracteristică nouă în aplicația dvs.

Cu toate acestea, am vrut să explorăm puțin ce se întâmplă atunci când aplicația se extinde. Ne-am dat seama că un singur fișier pentru o caracteristică devine prea aglomerat și greu de întreținut pe termen lung.

Așa se face re-rațe a fost nascut. Soluția a fost împărțirea fiecărei caracteristici într-un rață pliant.

duck/
├── actions.js
├── index.js
├── operations.js
├── reducers.js
├── selectors.js
├── tests.js
├── types.js
├── utils.js

Un dosar de rață TREBUIE:

  • conține întreaga logică pentru gestionarea unui singur concept în aplicația ta, de exemplu: produs, cart, sesiune, etc.
  • au un index.js fișier care exportă în conformitate cu regulile inițiale de rață.
  • păstrați codul cu scop similar în același fișier, cum ar fi reductoare, selectoare, și acțiuni
  • conține teste legat de rață.

Pentru acest exemplu, nu am folosit nicio abstracție construită deasupra redux. Când creați software, este important să începeți cu cea mai mică cantitate de abstracții. În acest fel, vă asigurați că costul abstracțiilor dvs. nu depășește beneficiile.

Dacă trebuie să vă convingeți că abstracțiile pot fi rele, urmăriți acest lucru discuție minunată de Cheng Lou.

Să vedem ce intră în fiecare fișier.

Tipuri

tipuri fișierul conține numele acțiunilor pe care le expediați în aplicația dvs. Ca o bună practică, ar trebui să încercați să extindeți numele pe baza caracteristicii de care aparțin. Acest lucru ajută la depanarea aplicațiilor mai complexe.

const QUACK = "app/duck/QUACK";
const SWIM = "app/duck/SWIM";

export default {
    QUACK,
    SWIM
};

Acțiuni

Acest fișier conține toate funcțiile creatorului de acțiuni.

import types from "./types";

const quack = ( ) => ( {
    type: types.QUACK
} );

const swim = ( distance ) => ( {
    type: types.SWIM,
    payload: {
        distance
    }
} );

export default {
    swim,
    quack
};

Observați cum toate acțiunile sunt reprezentate de funcții, chiar dacă acestea nu sunt parametrizate. O abordare consecventă este mai mult decât necesară într-o bază de cod mare.

Operațiuni

Pentru a reprezenta operații înlănțuite aveți nevoie de un redux middleware pentru a îmbunătăți funcția de expediere. Câteva exemple populare sunt: redux-thunk, redux-saga sau redux-observabil.

În cazul nostru, folosim redux-thunk. Vrem să-i separăm pe thunks de creatorii de acțiuni, chiar și cu costul scrierii unui cod suplimentar. Deci, definim o operațiune ca o împachetare peste acțiuni.

Dacă operațiunea trimite doar o singură acțiune – nu folosește de fapt redux-thunk – redirecționăm funcția creator de acțiune. Dacă operațiunea folosește un thunk, poate să trimită multe acțiuni și să le lanseze cu promisiuni.

import actions from "./actions";

// This is a link to an action defined in actions.js.
const simpleQuack = actions.quack;

// This is a thunk which dispatches multiple actions from actions.js
const complexQuack = ( distance ) => ( dispatch ) => {
    dispatch( actions.quack( ) ).then( ( ) => {
        dispatch( actions.swim( distance ) );
        dispatch( /* any action */ );
    } );
}

export default {
    simpleQuack,
    complexQuack
};

Spune-le operații, ciudate, saga, epopee, este alegerea ta. Găsiți doar o convenție de numire și rămâneți cu ea.

La sfârșit, când discutăm despre index, vom vedea că operațiunile fac parte din interfața publică a raței. Acțiunile sunt încapsulate, operațiunile sunt expuse.

Reductoare

Dacă o caracteristică are mai multe fațete, ar trebui să utilizați cu siguranță mai multe reductoare pentru a gestiona diferite părți ale formei de stare. În plus, nu vă fie frică să utilizați combineReducere cât este nevoie. Acest lucru vă oferă multă flexibilitate atunci când lucrați cu o formă complexă de stare.

import { combineReducers } from "redux";
import types from "./types";

/* State Shape
{
    quacking: bool,
    distance: number
}
*/

const quackReducer = ( state = false, action ) => {
    switch( action.type ) {
        case types.QUACK: return true;
        /* ... */
        default: return state;
    }
}

const distanceReducer = ( state = 0, action ) => {
    switch( action.type ) {
        case types.SWIM: return state + action.payload.distance;
        /* ... */
        default: return state;
    }
}

const reducer = combineReducers( {
    quacking: quackReducer,
    distance: distanceReducer
} );

export default reducer;

Într-o aplicație la scară largă, arborele dvs. de stare va avea o adâncime de cel puțin 3 niveluri. Funcțiile de reducere ar trebui să fie cât mai mici posibil și să gestioneze numai construcții simple de date. combineReducere funcția de utilitate este tot ceea ce aveți nevoie pentru a construi o formă de stare flexibilă și menținută.

Verificați exemplu de proiect complet și uite cum combineReducere este folosit. Odată ajuns în reductori.js fișiere și apoi în store.js fișier, unde punem împreună întregul arbore de stare.

Selectoare

Împreună cu operațiile, selectoarele fac parte din interfața publică a unei rațe. Împărțirea între operații și selectoare seamănă cu Model CQRS.

Funcțiile de selecție iau o porțiune din starea aplicației și returnează unele date pe baza acestora. Nu introduc niciodată modificări în starea aplicației.

function checkIfDuckIsInRange( duck ) {
    return duck.distance > 1000;
}

export default {
    checkIfDuckIsInRange
};

Index

Acest fișier specifică ce este exportat din folderul rață. O sa:

  • exportați implicit funcția de reducere a raței.
  • exportați ca denumite exporturi selectorii și operațiunile.
  • exportați tipurile dacă sunt necesare în alte rațe.
import reducer from "./reducers";

export { default as duckSelectors } from "./selectors";
export { default as duckOperations } from "./operations";
export { default as duckTypes } from "./types";

export default reducer;

Teste

Un avantaj al utilizării Redux și a structurii rațelor este că vă puteți scrie testele lângă codul pe care îl testați.

Testarea codului dvs. Redux este destul de simplă:

import expect from "expect.js";
import reducer from "./reducers";
import actions from "./actions";

describe( "duck reducer", function( ) {
    describe( "quack", function( ) {
        const quack = actions.quack( );
        const initialState = false;

        const result = reducer( initialState, quack );

        it( "should quack", function( ) {
            expect( result ).to.be( true ) ;
        } );
    } );
} );

În interiorul acestui fișier puteți scrie teste pentru reductoare, operații, selectoare etc.

Aș putea scrie un articol complet diferit despre beneficiile testării codului dvs., există atât de multe dintre ele. Doar fă-o!

Așa că este

Partea drăguță despre re-ducks este că veți putea folosi același model pentru tot codul redux.

Împărțirea bazată pe caracteristici pentru codul redux este mult mai flexibilă și scalabilă pe măsură ce baza de cod a aplicației crește. Și împărțirea bazată pe funcții pentru vizualizări funcționează atunci când creați componente mici care sunt partajate între aplicație.

Puteți verifica o bază de cod complet react-redux-example Aici. Rețineți că repo este încă în curs de dezvoltare activă.

Cum vă structurați aplicațiile de redux? Aștept cu nerăbdare să aud niște feedback cu privire la această abordare pe care am prezentat-o.

Dacă vi s-a părut util acest articol, faceți clic pe inima verde de mai jos și voi ști că eforturile mele nu sunt în zadar.