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ędziehistory.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:
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:
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 <Lin
K/> 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 <Route
r/> 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 <Rout
e/> 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 <Rout
e/> 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 <BrowserRoute
r/>, the &l
t;trasa/> komponent could si
mply łą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.