autorstwa Michaela Sargenta

biblioteka routingu jest kluczowym elementem każdej złożonej, jednostronicowej aplikacji. Jeśli tworzysz aplikacje internetowe za pomocą Reacta i Redux, prawdopodobnie używałeś lub przynajmniej słyszałeś o routerze Reactowym. Jest to dobrze znana biblioteka routingu dla Reacta i świetne rozwiązanie dla wielu zastosowań.

ale React Router nie jest jedynym realnym rozwiązaniem w ekosystemie React / Redux. W rzeczywistości istnieje mnóstwo rozwiązań routingu stworzonych dla Reacta i Redux, z których każde ma inne interfejsy API, funkcje i cele — a lista ta dopiero się powiększa. Nie trzeba dodawać, że routing po stronie klienta nie zniknie w najbliższym czasie, a Biblioteki routingu jutra wciąż mają dużo miejsca na projektowanie.

dzisiaj chcę zwrócić waszą uwagę na temat routingu w Redux. Przedstawię i przedstawię argumenty za Redux-first routing-paradygmatem, który sprawia, że Redux jest gwiazdą modelu routingu i wspólnym wątkiem wśród wielu rozwiązań routingu Redux. Zademonstruję, jak połączyć podstawowe, niezależne od frameworka API w mniej niż 100 liniach kodu, zanim zbadam możliwości rzeczywistego użycia z Reactem i innymi frameworkami front-end.

trochę historii

w przeglądarce lokalizacja (informacje URL) i historia sesji (stos lokalizacji odwiedzanych przez bieżącą kartę przeglądarki) są przechowywane w globalnym obiekcie window. Są one dostępne za pośrednictwem:

  • window.location (API lokalizacji)
  • window.history (History API).

interfejs API historii oferuje następujące metody nawigacji historii, godne uwagi ze względu na ich możliwość aktualizacji historii i lokalizacji przeglądarki bez konieczności ponownego ładowania strony:

  • pushState(href) — przesuwa nową lokalizację na stos historii
  • replaceState(href) — nadpisuje bieżącą lokalizację na stosie
  • back() — przechodzi do poprzedniej lokalizacji na stosie
  • forward() — przechodzi do następnej lokalizacji na stosie
  • go(index) — nawiguje do miejsca na stosie, w dowolnym kierunku.

razem interfejsy API Historia i lokalizacja umożliwiają nowoczesny paradygmat routingu po stronie klienta znany jako routing pushState-pierwszy bohater naszej historii.

teraz prawie przestępstwem jest wspominanie o interfejsach API historii i lokalizacji bez wspominania o nowoczesnej bibliotece opakowań, takiej jak history.

Reacttraining/history
Zarządzaj historią sesji za pomocą JavaScriptgithub.com

history zapewnia prosty, ale potężny interfejs API do łączenia się z historią i lokalizacją przeglądarki, jednocześnie pokrywając niespójności między różnymi implementacjami przeglądarki. Jest on używany jako wzajemna lub wewnętrzna zależność w wielu nowoczesnych bibliotekach routingu, i zrobię wiele odniesień do niego w tym artykule.

Redux i Pushstate Routing

drugim bohaterem naszej historii jest Redux. Jest rok 2017, więc oszczędzę wam wstępu i przejdę od razu do rzeczy:

używając zwykłego routingu pushState w aplikacji Redux, dzielimy stan aplikacji na dwie domeny: historię przeglądarki i sklep Redux.

oto jak to wygląda z Reactowym routerem, który tworzy instancje i zawija history:

history → React Router ↘ view Redux ↗

teraz wiemy, że nie wszystkie dane muszą znajdować się w sklepie. Na przykład lokalny stan komponentu jest często odpowiednim miejscem do przechowywania danych specyficznych dla pojedynczego komponentu.

ale dane o lokalizacji nie są trywialne. Jest to dynamiczna i ważna część stanu aplikacji — rodzaj danych, które należą do sklepu. Trzymanie go w sklepie umożliwia Redux luksusowe funkcje, takie jak debugowanie podróży w czasie i łatwy dostęp z dowolnego komponentu podłączonego do sklepu.

jak przenieść lokal do sklepu?

nie można obejść faktu, że przeglądarka odczytuje i przechowuje historię i informacje o lokalizacji w window, ale możemy zachować kopię danych o lokalizacji w sklepie i synchronizować je z przeglądarką.

czy to nie jest to, co react-router-redux robi dla Reactowego routera?

tak, ale tylko po to, aby włączyć możliwości podróżowania w czasie DevTools Redux. Aplikacja nadal zależy od danych lokalizacji przechowywanych w Reactowym routerze:

history → React Router ↘ ↕ view Redux ↗

używanie react-router-redux do odczytu danych o lokalizacji ze sklepu zamiast Reactowego routera jest odradzane (ze względu na potencjalnie sprzeczne źródła prawdy).

Czy możemy zrobić coś lepszego?

czy możemy zbudować alternatywny model routingu — taki, który jest zbudowany od podstaw, aby dobrze grać z Redux, umożliwiając nam odczytywanie i aktualizowanie lokalizacji w sposób Redux — za pomocą store.getState()i store.dispatch()?

absolutnie możemy, A nazywa się to Redux-first routing.

Redux – pierwszy Routing

Redux – First routing jest odmianą routingu pushState, który czyni Redux gwiazdą modelu routingu.

rozwiązanie Redux-first routing spełnia następujące kryteria:

  • lokal znajduje się w sklepie Redux.
  • lokalizacja jest zmieniana przez wysyłanie akcji Redux.
  • aplikacja odczytuje dane o lokalizacji wyłącznie ze sklepu.
  • historia sklepu i przeglądarki są synchronizowane za kulisami.

oto podstawowa idea tego, jak to wygląda:

history ↕ Redux → router → view

czekaj, czy nie ma jeszcze dwóch źródeł danych o lokalizacji?

tak, ale jeśli możemy zaufać, że historia przeglądarki i sklep Redux są zsynchronizowane, możemy zbudować nasze aplikacje tak, aby tylko odczytywały dane o lokalizacji ze sklepu. Następnie, z punktu widzenia aplikacji, jest tylko jedno źródło prawdy-sklep.

jak wykonać Redux-First routing?

możemy zacząć od stworzenia modelu koncepcyjnego, łącząc podstawowe elementy routingu po stronie klienta i modeli cyklu życia danych Redux.

powrót do modelu routingu po stronie klienta

routing po stronie Klienta to wieloetapowy proces, który rozpoczyna się od nawigacji, a kończy na renderowaniu-routing sam w sobie jest tylko jednym krokiem w tym procesie! Przejrzyjmy szczegóły:

  • nawigacja-Wszystko zaczyna się od zmiany lokalizacji. Istnieją 2 rodzaje nawigacji: wewnętrzna i zewnętrzna. Wewnętrzna nawigacja odbywa się z poziomu aplikacji (np. poprzez API Historia), podczas gdy nawigacja zewnętrzna występuje, gdy użytkownik wchodzi w interakcję z paskiem nawigacyjnym przeglądarki lub wchodzi do aplikacji z zewnętrznej strony.
  • reagowanie na nawigację-po zmianie lokalizacji aplikacja reaguje przekazując nową lokalizację do routera. Starsze techniki routingu polegały na wyszukiwaniu window.location, ale obecnie mamy przydatne narzędzie history.listen.
  • Routing — następnie nowa lokalizacja jest dopasowywana do odpowiadającej jej zawartości strony. Kod obsługujący ten krok nazywany jest routerem i zazwyczaj pobiera parametr wejściowy pasujących tras i stron zwany konfiguracją trasy.
  • renderowanie — ostatecznie zawartość jest renderowana na kliencie. Ten krok może być oczywiście obsługiwany przez front-endowy framework / bibliotekę, taką jak React.

zauważ, że biblioteki routingu nie muszą obsługiwać każdej części modelu routingu.

niektóre biblioteki, takie jak router React i Router Vue, robią — podczas gdy inne, takie jak router Uniwersalny, dotyczą tylko jednego aspektu (takiego jak routing), zapewniając elastyczność w innych aspektach:

Biblioteki routingu mogą mieć różne zakresy odpowiedzialności. (Kliknij aby powiększyć)

powrót do modelu cyklu życia danych Redux

Redux oferuje jednokierunkowy model przepływu danych/cyklu życia, który prawdopodobnie nie wymaga wprowadzenia — ale oto krótki przegląd dla dobrej miary:

  • Akcja — każda zmiana stanu rozpoczyna się od wysłania akcji Redux (zwykły obiekt zawierający type i opcjonalny ładunek).
  • Middleware — akcja przechodzi przez sieć middleware sklepu, gdzie można przechwycić akcje i wykonać dodatkowe zachowanie. Middleware są powszechnie używane do obsługi efektów ubocznych w aplikacjach Redux.
  • reduktor-akcja dociera następnie do reduktora głównego, który oblicza Następny stan magazynu jako czystą funkcję poprzedniego stanu i odebraną akcję. Reduktor korzeniowy może składać się z pojedynczych reduktorów, z których każdy obsługuje kawałek stanu sklepu.
  • nowy stan-sklep zapisuje nowy stan zwrócony przez reduktor i powiadamia swoich subskrybentów o zmianie (w Reaccie, poprzez connect).
  • renderowanie — wreszcie widok połączony ze sklepem może ponownie renderować zgodnie z nowym stanem.

budowanie modelu Redux-First Routing

jednokierunkowy charakter routingu po stronie klienta i modeli cyklu życia danych Redux dobrze nadają się do połączonego modelu, który spełnia kryteria określone dla routingu Redux-first.

w tym modelu router jest subskrybowany w sklepie, nawigacja odbywa się za pomocą akcji Redux, a aktualizacje historii przeglądarki są obsługiwane przez niestandardowe oprogramowanie pośredniczące. Przyjrzyjmy się szczegółom tego modelu:

  • wewnętrzna nawigacja za pomocą akcji Redux — zamiast używać bezpośrednio API historii, wewnętrzna nawigacja jest osiągana przez wysłanie jednej z 5 akcji nawigacyjnych, które odzwierciedlają metody nawigacji historii.
  • aktualizacja historii przeglądarki za pomocą oprogramowania pośredniczącego-oprogramowanie pośredniczące służy do przechwytywania działań nawigacyjnych i obsługi efektu ubocznego aktualizacji historii przeglądarki. Ponieważ nowa lokalizacja nie jest koniecznie lub łatwo znana bez uprzedniego zapoznania się z historią przeglądarki (np. w przypadku działania go), działania nawigacyjne nie mogą dotrzeć do reduktora.
  • reagowanie na nawigację-proces wykonywania jest kontynuowany za pomocą słuchacza history, który reaguje na nawigację (zarówno z poziomu oprogramowania pośredniego, jak i nawigacji zewnętrznej), wysyłając drugą akcję, która zawiera nową lokalizację.
  • location Reducer — akcja wysyłana przez słuchacza dociera następnie do Location reducer, który dodaje lokalizację do sklepu. Reduktor położenia określa również kształt stanu położenia.
  • połączony routing – Router połączony ze sklepem może następnie reaktywnie określić nową zawartość strony po powiadomieniu o zmianie lokalizacji w sklepie.
  • renderowanie — wreszcie, strona może być ponownie renderowana z nową zawartością.

zauważ, że nie jest to jedyny sposób na osiągnięcie Redux-first routing — niektóre warianty zawierają użycie wzmacniacza sklepu i/lub dodatkowej logiki w oprogramowaniu pośrednim — ale jest to prosty model, który obejmuje wszystkie podstawy.

Podstawowa implementacja

idąc za modelem, na który właśnie spojrzeliśmy, zaimplementujmy podstawowe API — działania, oprogramowanie pośrednie, słuchacz i reduktor.

użyjemy pakietu history jako wewnętrznej zależności i zbudujemy rozwiązanie stopniowo. Jeśli wolisz śledzić wraz z końcowym wynikiem, możesz go zobaczyć tutaj.

akcje

zaczniemy od zdefiniowania 5 akcji nawigacyjnych, które odzwierciedlają metody nawigacji historii:

// constants.jsexport const PUSH = 'ROUTER/PUSH';export const REPLACE = 'ROUTER/REPLACE';export const GO = 'ROUTER/GO';export const GO_BACK = 'ROUTER/GO_BACK';export const GO_FORWARD = 'ROUTER/GO_FORWARD';

// actions.jsexport const push = (href) => ({ type: PUSH, payload: href,});
export const replace = (href) => ({ type: REPLACE, payload: href,});
export const go = (index) => ({ type: GO, payload: index,});
export const goBack = () => ({ type: GO_BACK,});
export const goForward = () => ({ type: GO_FORWARD,});

Middleware

następnie zdefiniujmy middleware. Powinien przechwycić działania nawigacyjne, wywołać odpowiednie metody nawigacji history, a następnie zatrzymać akcję przed dotarciem do reduktora – ale pozostawić wszystkie inne działania w spokoju:

// middleware.jsexport const routerMiddleware = (history) => () => (next) => (action) => { switch (action.type) { case PUSH: history.push(action.payload); break; case REPLACE: history.replace(action.payload); break; case GO: history.go(action.payload); break; case GO_BACK: history.goBack(); break; case GO_FORWARD: history.goForward(); break; default: return next(action); }};

jeśli wcześniej nie miałeś okazji napisać lub zbadać wewnętrznych programów middleware Redux, zapoznaj się z tym wstępem.

słuchacz historii

następnie będziemy potrzebować słuchacza history, który reaguje na nawigację, wysyłając nową akcję zawierającą nowe informacje o lokalizacji.

najpierw dodajmy nowy typ akcji i twórcę. Ciekawymi częściami lokalizacji są pathname, search i

// constants.jsexport const LOCATION_CHANGE = 'ROUTER/LOCATION_CHANGE';
// actions.jsexport const locationChange = ({ pathname, search, hash }) => ({ type: LOCATION_CHANGE, payload: { pathname, search, hash, },});

następnie napiszmy funkcję listener:

// listener.jsexport function startListener(history, store) { history.listen((location) => { store.dispatch(locationChange({ pathname: location.pathname, search: location.search, hash: location.hash, })); });}

zrobimy jeden mały dodatek-początkową wysyłkę locationChange, aby uwzględnić początkowe wejście do aplikacji (które nie zostanie odebrane przez słuchacza historii):

// listener.jsexport function startListener(history, store) { store.dispatch(locationChange({ pathname: history.location.pathname, search: history.location.search, hash: history.location.hash, }));
 history.listen((location) => { store.dispatch(locationChange({ pathname: location.pathname, search: location.search, hash: location.hash, })); });}

reduktor

następnie zdefiniujmy reduktor lokalizacji. Użyjemy prostego kształtu stanu i wykonamy minimalną pracę w reduktorze:

// reducer.jsconst initialState = { pathname: '/', search: '', hash: '',};
export const routerReducer = (state = initialState, action) => { switch (action.type) { case LOCATION_CHANGE: return { ...state, ...action.payload, }; default: return state; }};

kod aplikacji

na koniec podłączmy nasze API do kodu aplikacji:

// index.jsimport { combineReducers, applyMiddleware, createStore } from 'redux'import { createBrowserHistory } from 'history'import { routerReducer } from './reducer'import { routerMiddleware } from './middleware'import { startListener } from './listener'import { push } from './actions'
// Create the history objectconst history = createBrowserHistory()
// Build the root reducerconst rootReducer = combineReducers({ // ...otherReducers, router: routerReducer,}) // Build the middlewareconst middleware = routerMiddleware(history)
// Create the storeconst store = createStore(rootReducer, {}, applyMiddleware(middleware))
// Start the history listenerstartListener(history, store)
// Now you can read location data from the store!let currentLocation = store.getState().router.pathname
// You can also subscribe to changes in the location!let unsubscribe = store.subscribe(() => { let previousLocation = currentLocation currentLocation = store.getState().router.pathname
 if (previousLocation !== currentLocation) { // You can render your application reactively here! }})
// And you can dispatch navigation actions from anywhere!store.dispatch(push('/about'))

i to wszystko! Korzystając z naszego małego (poniżej 100 linii kodu) API, spełniliśmy wszystkie kryteria routingu Redux-first:

  • lokal znajduje się w sklepie Redux. ✔
  • lokalizacja jest zmieniana przez wysyłanie akcji Redux. ✔
  • aplikacja odczytuje dane o lokalizacji wyłącznie ze sklepu. ✔
  • historia sklepu i przeglądarki są synchronizowane za kulisami. ✔

Zobacz wszystkie pliki razem tutaj-możesz je zaimportować do swojego projektu lub użyć go jako punktu wyjścia do opracowania własnej implementacji.

pakiet redux-first-routing

umieściłem również API w pakiecie redux-first-routing, który możesz npm install i używać w ten sam sposób.

mksarge / redux-first-routing
redux-first-routing — minimalna, niezależna od frameworka baza do realizacji Redux-first routing.github.com

zawiera implementację podobną do tej, którą tutaj zbudowaliśmy, ale z godnym uwagi dodatkiem parsowania zapytań za pośrednictwem pakietu query-string.

czekaj-a co z rzeczywistym składnikiem routingu?

być może zauważyłeś, że redux-first-routing dotyczy tylko nawigacyjnego aspektu modelu routingu:

oddzielając aspekt nawigacyjny od innych aspektów naszego modelu routingu, zyskaliśmy pewną elastyczność — redux-first-routing jest zarówno agnostyczny dla routera, jak i agnostyczny dla frameworka.

możesz więc sparować go z biblioteką taką jak Universal Router, aby stworzyć kompletne rozwiązanie routingu Redux-first dla dowolnej platformy front-end:

Kliknij tutaj, aby rozpocząć korzystanie z redux-first-routing + universal-router.

lub możesz zbudować opiniotwórcze wiązania dla wybranego frameworka — i to właśnie zrobimy dla Reacta w następnej i ostatniej sekcji tego artykułu.

użycie z Reaktem

zakończmy naszą eksplorację, przyglądając się, w jaki sposób możemy zbudować komponenty połączone ze sklepem do deklaratywnej nawigacji i routingu w Reakcie.

deklaratywna Nawigacja

do nawigacji możemy użyć komponentu podłączonego do sklepu <LinK/> podobnego do tego w Reactowym routerze i innych rozwiązaniach reactowego routingu.

po prostu nadpisuje domyślne zachowanie elementu kotwicy <a/> i dispatchhes akcji push po kliknięciu:

// Link.jsimport React from 'react';import { connect } from 'react-redux';import { push as pushAction, replace as replaceAction } from './actions';
const Link = (props) => { const { to, replace, children, dispatch, ...other } = props;
 const handleClick = (event) => { // Ignore any click other than a left click if ((event.button && event.button !== 0) || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey || event.defaultPrevented === true) { return; } // Prevent the default behaviour (page reload, etc.) event.preventDefault();
 // Dispatch the appropriate navigation action if (replace) { dispatch(replaceAction(to)); } else { dispatch(pushAction(to)); } };
 return ( <a href={to} onClick={handleClick} {...other}> {children} </a>);};
export default connect()(Link);

bardziej kompletną implementację można znaleźć tutaj.

Routing deklaratywny

chociaż komponent nawigacyjny nie ma zbyt wiele, istnieje niezliczona ilość sposobów zaprojektowania komponentu routingu — co czyni go najciekawszym elementem każdego rozwiązania routingu.

co to w ogóle jest router?

możesz ogólnie zobaczyć router jako funkcję lub czarną skrzynkę z dwoma wejściami i Jednym Wyjściem:

route configuration ↘ matched content current location ↗

chociaż routing i późniejsze renderowanie mogą występować w oddzielnych krokach, React ułatwia i intuicyjnie łączy je w deklaratywne API routingu. Przyjrzyjmy się dwóm strategiom osiągnięcia tego celu.

Strategy 1: a monolithic <Router/> component

We can use a monolithic, Store-connected<Route r/ > component that:

  • akceptuje obiekt konfiguracji trasy za pomocą właściwości
  • odczytuje dane lokalizacji ze sklepu Redux
  • oblicza nową zawartość przy każdej zmianie lokalizacji
  • renderuje/ponownie renderuje zawartość odpowiednio.

konfiguracja trasy może być zwykłym obiektem JavaScript, który zawiera wszystkie pasujące ścieżki i strony (scentralizowana konfiguracja trasy).

oto jak to może wyglądać:

const routes = 
React.render( <Provider store={store}> <Router routes={routes}> </Provider>, document.getElementById('app'))

całkiem proste, prawda? Nie ma potrzeby stosowania zagnieżdżonych tras JSX-wystarczy pojedynczy obiekt konfiguracji trasy i pojedynczy komponent routera.

jeśli ta strategia jest dla Ciebie atrakcyjna, sprawdź moją pełniejszą implementację w bibliotece redux-json-router. Zawiera redux-first-routing i zapewnia powiązania Reactowe do deklaratywnej nawigacji i routingu przy użyciu strategii, które do tej pory zbadaliśmy.

mksarge / redux-JSON-router
redux-json-router – deklaratywny, Redux – pierwszy routing dla przeglądarki React/Redux applications.github.com

Strategia 2: Komponowalne <Route/> komponenty

chociaż monolityczny komponent może być prostym sposobem na osiągnięcie deklaratywnego routingu w Reakcie, to zdecydowanie nie jest to jedyny sposób.

komponowalny charakter Reacta pozwala na inną interesującą możliwość: używanie JSX do definiowania tras w zdecentralizowany sposób. Oczywiście najlepszym przykładem jest react Router <Route/> API:

React.render( <BrowserRouter> <Route path='/' component={Home}/> <Route path='/about component={About}/> ... </BrowserRouter>

inne biblioteki routingu również badają ten pomysł. Chociaż nie miałem okazji tego zrobić, nie widzę powodu, dla którego podobne API nie mogło zostać zaimplementowane na pakiecie redux-first-routing.

zamiast polegać na danych lokalizacji dostarczonych przez <BrowserRouter/>, the &lt;trasa/> komponent could simply łączy się ze sklepem:

React.render( <Provider store={store}> <Route path='/' component={Home}/> <Route path='/about component={About}/> ... </Provider>

jeśli jest to coś, co jesteś zainteresowany w budowaniu lub użyciu, daj mi znać w komentarzach! Aby dowiedzieć się więcej o różnych strategiach konfiguracji tras, zapoznaj się z tym wprowadzeniem na stronie React Router.

podsumowanie

mam nadzieję, że ta eksploracja pomogła pogłębić Twoją wiedzę na temat routingu po stronie klienta i pokazała, jak łatwo jest to osiągnąć w sposób Redux.

jeśli szukasz kompletnego rozwiązania routingu Redux, możesz użyć pakietu redux-first-routing z kompatybilnym routerem wymienionym w readme. A jeśli uważasz, że potrzebujesz opracować dostosowane rozwiązanie, mam nadzieję, że ten post dał ci dobry punkt wyjścia do tego.

jeśli chcesz dowiedzieć się więcej o routingu po stronie klienta w Reakcie i Redux, zapoznaj się z poniższymi artykułami-pomogły mi one lepiej zrozumieć tematy, które omawiałem tutaj:

  • niech URL Do rozmowy Tyler Thompson
  • może nie potrzebujesz React Router Konstantin Tarkus
  • czy w ogóle potrzebuję Biblioteki routingu? przez Jamesa K. Nelsona
  • i niezliczone dyskusje informacyjne w kwestiach react-router-redux.

routing po stronie Klienta to przestrzeń z nieskończonymi możliwościami projektowymi i jestem pewien, że niektórzy z was bawili się pomysłami podobnymi do tych, którymi się tutaj podzieliłem. Jeśli chcesz kontynuować rozmowę, chętnie skontaktuję się z Tobą w komentarzach lub za pośrednictwem Twittera. Dzięki za przeczytanie!

Edycja 22/06/17: Sprawdź również ten artykuł na redux-first-router, osobnym projekcie, który wykorzystuje inteligentne typy akcji, aby uzyskać potężne możliwości routingu.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.