Construirea unei aplicații web cu React, Redux și Sanity.io
Publicat: 2022-03-10Evoluția rapidă a platformelor digitale a impus limite serioase CMS-urilor tradiționale precum Wordpress. Aceste platforme sunt cuplate, inflexibile și se concentrează mai degrabă pe proiect, decât pe produs. Din fericire, au fost dezvoltate mai multe CMS fără cap pentru a face față acestor provocări și multe altele.
Spre deosebire de CMS tradițional, CMS fără cap, care poate fi descris ca Software ca serviciu (SaaS), poate fi folosit pentru a dezvolta site-uri web, aplicații mobile, afișaje digitale și multe altele. Ele pot fi folosite pe platforme nelimitate. Dacă sunteți în căutarea unui CMS care să fie independent de platformă, mai întâi de dezvoltator și care oferă suport pe mai multe platforme, nu trebuie să priviți mai departe de CMS fără cap.
Un CMS fără cap este pur și simplu un CMS fără cap. head
aici se referă la interfața sau stratul de prezentare, în timp ce body
se referă la backend sau depozitul de conținut. Acest lucru oferă o mulțime de beneficii interesante. De exemplu, permite dezvoltatorului să aleagă orice interfață la alegere și, de asemenea, puteți proiecta stratul de prezentare după cum doriți.
Există o mulțime de CMS fără cap, unele dintre cele mai populare includ Strapi, Contentful, Contentstack, Sanity, Butter CMS, Prismic, Storyblok, Directus etc. Aceste CMS fără cap sunt bazate pe API și au punctele lor forte individuale. De exemplu, CMS precum Sanity, Strapi, Contentful și Storyblok sunt gratuite pentru proiecte mici.
Aceste CMS fără cap se bazează și pe diferite stive tehnologice. În timp ce Sanity.io se bazează pe React.js, Storyblok se bazează pe Vue.js. În calitate de dezvoltator React, acesta este motivul principal pentru care m-am interesat rapid de Sanity. Cu toate acestea, fiind un CMS fără cap, fiecare dintre aceste platforme poate fi conectată pe orice frontend, fie Angular, Vue sau React.
Fiecare dintre aceste CMS fără cap are atât planuri gratuite, cât și plătite, care reprezintă un salt semnificativ de preț. Deși aceste planuri plătite oferă mai multe funcții, nu ați dori să plătiți atât de mult pentru un proiect de dimensiuni mici sau mijlocii. Sanity încearcă să rezolve această problemă prin introducerea opțiunilor de plata pe măsură. Cu aceste opțiuni, vei putea plăti pentru ceea ce folosești și vei evita creșterea prețului.
Un alt motiv pentru care aleg Sanity.io este limbajul lor GROQ. Pentru mine, Sanity iese în evidență din mulțime prin oferirea acestui instrument. Graphical-Relational Object Queries (GROQ) reduce timpul de dezvoltare, vă ajută să obțineți conținutul de care aveți nevoie în forma în care aveți nevoie și, de asemenea, ajută dezvoltatorul să creeze un document cu un nou model de conținut fără modificări de cod.
Mai mult, dezvoltatorii nu sunt constrânși la limbajul GROQ. De asemenea, puteți utiliza GraphQL sau chiar axios
tradițional și puteți fetch
în aplicația dvs. React pentru a interoga backend-ul. La fel ca majoritatea altor CMS fără cap, Sanity are o documentație cuprinzătoare care conține sfaturi utile pentru a construi pe platformă.
Notă: Acest articol necesită o înțelegere de bază a React, Redux și CSS.
Noțiuni introductive cu Sanity.io
Pentru a utiliza Sanity în aparatul dvs., va trebui să instalați instrumentul Sanity CLI. Deși acesta poate fi instalat local pe proiectul dvs., este de preferat să îl instalați global pentru a-l face accesibil oricăror aplicații viitoare.
Pentru a face acest lucru, introduceți următoarele comenzi în terminalul dvs.
npm install -g @sanity/cli
Indicatorul -g
din comanda de mai sus permite instalarea globală.
Apoi, trebuie să inițializam Sanity în aplicația noastră. Deși acesta poate fi instalat ca proiect separat, de obicei este de preferat să îl instalați în aplicația dvs. frontend (în acest caz React).
Pe blogul ei, Kapehe a explicat în detaliu cum să integrezi Sanity cu React. Va fi util să parcurgeți articolul înainte de a continua cu acest tutorial.
Introduceți următoarele comenzi pentru a inițializa Sanity în aplicația dvs. React.
sanity init
Comanda sanity
devine disponibilă pentru noi când am instalat instrumentul Sanity CLI. Puteți vizualiza o listă a comenzilor Sanity disponibile tastând sanity
sau sanity help
în terminalul dvs.
Când configurați sau inițializați proiectul, va trebui să urmați instrucțiunile pentru a-l personaliza. De asemenea, vi se va cere să creați un set de date și puteți chiar să alegeți setul de date personalizat al acestora, populat cu date. Pentru această aplicație de listare, vom folosi setul de date personalizat pentru filme științifico-fantastice de la Sanity. Acest lucru ne va scuti de introducerea datelor noi înșine.
Pentru a vizualiza și edita setul de date, accesați subdirectorul cd
din terminalul dvs. și introduceți sanity start
. Acesta rulează de obicei pe https://localhost:3333/
. Este posibil să vi se solicite să vă autentificați pentru a accesa interfața (asigurați-vă că vă autentificați cu același cont pe care l-ați folosit la inițializarea proiectului). O captură de ecran a mediului este prezentată mai jos.
Comunicare bidirecțională Sanity-React
Sanity și React trebuie să comunice între ele pentru o aplicație complet funcțională.
Setarea CORS Origins în Sanity Manager
Mai întâi vom conecta aplicația noastră React la Sanity. Pentru a face acest lucru, conectați-vă la https://manage.sanity.io/
și localizați CORS origins
în API Settings
din fila Settings
. Aici, va trebui să vă conectați originea frontend-ului la backend-ul Sanity. Aplicația noastră React rulează implicit pe https://localhost:3000/
, așa că trebuie să o adăugăm la CORS.
Acest lucru este prezentat în figura de mai jos.
Conectarea Sanity pentru a reacționa
Sanity asociază un project ID
de proiect fiecărui proiect pe care îl creați. Acest ID este necesar atunci când îl conectați la aplicația dvs. frontală. Puteți găsi ID-ul proiectului în Sanity Manager.
Backend-ul comunică cu React folosind o bibliotecă cunoscută sub numele de sanity client
. Trebuie să instalați această bibliotecă în proiectul dvs. Sanity introducând următoarele comenzi.
npm install @sanity/client
Creați un fișier sanitySetup.js
(numele fișierului nu contează), în folderul src
al proiectului și introduceți următoarele coduri React pentru a configura o conexiune între Sanity și React.
import sanityClient from "@sanity/client" export default sanityClient({ projectId: PROJECT_ID, dataset: DATASET_NAME, useCdn: true });
Am transmis useCdn
-ul nostru proiect, dataset name
și un projectId
boolean instanței clientului Sanity importat de la @sanity/client
. Acest lucru funcționează magic și conectează aplicația noastră la backend.
Acum că am finalizat conexiunea bidirecțională, să ne lansăm direct pentru a ne construi proiectul.
Configurarea și conectarea Redux la aplicația noastră
Vom avea nevoie de câteva dependențe pentru a lucra cu Redux în aplicația noastră React. Deschideți terminalul în mediul dvs. React și introduceți următoarele comenzi bash.
npm install redux react-redux redux-thunk
Redux este o bibliotecă globală de gestionare a stării care poate fi utilizată cu majoritatea cadrelor și bibliotecilor frontend, cum ar fi React. Cu toate acestea, avem nevoie de un instrument intermediar react-redux
pentru a permite comunicarea între magazinul nostru Redux și aplicația noastră React. Redux thunk ne va ajuta să returnăm o funcție în loc de un obiect de acțiune de la Redux.
Deși am putea scrie întregul flux de lucru Redux într-un singur fișier, este adesea mai ordonat și mai bine să ne separăm preocupările. Pentru aceasta, ne vom împărți fluxul de lucru în trei fișiere și anume, actions
, reducers
și apoi store
. Cu toate acestea, avem nevoie și de un fișier separat pentru a stoca action types
, cunoscute și sub numele de constants
.
Configurarea magazinului
Magazinul este cel mai important fișier din Redux. Acesta organizează și împachetează statele și le trimite către aplicația noastră React.
Iată configurarea inițială a magazinului nostru Redux necesară pentru a conecta fluxul nostru de lucru Redux.
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/"; export default createStore( reducers, applyMiddleware(thunk) );
Funcția createStore
din acest fișier ia trei parametri: reducer
(necesar), starea inițială și amplificatorul (de obicei un middleware, în acest caz, thunk
furnizat prin applyMiddleware
). Reductorii noștri vor fi stocați într-un folder de reducers
și le vom combina și exporta într-un fișier index.js
în folderul reducers
. Acesta este fișierul pe care l-am importat în codul de mai sus. Vom revizui acest fișier mai târziu.
Introducere în limbajul GROQ de la Sanity
Sanity duce interogarea datelor JSON cu un pas mai departe prin introducerea GROQ. GROQ înseamnă Interogări de obiecte grafice-relaționale. Potrivit Sanity.io, GROQ este un limbaj de interogare declarativ conceput pentru a interoga colecții de documente JSON în mare parte fără schemă.
Sanity oferă chiar și GROQ Playground pentru a ajuta dezvoltatorii să se familiarizeze cu limbajul. Cu toate acestea, pentru a accesa locul de joacă, trebuie să instalați Sanity Vision . Rulați sanity install @sanity/vision
pe terminalul dvs. pentru a-l instala.
GROQ are o sintaxă similară cu GraphQL, dar este mai condensată și mai ușor de citit. În plus, spre deosebire de GraphQL, GROQ poate fi folosit pentru a interoga datele JSON.
De exemplu, pentru a prelua fiecare articol din documentul nostru film, vom folosi următoarea sintaxă GROQ.
*[_type == "movie"]
Cu toate acestea, dacă dorim să recuperăm numai _ids
-urile și crewMembers
din documentul nostru de film. Trebuie să specificăm acele câmpuri după cum urmează.
`*[_type == 'movie']{ _id, crewMembers }
Aici, am folosit *
pentru a spune lui GROQ că vrem fiecare document de _type
. _type
este un atribut din colecția de filme. De asemenea, putem returna tipul așa cum am făcut pentru _id
și crewMembers
, după cum urmează:
*[_type == 'movie']{ _id, _type, crewMembers }
Vom lucra mai mult la GROQ prin implementarea acestuia în acțiunile noastre Redux, dar puteți verifica documentația Sanity.io pentru GROQ pentru a afla mai multe despre acesta. Foaia de cheat pentru interogări GROQ oferă o mulțime de exemple pentru a vă ajuta să stăpâniți limbajul de interogare.
Configurarea constantelor
Avem nevoie de constante pentru a urmări tipurile de acțiuni în fiecare etapă a fluxului de lucru Redux. Constantele ajută la determinarea tipului de acțiune trimisă în fiecare moment în timp. De exemplu, putem urmări când se încarcă API-ul, când se încarcă complet și când apare o eroare.
Nu trebuie neapărat să definim constante într-un fișier separat, dar pentru simplitate și claritate, aceasta este de obicei cea mai bună practică în Redux.
Prin convenție, constantele în Javascript sunt definite cu majuscule. Vom urma cele mai bune practici aici pentru a ne defini constantele. Iată un exemplu de constantă pentru indicarea cererilor de mutare a preluarii filmului.
export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST";
Aici, am creat o constantă MOVIE_FETCH_REQUEST
care denotă un tip de acțiune de MOVIE_FETCH_REQUEST
. Acest lucru ne ajută să apelăm cu ușurință acest tip de acțiune fără a folosi strings
și să evităm erorile. De asemenea, am exportat constanta pentru a fi disponibilă oriunde în proiectul nostru.
În mod similar, putem crea alte constante pentru preluarea tipurilor de acțiuni care indică când cererea reușește sau eșuează. Un cod complet pentru movieConstants.js
este dat în codul de mai jos.
export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST"; export const MOVIE_FETCH_SUCCESS = "MOVIE_FETCH_SUCCESS"; export const MOVIE_FETCH_FAIL = "MOVIE_FETCH_FAIL"; export const MOVIES_FETCH_REQUEST = "MOVIES_FETCH_REQUEST"; export const MOVIES_FETCH_SUCCESS = "MOVIES_FETCH_SUCCESS"; export const MOVIES_FETCH_FAIL = "MOVIES_FETCH_FAIL"; export const MOVIES_FETCH_RESET = "MOVIES_FETCH_RESET"; export const MOVIES_REF_FETCH_REQUEST = "MOVIES_REF_FETCH_REQUEST"; export const MOVIES_REF_FETCH_SUCCESS = "MOVIES_REF_FETCH_SUCCESS"; export const MOVIES_REF_FETCH_FAIL = "MOVIES_REF_FETCH_FAIL"; export const MOVIES_SORT_REQUEST = "MOVIES_SORT_REQUEST"; export const MOVIES_SORT_SUCCESS = "MOVIES_SORT_SUCCESS"; export const MOVIES_SORT_FAIL = "MOVIES_SORT_FAIL"; export const MOVIES_MOST_POPULAR_REQUEST = "MOVIES_MOST_POPULAR_REQUEST"; export const MOVIES_MOST_POPULAR_SUCCESS = "MOVIES_MOST_POPULAR_SUCCESS"; export const MOVIES_MOST_POPULAR_FAIL = "MOVIES_MOST_POPULAR_FAIL";
Aici am definit mai multe constante pentru preluarea unui film sau a unei liste de filme, sortarea și preluarea celor mai populare filme. Observați că am setat constante pentru a determina când cererea este loading
, successful
și failed
.
În mod similar, fișierul nostru personConstants.js
este prezentat mai jos:
export const PERSONS_FETCH_REQUEST = "PERSONS_FETCH_REQUEST"; export const PERSONS_FETCH_SUCCESS = "PERSONS_FETCH_SUCCESS"; export const PERSONS_FETCH_FAIL = "PERSONS_FETCH_FAIL"; export const PERSON_FETCH_REQUEST = "PERSON_FETCH_REQUEST"; export const PERSON_FETCH_SUCCESS = "PERSON_FETCH_SUCCESS"; export const PERSON_FETCH_FAIL = "PERSON_FETCH_FAIL"; export const PERSONS_COUNT = "PERSONS_COUNT";
La fel ca movieConstants.js
, am stabilit o listă de constante pentru preluarea unei persoane sau a unor persoane. De asemenea, am stabilit o constantă pentru numărarea persoanelor. Constantele urmează convenția descrisă pentru movieConstants.js
și, de asemenea, le-am exportat pentru a fi accesibile altor părți ale aplicației noastre.
În cele din urmă, vom implementa modul de lumină și întuneric în aplicație și astfel avem un alt fișier de constante globalConstants.js
. Să aruncăm o privire.
export const SET_LIGHT_THEME = "SET_LIGHT_THEME"; export const SET_DARK_THEME = "SET_DARK_THEME";
Aici setăm constante pentru a determina când este trimis modul de lumină sau întuneric. SET_LIGHT_THEME
determină când utilizatorul trece la tema luminoasă, iar SET_DARK_THEME
determină când este selectată tema întunecată. De asemenea, am exportat constantele noastre, așa cum se arată.
Configurarea acțiunilor
Prin convenție, acțiunile noastre sunt stocate într-un folder separat. Acțiunile sunt grupate în funcție de tipurile lor. De exemplu, acțiunile noastre din film sunt stocate în movieActions.js
, în timp ce acțiunile noastre personale sunt stocate în fișierul personActions.js
.
Avem, de asemenea, globalActions.js
pentru a se ocupa de comutarea temei de la modul deschis la modul întunecat.
Să preluăm toate filmele din moviesActions.js
.
import sanityAPI from "../../sanitySetup"; import { MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS } from "../constants/movieConstants"; const fetchAllMovies = () => async (dispatch) => { try { dispatch({ type: MOVIES_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster": poster.asset->url, } ` ); dispatch({ type: MOVIES_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_FETCH_FAIL, payload: error.message }); } };
Vă amintiți când am creat fișierul sanitySetup.js
pentru a conecta React la backend-ul nostru Sanity? Aici, am importat configurația pentru a ne permite să interogăm backend-ul nostru sanity folosind GROQ. De asemenea, am importat câteva constante exportate din fișierul movieConstants.js
din folderul constants
.
Apoi, am creat funcția de acțiune fetchAllMovies
pentru a prelua fiecare film din colecția noastră. Majoritatea aplicațiilor React tradiționale folosesc axios
sau fetch
pentru a prelua date din backend. Dar, deși am putea folosi oricare dintre acestea aici, folosim GROQ
de la Sanity. Pentru a intra în modul GROQ
, trebuie să apelăm funcția sanityAPI.fetch()
așa cum se arată în codul de mai sus. Aici, sanityAPI
este conexiunea React-Sanity pe care am configurat-o mai devreme. Aceasta returnează o Promise
și, prin urmare, trebuie apelată asincron. Am folosit aici sintaxa async-await
, dar putem folosi și sintaxa .then
.
Deoarece folosim thunk
în aplicația noastră, putem returna o funcție în loc de un obiect de acțiune. Cu toate acestea, am ales să transmitem instrucțiunea return într-o singură linie.
const fetchAllMovies = () => async (dispatch) => { ... }
Rețineți că putem scrie și funcția astfel:
const fetchAllMovies = () => { return async (dispatch)=>{ ... } }
În general, pentru a prelua toate filmele, am trimis mai întâi un tip de acțiune care urmărește când solicitarea încă se încarcă. Apoi am folosit sintaxa GROQ a lui Sanity pentru a interoga asincron documentul filmului. Am preluat _id
-ul și adresa URL a posterului datelor filmului. Apoi am returnat o sarcină utilă care conținea datele obținute din API.
În mod similar, putem prelua filme după _id
-ul lor, putem sorta filme și obținem cele mai populare filme.
De asemenea, putem prelua filme care se potrivesc cu referința unei anumite persoane. Am făcut acest lucru în funcția fetchMoviesByRef
.
const fetchMoviesByRef = (ref) => async (dispatch) => { try { dispatch({ type: MOVIES_REF_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && (castMembers[person._ref match '${ref}'] || crewMembers[person._ref match '${ref}']) ]{ _id, "poster" : poster.asset->url, title } ` ); dispatch({ type: MOVIES_REF_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_REF_FETCH_FAIL, payload: error.message }); } };
Această funcție preia un argument și verifică dacă person._ref
fie din castMembers
, fie din crewMembers
potrivește cu argumentul transmis. _id
-ul filmului, poster url
și title
alături. De asemenea, trimitem o acțiune de tip MOVIES_REF_FETCH_SUCCESS
, atașând o sarcină utilă a datelor returnate, iar dacă apare o eroare, trimitem o acțiune de tip MOVIE_REF_FETCH_FAIL
, atașând o încărcare utilă a mesajului de eroare, datorită wrapper-ului try-catch
.
În funcția fetchMovieById
, am folosit GROQ
pentru a prelua un film care se potrivește cu un anumit id
transmis funcției.
Sintaxa GROQ
pentru funcție este prezentată mai jos.
const data = await sanityAPI.fetch( `*[_type == 'movie' && _id == '${id}']{ _id, "cast" : castMembers[]{ "ref": person._ref, characterName, "name": person->name, "image": person->image.asset->url } , "crew" : crewMembers[]{ "ref": person._ref, department, job, "name": person->name, "image": person->image.asset->url } , "overview": { "text": overview[0].children[0].text }, popularity, "poster" : poster.asset->url, releaseDate, title }[0]` );
La fel ca și acțiunea fetchAllMovies
, am început prin a selecta toate documentele de tip movie
dar am mers mai departe pentru a le selecta doar pe cele cu un id furnizat funcției. Deoarece intenționăm să afișăm o mulțime de detalii pentru film, am specificat o grămadă de atribute de preluat.
Am preluat id
-ul filmului și, de asemenea, câteva atribute din matricea castMembers
și anume ref
, characterName
, numele persoanei și imaginea persoanei. De asemenea, am schimbat aliasul din castMembers
în cast
.
La fel ca și castMembers
, am selectat câteva atribute din matricea crewMembers
, și anume ref
, department
, job
, numele persoanei și imaginea persoanei. am schimbat, de asemenea, pseudonimul din crewMembers
echipajului în crew
.
În același mod, am selectat textul de prezentare generală, popularitatea, adresa URL a afișului filmului, data lansării filmului și titlul.
Limbajul GROQ al lui Sanity ne permite, de asemenea, să sortăm un document. Pentru a sorta un articol, trecem comanda lângă un operator de conducte .
De exemplu, dacă dorim să sortăm filmele după data de releaseDate
în ordine crescătoare, am putea face următoarele.
const data = await sanityAPI.fetch( `*[_type == 'movie']{ ... } | order(releaseDate, asc)` );
Am folosit această noțiune în funcția sortMoviesBy
pentru a sorta fie în ordine crescătoare, fie în ordine descrescătoare.
Să aruncăm o privire la această funcție de mai jos.
const sortMoviesBy = (item, type) => async (dispatch) => { try { dispatch({ type: MOVIES_SORT_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, title } | order( ${item} ${type})` ); dispatch({ type: MOVIES_SORT_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_SORT_FAIL, payload: error.message }); } };
Am început prin a trimite o acțiune de tip MOVIES_SORT_REQUEST
pentru a determina când se încarcă solicitarea. Apoi am folosit sintaxa GROQ
pentru a sorta și a prelua date din colecția de movie
. Elementul de sortat este furnizat în item
variabil, iar modul de sortare (crescător sau descrescător) este furnizat în type
de variabilă . În consecință, am returnat id
-ul , adresa URL a posterului și titlul. Odată ce datele sunt returnate, trimitem o acțiune de tip MOVIES_SORT_SUCCESS
și dacă eșuează, trimitem o acțiune de tip MOVIES_SORT_FAIL
.
Un concept GROQ
similar se aplică funcției getMostPopular
. Sintaxa GROQ
este prezentată mai jos.
const data = await sanityAPI.fetch( ` *[_type == 'movie']{ _id, "overview": { "text": overview[0].children[0].text }, "poster" : poster.asset->url, title }| order(popularity desc) [0..2]` );
Singura diferență aici este că am sortat filmele după popularitate în ordine descrescătoare și apoi le-am selectat doar pe primele trei. Elementele sunt returnate într-un index bazat pe zero și, prin urmare, primii trei elemente sunt elementele 0, 1 și 2. Dacă dorim să recuperăm primele zece elemente, am putea trece [0..9]
funcției.
Iată codul complet pentru acțiunile filmului din fișierul movieActions.js
.
import sanityAPI from "../../sanitySetup"; import { MOVIE_FETCH_FAIL, MOVIE_FETCH_REQUEST, MOVIE_FETCH_SUCCESS, MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, MOVIES_SORT_REQUEST, MOVIES_SORT_SUCCESS, MOVIES_SORT_FAIL, MOVIES_MOST_POPULAR_REQUEST, MOVIES_MOST_POPULAR_SUCCESS, MOVIES_MOST_POPULAR_FAIL, MOVIES_REF_FETCH_SUCCESS, MOVIES_REF_FETCH_FAIL, MOVIES_REF_FETCH_REQUEST } from "../constants/movieConstants"; const fetchAllMovies = () => async (dispatch) => { try { dispatch({ type: MOVIES_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, } ` ); dispatch({ type: MOVIES_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_FETCH_FAIL, payload: error.message }); } }; const fetchMoviesByRef = (ref) => async (dispatch) => { try { dispatch({ type: MOVIES_REF_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && (castMembers[person._ref match '${ref}'] || crewMembers[person._ref match '${ref}']) ]{ _id, "poster" : poster.asset->url, title }` ); dispatch({ type: MOVIES_REF_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_REF_FETCH_FAIL, payload: error.message }); } }; const fetchMovieById = (id) => async (dispatch) => { try { dispatch({ type: MOVIE_FETCH_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie' && _id == '${id}']{ _id, "cast" : castMembers[]{ "ref": person._ref, characterName, "name": person->name, "image": person->image.asset->url } , "crew" : crewMembers[]{ "ref": person._ref, department, job, "name": person->name, "image": person->image.asset->url } , "overview": { "text": overview[0].children[0].text }, popularity, "poster" : poster.asset->url, releaseDate, title }[0]` ); dispatch({ type: MOVIE_FETCH_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIE_FETCH_FAIL, payload: error.message }); } }; const sortMoviesBy = (item, type) => async (dispatch) => { try { dispatch({ type: MOVIES_MOST_POPULAR_REQUEST }); const data = await sanityAPI.fetch( `*[_type == 'movie']{ _id, "poster" : poster.asset->url, title } | order( ${item} ${type})` ); dispatch({ type: MOVIES_SORT_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_SORT_FAIL, payload: error.message }); } }; const getMostPopular = () => async (dispatch) => { try { dispatch({ type: MOVIES_SORT_REQUEST }); const data = await sanityAPI.fetch( ` *[_type == 'movie']{ _id, "overview": { "text": overview[0].children[0].text }, "poster" : poster.asset->url, title }| order(popularity desc) [0..2]` ); dispatch({ type: MOVIES_MOST_POPULAR_SUCCESS, payload: data }); } catch (error) { dispatch({ type: MOVIES_MOST_POPULAR_FAIL, payload: error.message }); } }; export { fetchAllMovies, fetchMovieById, sortMoviesBy, getMostPopular, fetchMoviesByRef };
Configurarea reductoarelor
Reductoarele sunt unul dintre cele mai importante concepte din Redux. Ei iau starea anterioară și determină schimbările de stare.
De obicei, vom folosi instrucțiunea switch pentru a executa o condiție pentru fiecare tip de acțiune. De exemplu, putem returna loading
când tipul de acțiune indică încărcare și apoi sarcina utilă când denotă succes sau eroare. Este de așteptat să ia în initial state
și action
ca argumente.
Fișierul nostru movieReducers.js
conține diverși reductori pentru a se potrivi cu acțiunile definite în fișierul movieActions.js
. Cu toate acestea, fiecare dintre reductori are o sintaxă și o structură similare. Singurele diferențe sunt constants
pe care le numesc și valorile pe care le returnează.
Să începem prin a arunca o privire la fetchAllMoviesReducer
din fișierul movieReducers.js
.
import { MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, } from "../constants/movieConstants"; const fetchAllMoviesReducer = (state = {}, action) => { switch (action.type) { case MOVIES_FETCH_REQUEST: return { loading: true }; case MOVIES_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_FETCH_FAIL: return { loading: false, error: action.payload }; case MOVIES_FETCH_RESET: return {}; default: return state; } };
La fel ca toate reductoarele, fetchAllMoviesReducer
ia ca argumente obiectul de stare inițială ( state
) și obiectul de action
. Am folosit instrucțiunea switch pentru a verifica tipurile de acțiuni la fiecare moment. Dacă corespunde MOVIES_FETCH_REQUEST
, returnăm încărcarea ca adevărată pentru a ne permite să arătăm utilizatorului un indicator de încărcare.
Dacă corespunde MOVIES_FETCH_SUCCESS
, dezactivăm indicatorul de încărcare și apoi returnăm sarcina utilă de acțiune într-un movies
variabil. Dar dacă este MOVIES_FETCH_FAIL
, oprim și încărcarea și apoi returnăm eroarea. Dorim și opțiunea de a ne reseta filmele. Acest lucru ne va permite să curățăm stările atunci când trebuie să facem acest lucru.
Avem aceeași structură pentru celelalte reductoare. movieReducers.js
complet este prezentat mai jos.
import { MOVIE_FETCH_FAIL, MOVIE_FETCH_REQUEST, MOVIE_FETCH_SUCCESS, MOVIES_FETCH_FAIL, MOVIES_FETCH_REQUEST, MOVIES_FETCH_SUCCESS, MOVIES_SORT_REQUEST, MOVIES_SORT_SUCCESS, MOVIES_SORT_FAIL, MOVIES_MOST_POPULAR_REQUEST, MOVIES_MOST_POPULAR_SUCCESS, MOVIES_MOST_POPULAR_FAIL, MOVIES_FETCH_RESET, MOVIES_REF_FETCH_REQUEST, MOVIES_REF_FETCH_SUCCESS, MOVIES_REF_FETCH_FAIL } from "../constants/movieConstants"; const fetchAllMoviesReducer = (state = {}, action) => { switch (action.type) { case MOVIES_FETCH_REQUEST: return { loading: true }; case MOVIES_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_FETCH_FAIL: return { loading: false, error: action.payload }; case MOVIES_FETCH_RESET: return {}; default: return state; } }; const fetchMoviesByRefReducer = (state = {}, action) => { switch (action.type) { case MOVIES_REF_FETCH_REQUEST: return { loading: true }; case MOVIES_REF_FETCH_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_REF_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const fetchMovieByIdReducer = (state = {}, action) => { switch (action.type) { case MOVIE_FETCH_REQUEST: return { loading: true }; case MOVIE_FETCH_SUCCESS: return { loading: false, movie: action.payload }; case MOVIE_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const sortMoviesByReducer = (state = {}, action) => { switch (action.type) { case MOVIES_SORT_REQUEST: return { loading: true }; case MOVIES_SORT_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_SORT_FAIL: return { loading: false, error: action.payload }; default: return state; } }; const getMostPopularReducer = (state = {}, action) => { switch (action.type) { case MOVIES_MOST_POPULAR_REQUEST: return { loading: true }; case MOVIES_MOST_POPULAR_SUCCESS: return { loading: false, movies: action.payload }; case MOVIES_MOST_POPULAR_FAIL: return { loading: false, error: action.payload }; default: return state; } }; export { fetchAllMoviesReducer, fetchMovieByIdReducer, sortMoviesByReducer, getMostPopularReducer, fetchMoviesByRefReducer };
De asemenea, am urmat exact aceeași structură pentru personReducers.js
. De exemplu, funcția fetchAllPersonsReducer
definește stările pentru preluarea tuturor persoanelor din baza de date.
Acest lucru este dat în codul de mai jos.
import { PERSONS_FETCH_FAIL, PERSONS_FETCH_REQUEST, PERSONS_FETCH_SUCCESS, } from "../constants/personConstants"; const fetchAllPersonsReducer = (state = {}, action) => { switch (action.type) { case PERSONS_FETCH_REQUEST: return { loading: true }; case PERSONS_FETCH_SUCCESS: return { loading: false, persons: action.payload }; case PERSONS_FETCH_FAIL: return { loading: false, error: action.payload }; default: return state; } };
La fel ca fetchAllMoviesReducer
, am definit fetchAllPersonsReducer
cu state
și action
ca argumente. Acestea sunt configurații standard pentru reductoarele Redux. Apoi am folosit instrucțiunea switch pentru a verifica tipurile de acțiuni și dacă este de tip PERSONS_FETCH_REQUEST
, returnăm încărcarea ca adevărată. Dacă este PERSONS_FETCH_SUCCESS
, oprim încărcarea și returnăm sarcina utilă, iar dacă este PERSONS_FETCH_FAIL
, returnăm eroarea.
Combinarea reductoarelor
Funcția combineReducers
de la Redux ne permite să combinăm mai mult de un reductor și să-l transmitem magazinului. Vom combina filmele și reducers
de persoane într-un fișier index.js
din folderul reduceri.
Să aruncăm o privire.
import { combineReducers } from "redux"; import { fetchAllMoviesReducer, fetchMovieByIdReducer, sortMoviesByReducer, getMostPopularReducer, fetchMoviesByRefReducer } from "./movieReducers"; import { fetchAllPersonsReducer, fetchPersonByIdReducer, countPersonsReducer } from "./personReducers"; import { toggleTheme } from "./globalReducers"; export default combineReducers({ fetchAllMoviesReducer, fetchMovieByIdReducer, fetchAllPersonsReducer, fetchPersonByIdReducer, sortMoviesByReducer, getMostPopularReducer, countPersonsReducer, fetchMoviesByRefReducer, toggleTheme });
Aici am importat toate reductoarele din fișierul filme, persoane și reductoare globale și le-am trecut la funcția combineReducers
. Funcția combineReducers
preia un obiect care ne permite să trecem toate reductoarele noastre. Putem chiar adăuga un alias la argumentele din proces.
Vom lucra la globalReducers
mai târziu.
Acum putem trece reductoarele în fișierul Redux store.js
. Acest lucru este prezentat mai jos.
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/index"; export default createStore(reducers, initialState, applyMiddleware(thunk));
După ce ne-am configurat fluxul de lucru Redux, haideți să configuram aplicația noastră React.
Configurarea aplicației noastre React
Aplicația noastră react va lista filmele și distribuția și membrii echipajului corespunzător. Vom folosi react-router-dom
pentru rutare și styled-components
pentru stilarea aplicației. Vom folosi, de asemenea, Material UI pentru pictograme și unele componente UI.
Introduceți următoarea comandă bash
pentru a instala dependențele.
npm install react-router-dom @material-ui/core @material-ui/icons query-string
Iată ce vom construi:
Conectarea Redux la aplicația noastră React
React-redux
livrat cu o funcție Provider care ne permite să ne conectăm aplicația la magazinul Redux. Pentru a face acest lucru, trebuie să transmitem o instanță a magazinului Furnizorului. Putem face acest lucru fie în fișierul nostru index.js
, fie în fișierul App.js
Iată fișierul nostru index.js.
import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import { Provider } from "react-redux"; import store from "./redux/store"; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById("root") );
Aici, am importat Provider
din react-redux
și store
din magazinul nostru Redux. Apoi am împachetat întregul nostru arbore de componente cu Furnizorul, pasând magazinul acestuia.
În continuare, avem nevoie react-router-dom
pentru rutare în aplicația noastră React. react-router-dom
vine cu BrowserRouter
, Switch
și Route
care pot fi folosite pentru a ne defini calea și rutele.
Facem acest lucru în fișierul nostru App.js
Acest lucru este prezentat mai jos.
import React from "react"; import Header from "./components/Header"; import Footer from "./components/Footer"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import MoviesList from "./pages/MoviesListPage"; import PersonsList from "./pages/PersonsListPage"; function App() { return ( <Router> <main className="contentwrap"> <Header /> <Switch> <Route path="/persons/"> <PersonsList /> </Route> <Route path="/" exact> <MoviesList /> </Route> </Switch> </main> <Footer /> </Router> ); } export default App;
Aceasta este o setare standard pentru rutarea cu react-router-dom. Îl puteți verifica în documentația lor. Am importat componentele noastre Header
, Footer
, PersonsList
și MovieList
. Apoi am configurat react-router-dom
prin împachetarea totul în Router
și Switch
.
Deoarece dorim ca paginile noastre să partajeze același antet și subsol, a trebuit să trecem componenta <Header />
și <Footer />
înainte de a încheia structura cu Switch
. De asemenea, am făcut un lucru similar cu elementul main
, deoarece dorim ca acesta să încapsuleze întreaga aplicație.
Am trecut fiecare componentă pe traseu folosind Route
from react-router-dom
.
Definirea paginilor și componentelor noastre
Aplicația noastră este organizată într-un mod structurat. Componentele reutilizabile sunt stocate în folderul de components
în timp ce Paginile sunt stocate în folderul de pages
.
pages
noastre includ movieListPage.js
, moviePage.js
, PersonListPage.js
și PersonPage.js
. MovieListPage.js
listează toate filmele din backend-ul nostru Sanity.io, precum și cele mai populare filme.
Pentru a lista toate filmele, pur și simplu dispatch
acțiunea fetchAllMovies
definită în fișierul nostru movieAction.js
. Deoarece trebuie să preluăm lista de îndată ce pagina se încarcă, trebuie să o definim în useEffect
. Acest lucru este prezentat mai jos.
import React, { useEffect } from "react"; import { fetchAllMovies } from "../redux/actions/movieActions"; import { useDispatch, useSelector } from "react-redux"; const MoviesListPage = () => { const dispatch = useDispatch(); useEffect(() => { dispatch(fetchAllMovies()); }, [dispatch]); const { loading, error, movies } = useSelector( (state) => state.fetchAllMoviesReducer ); return ( ... ) }; export default MoviesListPage;
Datorită useDispatch
și useSelector
Hooks, putem trimite acțiuni Redux și selecta stările corespunzătoare din magazinul Redux. Observați că stările de loading
, error
și movies
au fost definite în funcțiile noastre Reducer și aici le-am selectat folosind Hook useSelector
de la React Redux. Aceste stări și anume loading
, error
și movies
devin disponibile imediat când am trimis acțiunile fetchAllMovies()
.
Odată ce obținem lista de filme, o putem afișa în aplicația noastră folosind funcția de map
sau cum ne dorim.
Iată codul complet pentru fișierul moviesListPage.js
.
import React, {useState, useEffect} from 'react' import {fetchAllMovies, getMostPopular, sortMoviesBy} from "../redux/actions/movieActions" import {useDispatch, useSelector} from "react-redux" import Loader from "../components/BackdropLoader" import {MovieListContainer} from "../styles/MovieStyles.js" import SortIcon from '@material-ui/icons/Sort'; import SortModal from "../components/Modal" import {useLocation, Link} from "react-router-dom" import queryString from "query-string" import {MOVIES_FETCH_RESET} from "../redux/constants/movieConstants" const MoviesListPage = () => { const location = useLocation() const dispatch = useDispatch() const [openSort, setOpenSort] = useState(false) useEffect(()=>{ dispatch(getMostPopular()) const {order, type} = queryString.parse(location.search) if(order && type){ dispatch({ type: MOVIES_FETCH_RESET }) dispatch(sortMoviesBy(order, type)) }else{ dispatch(fetchAllMovies()) } }, [dispatch, location.search]) const {loading: popularLoading, error: popularError, movies: popularMovies } = useSelector(state => state.getMostPopularReducer) const { loading: moviesLoading, error: moviesError, movies } = useSelector(state => state.fetchAllMoviesReducer) const { loading: sortLoading, error: sortError, movies: sortMovies } = useSelector(state => state.sortMoviesByReducer) return ( <MovieListContainer> <div className="mostpopular"> { popularLoading ? <Loader /> : popularError ? popularError : popularMovies && popularMovies.map(movie => ( <Link to={`/movie?id=${movie._id}`} className="popular" key={movie._id} style={{backgroundImage: `url(${movie.poster})`}}> <div className="content"> <h2>{movie.title}</h2> <p>{movie.overview.text.substring(0, 50)}…</p> </div> </Link> )) } </div> <div className="moviespanel"> <div className="top"> <h2>All Movies</h2> <SortIcon onClick={()=> setOpenSort(true)} /> </div> <div className="movieslist"> { moviesLoading ? <Loader /> : moviesError ? moviesError : movies && movies.map(movie =>( <Link to={`/movie?id=${movie._id}`} key={movie._id}> <img className="movie" src={movie.poster} alt={movie.title} /> </Link> )) } { ( sortLoading ? !movies && <Loader /> : sortError ? sortError : sortMovies && sortMovies.map(movie =>( <Link to={`/movie?id=${movie._id}`} key={movie._id}> <img className="movie" src={movie.poster} alt={movie.title} /> </Link> )) ) } </div> </div> <SortModal open={openSort} setOpen={setOpenSort} /> </MovieListContainer> ) } export default MoviesListPage
Am început prin a trimite acțiunea getMostPopular
movies (această acțiune selectează filmele cu cea mai mare popularitate) în useEffect
Hook. Acest lucru ne permite să recuperăm cele mai populare filme imediat ce pagina se încarcă. În plus, le-am permis utilizatorilor să sorteze filmele după releaseDate
și popularity
. Acest lucru este gestionat de acțiunea sortMoviesBy
trimisă în codul de mai sus. În plus, am expediat fetchAllMovies
în funcție de parametrii de interogare.
De asemenea, am folosit useSelector
Hook pentru a selecta reductoarele corespunzătoare pentru fiecare dintre aceste acțiuni. Am selectat stările de loading
, error
și movies
pentru fiecare dintre reductoare.
După ce obținem movies
de la reductoare, acum le putem afișa utilizatorului. Aici, am folosit funcția de map
ES6 pentru a face acest lucru. Mai întâi am afișat un încărcător ori de câte ori se încarcă fiecare dintre stările filmului și, dacă există o eroare, afișăm mesajul de eroare. În cele din urmă, dacă obținem un film, afișăm imaginea filmului utilizatorului folosind funcția de map
. Am împachetat întreaga componentă într-o componentă MovieListContainer
.
Eticheta <MovieListContainer> … </MovieListContainer>
este un div
definit folosind componente cu stil. Vom arunca o scurtă privire la asta în curând.
Stilizarea aplicației noastre cu componente stilizate
Componentele stilizate ne permit să ne stilăm paginile și componentele în mod individual. De asemenea, oferă câteva caracteristici interesante, cum ar fi inheritance
, Theming
, passing of props
etc.
Deși dorim întotdeauna să ne stilăm paginile în mod individual, uneori poate fi de dorit un stil global. În mod interesant, componentele cu stil oferă o modalitate de a face acest lucru, datorită funcției createGlobalStyle
.
Pentru a folosi componente cu stil în aplicația noastră, trebuie să o instalăm. Deschideți terminalul în proiectul dumneavoastră react și introduceți următoarea comandă bash
.
npm install styled-components
După ce au instalat componentele stilizate, să începem cu stilurile noastre globale.
Să creăm un folder separat în directorul nostru src
numit styles
. Aceasta va stoca toate stilurile noastre. Să creăm, de asemenea, un fișier globalStyles.js
în folderul stiluri. Pentru a crea un stil global în componentele cu stil, trebuie să importam createGlobalStyle
.
import { createGlobalStyle } from "styled-components";
Ne putem defini apoi stilurile după cum urmează:
export const GlobalStyle = createGlobalStyle` ... `
Componentele stilizate folosesc literalul șablon pentru a defini elementele de recuzită. În acest literal, putem scrie codurile noastre tradiționale CSS
.
De asemenea, am importat deviceWidth
definit într-un fișier numit definition.js
. deviceWidth
deține definiția punctelor de întrerupere pentru setarea interogărilor noastre media.
import { deviceWidth } from "./definition";
Am setat overflow la ascuns pentru a controla fluxul aplicației noastre.
html, body{ overflow-x: hidden; }
De asemenea, am definit stilul antetului folosind selectorul de stil .header
.
.header{ z-index: 5; background-color: ${(props)=>props.theme.midDarkBlue}; display:flex; align-items:center; padding: 0 20px; height:50px; justify-content:space-between; position:fixed; top:0; width:100%; @media ${deviceWidth.laptop_lg} { width:97%; } ... }
Aici, sunt definite diferite stiluri, cum ar fi culoarea de fundal, z-index, padding și multe alte proprietăți CSS tradiționale.
Am folosit elementele de props
pentru a seta culoarea de fundal. Acest lucru ne permite să setăm variabile dinamice care pot fi transmise de la componenta noastră. Mai mult, am trecut și variabila temei pentru a ne permite să profităm la maximum de comutarea temei.
Tematica este posibilă aici, deoarece am împachetat întreaga noastră aplicație cu ThemeProvider
din componentele stilate. Vom vorbi despre asta într-o clipă. În plus, am folosit CSS flexbox
pentru a stila corect antetul nostru și pentru a seta poziția la fixed
pentru a ne asigura că rămâne fix în raport cu browserul. Am definit, de asemenea, punctele de întrerupere pentru a face anteturile prietenoase cu dispozitivele mobile.
Iată codul complet pentru fișierul nostru globalStyles.js
.
import { createGlobalStyle } from "styled-components"; import { deviceWidth } from "./definition"; export const GlobalStyle = createGlobalStyle` html{ overflow-x: hidden; } body{ background-color: ${(props) => props.theme.lighter}; overflow-x: hidden; min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; } #root{ display: grid; flex-direction: column; } h1,h2,h3, label{ font-family: 'Aclonica', sans-serif; } h1, h2, h3, p, span:not(.MuiIconButton-label), div:not(.PrivateRadioButtonIcon-root-8), div:not(.tryingthis){ color: ${(props) => props.theme.bodyText} } p, span, div, input{ font-family: 'Jost', sans-serif; } .paginate button{ color: ${(props) => props.theme.bodyText} } .header{ z-index: 5; background-color: ${(props) => props.theme.midDarkBlue}; display: flex; align-items: center; padding: 0 20px; height: 50px; justify-content: space-between; position: fixed; top: 0; width: 100%; @media ${deviceWidth.laptop_lg}{ width: 97%; } @media ${deviceWidth.tablet}{ width: 100%; justify-content: space-around; } a{ text-decoration: none; } label{ cursor: pointer; color: ${(props) => props.theme.goldish}; font-size: 1.5rem; } .hamburger{ cursor: pointer; color: ${(props) => props.theme.white}; @media ${deviceWidth.desktop}{ display: none; } @media ${deviceWidth.tablet}{ display: block; } } } .mobileHeader{ z-index: 5; background-color: ${(props) => props.theme.darkBlue}; color: ${(props) => props.theme.white}; display: grid; place-items: center; width: 100%; @media ${deviceWidth.tablet}{ width: 100%; } height: calc(100% - 50px); transition: all 0.5s ease-in-out; position: fixed; right: 0; top: 50px; .menuitems{ display: flex; box-shadow: 0 0 5px ${(props) => props.theme.lightshadowtheme}; flex-direction: column; align-items: center; justify-content: space-around; height: 60%; width: 40%; a{ display: flex; flex-direction: column; align-items:center; cursor: pointer; color: ${(props) => props.theme.white}; text-decoration: none; &:hover{ border-bottom: 2px solid ${(props) => props.theme.goldish}; .MuiSvgIcon-root{ color: ${(props) => props.theme.lightred} } } } } } footer{ min-height: 30px; margin-top: auto; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 0.875rem; background-color: ${(props) => props.theme.midDarkBlue}; color: ${(props) => props.theme.white}; } `;
Observați că am scris cod CSS pur în literal, dar există câteva excepții. Componentele stilizate ne permit să trecem recuzită. Puteți afla mai multe despre acest lucru în documentație.
Pe lângă definirea stilurilor globale, putem defini stiluri pentru pagini individuale.
De exemplu, aici este stilul pentru PersonListPage.js
definit în PersonStyle.js
în folderul de styles
.
import styled from "styled-components"; import { deviceWidth, colors } from "./definition"; export const PersonsListContainer = styled.div` margin: 50px 80px; @media ${deviceWidth.tablet} { margin: 50px 10px; } a { text-decoration: none; } .top { display: flex; justify-content: flex-end; padding: 5px; .MuiSvgIcon-root { cursor: pointer; &:hover { color: ${colors.darkred}; } } } .personslist { margin-top: 20px; display: grid; place-items: center; grid-template-columns: repeat(5, 1fr); @media ${deviceWidth.laptop} { grid-template-columns: repeat(4, 1fr); } @media ${deviceWidth.tablet} { grid-template-columns: repeat(3, 1fr); } @media ${deviceWidth.tablet_md} { grid-template-columns: repeat(2, 1fr); } @media ${deviceWidth.mobile_lg} { grid-template-columns: repeat(1, 1fr); } grid-gap: 30px; .person { width: 200px; position: relative; img { width: 100%; } .content { position: absolute; bottom: 0; left: 8px; border-right: 2px solid ${colors.goldish}; border-left: 2px solid ${colors.goldish}; border-radius: 10px; width: 80%; margin: 20px auto; padding: 8px 10px; background-color: ${colors.transparentWhite}; color: ${colors.darkBlue}; h2 { font-size: 1.2rem; } } } } `;
Mai întâi am importat styled
din styled-components
stilate și deviceWidth
din fișierul de definition
. Apoi am definit PersonsListContainer
ca un div
pentru a ne păstra stilurile. Folosind interogări media și punctele de întrerupere stabilite, am făcut pagina prietenoasă pentru dispozitive mobile prin setarea diferitelor puncte de întrerupere.
Aici, am folosit doar punctele de întrerupere standard ale browserului pentru ecrane mici, mari și foarte mari. De asemenea, am profitat la maximum de caseta flexibilă și grilă CSS pentru a stila și afișa corect conținutul nostru pe pagină.
Pentru a folosi acest stil în fișierul nostru PersonListPage.js
, pur și simplu l-am importat și l-am adăugat la pagina noastră, după cum urmează.
import React from "react"; const PersonsListPage = () => { return ( <PersonsListContainer> ... </PersonsListContainer> ); }; export default PersonsListPage;
Wrapper-ul va scoate un div
deoarece l-am definit ca div în stilurile noastre.
Adăugarea temelor și împachetarea acesteia
Este întotdeauna o caracteristică grozavă să adăugați teme la aplicația noastră. Pentru aceasta, avem nevoie de următoarele:
- Temele noastre personalizate sunt definite într-un fișier separat (în cazul nostru, fișierul
definition.js
). - Logica definită în acțiunile și reductoarele noastre Redux.
- Apelarea temei noastre în aplicația noastră și trecerea ei prin arborele de componente.
Să verificăm asta.
Iată obiectul nostru theme
din fișierul definition.js
.
export const theme = { light: { dark: "#0B0C10", darkBlue: "#253858", midDarkBlue: "#42526e", lightBlue: "#0065ff", normal: "#dcdcdd", lighter: "#F4F5F7", white: "#FFFFFF", darkred: "#E85A4F", lightred: "#E98074", goldish: "#FFC400", bodyText: "#0B0C10", lightshadowtheme: "rgba(0, 0, 0, 0.1)" }, dark: { dark: "white", darkBlue: "#06090F", midDarkBlue: "#161B22", normal: "#dcdcdd", lighter: "#06090F", white: "white", darkred: "#E85A4F", lightred: "#E98074", goldish: "#FFC400", bodyText: "white", lightshadowtheme: "rgba(255, 255, 255, 0.9)" } };
Am adăugat diverse proprietăți de culoare pentru temele deschise și întunecate. Culorile sunt alese cu grijă pentru a permite vizibilitatea atât în modul deschis, cât și în modul întunecat. Puteți defini temele după cum doriți. Aceasta nu este o regulă strictă și rapidă.
Apoi, să adăugăm funcționalitatea la Redux.
Am creat globalActions.js
în folderul nostru de acțiuni Redux și am adăugat următoarele coduri.
import { SET_DARK_THEME, SET_LIGHT_THEME } from "../constants/globalConstants"; import { theme } from "../../styles/definition"; export const switchToLightTheme = () => (dispatch) => { dispatch({ type: SET_LIGHT_THEME, payload: theme.light }); localStorage.setItem("theme", JSON.stringify(theme.light)); localStorage.setItem("light", JSON.stringify(true)); }; export const switchToDarkTheme = () => (dispatch) => { dispatch({ type: SET_DARK_THEME, payload: theme.dark }); localStorage.setItem("theme", JSON.stringify(theme.dark)); localStorage.setItem("light", JSON.stringify(false)); };
Aici, am importat pur și simplu temele noastre definite. Am expediat acțiunile corespunzătoare, trecând încărcătura utilă a temelor de care aveam nevoie. Rezultatele încărcăturii sunt stocate în stocarea locală folosind aceleași taste atât pentru temele deschise, cât și pentru cele întunecate. Acest lucru ne permite să persistăm stările în browser.
De asemenea, trebuie să ne definim reductorul pentru teme.
import { SET_DARK_THEME, SET_LIGHT_THEME } from "../constants/globalConstants"; export const toggleTheme = (state = {}, action) => { switch (action.type) { case SET_LIGHT_THEME: return { theme: action.payload, light: true }; case SET_DARK_THEME: return { theme: action.payload, light: false }; default: return state; } };
Este foarte asemănător cu ceea ce am făcut noi. Am folosit instrucțiunea switch
pentru a verifica tipul de acțiune și apoi am returnat payload
corespunzătoare. De asemenea, am returnat o light
stare care determină dacă tema deschisă sau întunecată este selectată de utilizator. Vom folosi acest lucru în componentele noastre.
De asemenea, trebuie să-l adăugăm la reductorul nostru de rădăcină și să-l depozităm. Iată codul complet pentru store.js
.
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import { theme as initialTheme } from "../styles/definition"; import reducers from "./reducers/index"; const theme = localStorage.getItem("theme") ? JSON.parse(localStorage.getItem("theme")) : initialTheme.light; const light = localStorage.getItem("light") ? JSON.parse(localStorage.getItem("light")) : true; const initialState = { toggleTheme: { light, theme } }; export default createStore(reducers, initialState, applyMiddleware(thunk));
Deoarece trebuia să persistăm tema atunci când utilizatorul se reîmprospătează, a trebuit să o obținem din stocarea locală folosind localStorage.getItem()
și să o transmitem în starea noastră inițială.
Adăugarea de funcționalitate la aplicația noastră React
Componentele stilizate ne oferă ThemeProvider
care ne permite să trecem teme prin aplicația noastră. Putem modifica fișierul nostru App.js pentru a adăuga această funcționalitate.
Să aruncăm o privire.
import React from "react"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import { useSelector } from "react-redux"; import { ThemeProvider } from "styled-components"; function App() { const { theme } = useSelector((state) => state.toggleTheme); let Theme = theme ? theme : {}; return ( <ThemeProvider theme={Theme}> <Router> ... </Router> </ThemeProvider> ); } export default App;
Prin trecerea temelor prin ThemeProvider
, putem folosi cu ușurință elementele de recuzită a temei în stilurile noastre.
De exemplu, putem seta culoarea la culoarea personalizată bodyText
, după cum urmează.
color: ${(props) => props.theme.bodyText};
Putem folosi temele personalizate oriunde avem nevoie de culoare în aplicația noastră.
De exemplu, pentru a defini border-bottom
, facem următoarele.
border-bottom: 2px solid ${(props) => props.theme.goldish};
Concluzie
Am început prin a explora Sanity.io, a-l configura și a-l conecta la aplicația noastră React. Apoi am configurat Redux și am folosit limbajul GROQ pentru a interoga API-ul nostru. Am văzut cum să ne conectăm și să folosim Redux la aplicația noastră React folosind react-redux
, folosim componente cu stil și tematică.
Cu toate acestea, am zgâriat suprafața doar pe ceea ce este posibil cu aceste tehnologii. Vă încurajez să parcurgeți exemplele de cod din depozitul meu GitHub și să încercați un proiect complet diferit folosind aceste tehnologii pentru a le învăța și a le stăpâni.
Resurse
- Documentația Sanity
- Cum să construiți un blog cu Sanity.io de Kapehe
- Documentație Redux
- Documentația componentelor stilizate
- GROQ Cheat Sheet
- Documentația materialului UI
- Redux Middleware și SideEffects
- Documentația Redux Thunk