使用 React、Redux 和 Sanity.io 構建 Web 應用程序
已發表: 2022-03-10數字平台的快速發展對 Wordpress 等傳統 CMS 造成了嚴重限制。 這些平台是耦合的、不靈活的,並且專注於項目,而不是產品。 值得慶幸的是,已經開發了幾個無頭 CMS 來應對這些挑戰等等。
與傳統的 CMS 不同,無頭 CMS 可以被描述為軟件即服務 (SaaS),可用於開發網站、移動應用程序、數字顯示器等。 它們可以在無限的平台上使用。 如果您正在尋找獨立於平台、開發人員優先並提供跨平台支持的 CMS,那麼您無需再尋找無頭 CMS。
無頭 CMS 只是沒有頭的 CMS。 這裡的head
是指前端或表示層,而body
是指後端或內容存儲庫。 這提供了許多有趣的好處。 例如,它允許開發人員選擇他選擇的任何前端,您還可以根據需要設計表示層。
有很多無頭 CMS,其中一些最受歡迎的包括 Strapi、Contentful、Contentstack、Sanity、Butter CMS、Prismic、Storyblok、Directus 等。這些無頭 CMS 是基於 API 的,並且有各自的優勢。 例如,像 Sanity、Strapi、Contentful 和 Storyblok 這樣的 CMS 對小型項目是免費的。
這些無頭 CMS 也基於不同的技術堆棧。 Sanity.io 基於 React.js,Storyblok 基於 Vue.js。 作為一名 React 開發人員,這是我很快對 Sanity 產生興趣的主要原因。 然而,作為一個無頭 CMS,這些平台中的每一個都可以插入任何前端,無論是 Angular、Vue 還是 React。
這些無頭 CMS 中的每一個都有免費和付費計劃,這意味著價格大幅上漲。 儘管這些付費計劃提供了更多功能,但您不想為中小型項目支付那麼多費用。 Sanity 試圖通過引入現收現付選項來解決這個問題。 使用這些選項,您將能夠為使用的東西付費並避免價格上漲。
我選擇 Sanity.io 的另一個原因是他們的 GROQ 語言。 對我來說,Sanity 通過提供這個工具在人群中脫穎而出。 圖形關係對象查詢 (GROQ) 減少了開發時間,幫助您以所需的形式獲得所需的內容,還幫助開發人員創建具有新內容模型的文檔而無需更改代碼。
此外,開發人員不受 GROQ 語言的限制。 您還可以使用 GraphQL 甚至傳統的axios
並在您的 React 應用程序中fetch
來查詢後端。 與大多數其他無頭 CMS 一樣,Sanity 擁有全面的文檔,其中包含在平台上構建的有用提示。
注意:本文需要對 React、Redux 和 CSS 有基本的了解。
Sanity.io 入門
要在您的機器中使用 Sanity,您需要安裝 Sanity CLI 工具。 雖然這可以在您的項目中本地安裝,但最好全局安裝它以使其可供任何未來的應用程序訪問。
為此,請在終端中輸入以下命令。
npm install -g @sanity/cli
上述命令中的-g
標誌啟用全局安裝。
接下來,我們需要在我們的應用程序中初始化 Sanity。 雖然這可以作為一個單獨的項目安裝,但通常最好將它安裝在您的前端應用程序中(在本例中為 React)。
Kapehe 在她的博客中詳細解釋瞭如何將 Sanity 與 React 集成。 在繼續本教程之前通讀這篇文章會很有幫助。
輸入以下命令以在您的 React 應用程序中初始化 Sanity。
sanity init
當我們安裝了 Sanity CLI 工具後,我們就可以使用sanity
命令了。 您可以通過在終端中鍵入sanity
或sanity help
來查看可用的 Sanity 命令列表。
設置或初始化項目時,您需要按照提示對其進行自定義。 您還需要創建數據集,甚至可以選擇填充數據的自定義數據集。 對於這個列表應用程序,我們將使用 Sanity 的自定義科幻電影數據集。 這將使我們免於自己輸入數據。
要查看和編輯您的數據集,請cd
到終端中的 Sanity 子目錄並輸入sanity start
。 這通常在https://localhost:3333/
上運行。 您可能需要登錄才能訪問該界面(確保您使用初始化項目時使用的相同帳戶登錄)。 環境截圖如下所示。
Sanity-React 雙向通信
Sanity 和 React 需要相互通信才能實現功能齊全的應用程序。
理智管理器中的 CORS 起源設置
我們將首先將我們的 React 應用程序連接到 Sanity。 為此,請登錄https://manage.sanity.io/
並在Settings
選項卡的API Settings
下找到CORS origins
。 在這裡,您需要將前端來源連接到 Sanity 後端。 我們的 React 應用程序默認在https://localhost:3000/
上運行,因此我們需要將其添加到 CORS。
如下圖所示。
將理智與反應聯繫起來
Sanity 將project ID
與您創建的每個項目相關聯。 將其連接到前端應用程序時需要此 ID。 您可以在Sanity Manager 中找到項目 ID。
後端使用稱為sanity client
的庫與 React 通信。 您需要通過輸入以下命令在您的 Sanity 項目中安裝此庫。
npm install @sanity/client
在您的項目src
文件夾中創建一個文件sanitySetup.js
(文件名無關緊要)並輸入以下 React 代碼以建立 Sanity 和 React 之間的連接。
import sanityClient from "@sanity/client" export default sanityClient({ projectId: PROJECT_ID, dataset: DATASET_NAME, useCdn: true });
我們將projectId
、 dataset name
和布爾值useCdn
給從@sanity/client
導入的 sanity 客戶端實例。 這很神奇,並將我們的應用程序連接到後端。
現在我們已經完成了雙向連接,讓我們直接開始構建我們的項目。
設置 Redux 並將其連接到我們的應用程序
我們需要一些依賴項才能在我們的 React 應用程序中使用 Redux。 在 React 環境中打開終端並輸入以下 bash 命令。
npm install redux react-redux redux-thunk
Redux 是一個全局狀態管理庫,可以與大多數前端框架和庫(如 React)一起使用。 但是,我們需要一個中間工具react-redux
來實現我們的Redux 存儲和我們的 React 應用程序之間的通信。 Redux thunk將幫助我們從 Redux 返回一個函數而不是一個動作對象。
雖然我們可以將整個 Redux 工作流程編寫在一個文件中,但將我們的關注點分開通常更簡潔、更好。 為此,我們將工作流程分為三個文件,即actions
、 reducers
和store
。 但是,我們還需要一個單獨的文件來存儲action types
,也稱為constants
。
設置商店
store 是 Redux 中最重要的文件。 它組織和打包狀態並將它們發送到我們的 React 應用程序。
這是連接我們的 Redux 工作流程所需的 Redux 存儲的初始設置。
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/"; export default createStore( reducers, applyMiddleware(thunk) );
此文件中的createStore
函數採用三個參數: reducer
(必需)、初始狀態和增強器(通常是中間件,在這種情況下,通過applyMiddleware
提供thunk
)。 我們的 reducer 將存儲在reducers
文件夾中,我們會將它們組合併導出到reducers
文件夾中的index.js
文件中。 這是我們在上面的代碼中導入的文件。 我們稍後會重新訪問這個文件。
Sanity 的 GROQ 語言簡介
Sanity 通過引入 GROQ 將查詢 JSON 數據更進一步。 GROQ 代表圖關係對象查詢。 根據 Sanity.io 的說法,GROQ 是一種聲明式查詢語言,旨在查詢大部分無模式 JSON 文檔的集合。
Sanity 甚至提供了GROQ Playground來幫助開發人員熟悉該語言。 但是,要進入操場,您需要安裝sanity vision 。 在終端上運行sanity install @sanity/vision
進行安裝。
GROQ 的語法與 GraphQL 相似,但更簡潔且更易於閱讀。 此外,與 GraphQL 不同,GROQ 可用於查詢 JSON 數據。
例如,要檢索電影文檔中的每個項目,我們將使用以下 GROQ 語法。
*[_type == "movie"]
但是,如果我們希望僅檢索電影文檔中的_ids
和crewMembers
。 我們需要按如下方式指定這些字段。
`*[_type == 'movie']{ _id, crewMembers }
在這裡,我們使用*
告訴 GROQ 我們想要_type
電影的每個文檔。 _type
是電影集合下的一個屬性。 我們也可以像_id
和crewMembers
一樣返回類型,如下所示:
*[_type == 'movie']{ _id, _type, crewMembers }
我們將通過在 Redux 操作中實現 GROQ 來更多地工作,但您可以查看 Sanity.io 的 GROQ 文檔以了解更多信息。 GROQ 查詢備忘單提供了大量示例來幫助您掌握查詢語言。
設置常量
我們需要常量來跟踪 Redux 工作流程每個階段的操作類型。 常量有助於確定在每個時間點調度的操作類型。 例如,我們可以跟踪 API 何時加載、完全加載以及何時發生錯誤。
我們不一定需要在單獨的文件中定義常量,但為了簡單明了,這通常是 Redux 中的最佳實踐。
按照慣例,Javascript 中的常量用大寫字母定義。 我們將遵循此處的最佳實踐來定義我們的常量。 下面是一個常量示例,用於表示獲取移動電影的請求。
export const MOVIE_FETCH_REQUEST = "MOVIE_FETCH_REQUEST";
在這裡,我們創建了一個常量MOVIE_FETCH_REQUEST
來表示動作類型MOVIE_FETCH_REQUEST
。 這有助於我們在不使用strings
的情況下輕鬆調用此操作類型並避免錯誤。 我們還導出了常量,以便在我們項目的任何地方都可用。
同樣,我們可以創建其他常量來獲取表示請求成功或失敗的動作類型。 movieConstants.js
的完整代碼在下面的代碼中給出。
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";
在這裡,我們定義了幾個常量,用於獲取電影或電影列表、排序和獲取最受歡迎的電影。 請注意,我們設置常量來確定請求何時loading
、 successful
和failed
。
同樣,我們的personConstants.js
文件如下所示:
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";
像movieConstants.js
一樣,我們設置了一個常量列表來獲取一個或多個人。 我們還設置了一個常數來計算人數。 常量遵循為movieConstants.js
描述的約定,我們還將它們導出為可供應用程序的其他部分訪問。
最後,我們將在應用程序中實現明暗模式,因此我們有另一個常量文件globalConstants.js
。 讓我們來看看它。
export const SET_LIGHT_THEME = "SET_LIGHT_THEME"; export const SET_DARK_THEME = "SET_DARK_THEME";
在這裡,我們設置常量來確定何時分派亮模式或暗模式。 SET_LIGHT_THEME
確定用戶何時切換到淺色主題,而SET_DARK_THEME
確定何時選擇深色主題。 如圖所示,我們還導出了常量。
設置動作
按照慣例,我們的操作存儲在一個單獨的文件夾中。 動作根據其類型進行分組。 例如,我們的電影動作存儲在movieActions.js
中,而我們的人物動作存儲在personActions.js
文件中。
我們還有globalActions.js
來負責將主題從淺色模式切換到深色模式。
讓我們獲取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 }); } };
還記得我們創建sanitySetup.js
文件以將 React 連接到我們的 Sanity 後端時嗎? 在這裡,我們導入了設置,使我們能夠使用 GROQ 查詢健全的後端。 我們還導入了一些從常量文件夾中的movieConstants.js
文件導出的constants
。
接下來,我們創建了fetchAllMovies
動作函數來獲取集合中的每一部電影。 大多數傳統的 React 應用程序使用axios
或fetch
從後端獲取數據。 但是,雖然我們可以在這裡使用任何這些,但我們使用的是 Sanity 的GROQ
。 要進入GROQ
模式,我們需要調用sanityAPI.fetch()
函數,如上面的代碼所示。 在這裡, sanityAPI
是我們之前設置的 React-Sanity 連接。 這將返回一個Promise
,因此必須異步調用它。 我們在這裡使用了async-await
語法,但我們也可以使用.then
語法。
由於我們在應用程序中使用了thunk
,我們可以返回一個函數而不是一個動作對象。 但是,我們選擇在一行中傳遞 return 語句。
const fetchAllMovies = () => async (dispatch) => { ... }
請注意,我們也可以這樣編寫函數:
const fetchAllMovies = () => { return async (dispatch)=>{ ... } }
一般來說,為了獲取所有電影,我們首先分派了一個動作類型來跟踪請求仍在加載的時間。 然後我們使用 Sanity 的 GROQ 語法來異步查詢電影文檔。 我們檢索了電影數據的_id
和海報 url。 然後我們返回一個包含從 API 獲取的數據的有效負載。
同樣,我們可以通過電影的_id
檢索電影,對電影進行排序,並獲得最受歡迎的電影。
我們還可以獲取與特定人的參考相匹配的電影。 我們在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 }); } };
此函數接受一個參數並檢查castMembers
或person._ref
中的 person._ref crewMembers
與傳遞的參數匹配。 我們將電影_id
、 poster url
和title
一起返回。 我們還分派了一個MOVIES_REF_FETCH_SUCCESS
類型的動作,附加返回數據的有效負載,如果發生錯誤,我們分派一個MOVIE_REF_FETCH_FAIL
類型的動作,附加錯誤消息的有效負載,這要歸功於try-catch
包裝器。
在fetchMovieById
函數中,我們使用GROQ
檢索與傳遞給函數的特定id
匹配的電影。
該函數的GROQ
語法如下所示。
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]` );
與fetchAllMovies
動作一樣,我們首先選擇所有類型為movie
的文檔,但我們進一步只選擇那些具有提供給函數的 id 的文檔。 由於我們打算顯示電影的很多細節,我們指定了一堆屬性來檢索。
我們檢索了電影id
以及castMembers
數組中的一些屬性,即ref
、 characterName
、人名和人的圖像。 我們還將別名從castMembers
更改為cast
。
與castMembers
一樣,我們從crewMembers
數組中選擇了一些屬性,即ref
、 department
、 job
、人名和人像。 我們還將別名從crewMembers
更改為crew
。
以同樣的方式,我們選擇了概述文本、受歡迎程度、電影的海報 url、電影的上映日期和標題。
Sanity 的 GROQ 語言還允許我們對文檔進行排序。 要對項目進行排序,我們將order傳遞給管道運算符。
例如,如果我們希望按電影的releaseDate
升序對電影進行排序,我們可以執行以下操作。
const data = await sanityAPI.fetch( `*[_type == 'movie']{ ... } | order(releaseDate, asc)` );
我們在sortMoviesBy
函數中使用了這個概念來按升序或降序排序。
下面我們來看看這個函數。
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 }); } };
我們首先分派一個MOVIES_SORT_REQUEST
類型的操作來確定請求何時加載。 然後,我們使用GROQ
語法對movie
集合中的數據進行排序和獲取。 要排序的項目在變量item
中提供,排序模式(升序或降序)在變量type
中提供。 因此,我們返回了id
、海報 url 和標題。 一旦數據返回,我們發送一個MOVIES_SORT_SUCCESS
類型的動作,如果失敗,我們發送一個MOVIES_SORT_FAIL
類型的動作。
類似的GROQ
概念適用於getMostPopular
函數。 GROQ
語法如下所示。
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]` );
這裡唯一的區別是我們按受歡迎程度按降序對電影進行排序,然後只選擇前三部。 這些項目在從零開始的索引中返回,因此前三個項目是項目 0、1 和 2。如果我們希望檢索前十個項目,我們可以將[0..9]
傳遞給函數。
這是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 };
設置減速器
Reducer 是 Redux 中最重要的概念之一。 它們採用先前的狀態並確定狀態變化。
通常,我們將使用 switch 語句為每個動作類型執行一個條件。 例如,我們可以在動作類型表示加載時返回loading
,然後在表示成功或錯誤時返回有效負載。 期望將initial state
和action
作為參數。
我們的movieReducers.js
文件包含各種reducer 來匹配movieActions.js
文件中定義的動作。 但是,每個 reducer 都有相似的語法和結構。 唯一的區別是它們調用的constants
和它們返回的值。
讓我們首先看一下movieReducers.js
文件中的fetchAllMoviesReducer
。
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; } };
與所有 reducer 一樣, fetchAllMoviesReducer
將初始狀態對象 ( state
) 和action
對像作為參數。 我們使用 switch 語句來檢查每個時間點的動作類型。 如果它對應於MOVIES_FETCH_REQUEST
,我們將 loading 作為 true 以使我們能夠向用戶顯示加載指示器。
如果它對應於MOVIES_FETCH_SUCCESS
,我們關閉加載指示器,然後在變量movies
中返回動作負載。 但是如果是MOVIES_FETCH_FAIL
,我們也會關閉加載,然後返回錯誤。 我們還想要重置電影的選項。 這將使我們能夠在需要時清除狀態。
我們對其他減速器具有相同的結構。 完整的movieReducers.js
如下所示。
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 };
對於personReducers.js
,我們也遵循了完全相同的結構。 例如, fetchAllPersonsReducer
函數定義了獲取數據庫中所有人員的狀態。
這在下面的代碼中給出。
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; } };
就像fetchAllMoviesReducer
一樣,我們用state
和action
作為參數定義了fetchAllPersonsReducer
。 這些是 Redux reducer 的標准設置。 然後我們使用 switch 語句來檢查動作類型,如果它是PERSONS_FETCH_REQUEST
類型,我們將 loading 作為 true 返回。 如果是PERSONS_FETCH_SUCCESS
,我們關閉加載並返回有效負載,如果是PERSONS_FETCH_FAIL
,我們返回錯誤。
組合減速機
Redux 的combineReducers
函數允許我們組合多個 reducer 並將其傳遞給 store。 我們將在reducers
文件夾中的index.js
文件中組合我們的電影和人物減速器。
讓我們來看看它。
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 });
在這裡,我們從電影、人物和全局減速器文件中導入了所有減速器,並將它們傳遞給combineReducers
函數。 combineReducers
函數接受一個允許我們傳遞所有 reducer 的對象。 我們甚至可以為過程中的參數添加別名。
稍後我們將處理globalReducers
。
我們現在可以在 Redux store.js
文件中傳遞 reducer。 這如下所示。
import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/index"; export default createStore(reducers, initialState, applyMiddleware(thunk));
設置完 Redux 工作流程後,讓我們設置 React 應用程序。
設置我們的 React 應用程序
我們的反應應用程序將列出電影及其相應的演員和工作人員。 我們將使用react-router-dom
進行路由,使用styled-components
設置應用程序的樣式。 我們還將為圖標和一些 UI 組件使用 Material UI。
輸入以下bash
命令以安裝依賴項。
npm install react-router-dom @material-ui/core @material-ui/icons query-string
這是我們將要構建的內容:
將 Redux 連接到我們的 React 應用程序
React-redux
附帶一個Provider函數,允許我們將應用程序連接到 Redux 存儲。 為此,我們必須將 store 的一個實例傳遞給 Provider。 我們可以在index.js
或App.js
文件中執行此操作。
這是我們的 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") );
在這裡,我們從react-redux
導入Provider
並從我們的 Redux store
導入 store。 然後我們用 Provider 包裝整個組件樹,將 store 傳遞給它。
接下來,我們需要react-router-dom
來在我們的 React 應用程序中進行路由。 react-router-dom
帶有BrowserRouter
、 Switch
和Route
,可用於定義我們的路徑和路由。
我們在App.js
文件中執行此操作。 這如下所示。
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;
這是使用 react-router-dom 進行路由的標准設置。 你可以在他們的文檔中查看。 我們導入了我們的組件Header
、 Footer
、 PersonsList
和MovieList
。 然後,我們通過將所有內容包裝在Router
和Switch
中來設置react-router-dom
。
由於我們希望我們的頁面共享相同的頁眉和頁腳,我們必須在使用Switch
包裝結構之前傳遞<Header />
和<Footer />
組件。 我們還對main
元素做了類似的事情,因為我們希望它包裝整個應用程序。
我們使用react-router-dom
中的Route
將每個組件傳遞給路由。
定義我們的頁面和組件
我們的應用程序以結構化的方式組織。 可重用組件存儲在components
文件夾中,而 Pages 存儲在pages
文件夾中。
我們的pages
包括movieListPage.js
、 moviePage.js
、 PersonListPage.js
和PersonPage.js
。 MovieListPage.js
列出了我們 Sanity.io 後端中的所有電影以及最受歡迎的電影。
要列出所有電影,我們只需dispatch
在我們的movieAction.js
文件中定義的fetchAllMovies
動作。 由於我們需要在頁面加載後立即獲取列表,因此我們必須在useEffect
中定義它。 這如下所示。
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;
感謝useDispatch
和useSelector
Hooks,我們可以調度 Redux 操作並從 Redux 存儲中選擇適當的狀態。 請注意,狀態loading
、 error
和movies
是在我們的 Reducer 函數中定義的,這裡使用 React Redux 的useSelector
Hook 選擇它們。 這些狀態,即loading
、 error
和movies
,在我們發送fetchAllMovies()
動作後立即可用。
一旦我們得到電影列表,我們就可以使用map
函數或我們希望的任何方式在我們的應用程序中顯示它。
這是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
我們首先在useEffect
Hook 中調度getMostPopular
電影動作(此動作選擇最受歡迎的電影)。 這使我們能夠在頁面加載後立即檢索最受歡迎的電影。 此外,我們允許用戶按電影的releaseDate
和popularity
對電影進行排序。 這是由上面代碼中調度的sortMoviesBy
操作處理的。 此外,我們根據查詢參數調度了fetchAllMovies
。
此外,我們使用useSelector
Hook 為每個操作選擇相應的 reducer。 我們為每個減速器選擇了loading
、 error
和movies
的狀態。
從 reducer 獲取movies
后,我們現在可以將它們顯示給用戶。 在這裡,我們使用了 ES6 的map
函數來做到這一點。 每當每個電影狀態正在加載時,我們首先顯示一個加載器,如果有錯誤,我們會顯示錯誤消息。 最後,如果我們得到一部電影,我們使用map
函數將電影圖像顯示給用戶。 我們將整個組件包裝在一個MovieListContainer
組件中。
<MovieListContainer> … </MovieListContainer>
標記是使用樣式組件定義的div
。 我們很快就會對此進行簡要介紹。
使用樣式化的組件來樣式化我們的應用程序
樣式化的組件允許我們單獨設置頁面和組件的樣式。 它還提供了一些有趣的功能,例如inheritance
、 Theming
、 passing of props
等。
儘管我們總是希望單獨設置頁面樣式,但有時可能需要全局樣式。 有趣的是,styled-components 提供了一種方法來做到這一點,這要歸功於createGlobalStyle
函數。
要在我們的應用程序中使用 styled-components,我們需要安裝它。 在您的反應項目中打開您的終端並輸入以下bash
命令。
npm install styled-components
安裝 styled-components 後,讓我們開始使用我們的全局樣式。
讓我們在src
目錄中創建一個名為styles
的單獨文件夾。 這將存儲我們所有的樣式。 我們還要在樣式文件夾中創建一個globalStyles.js
文件。 要在樣式組件中創建全局樣式,我們需要導入createGlobalStyle
。
import { createGlobalStyle } from "styled-components";
然後我們可以定義我們的樣式如下:
export const GlobalStyle = createGlobalStyle` ... `
樣式化組件使用模板文字來定義道具。 在這個字面量中,我們可以編寫我們傳統的CSS
代碼。
我們還導入了在名為definition.js
的文件中定義的deviceWidth
。 deviceWidth
包含用於設置媒體查詢的斷點定義。
import { deviceWidth } from "./definition";
我們將溢出設置為隱藏以控制應用程序的流程。
html, body{ overflow-x: hidden; }
我們還使用.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%; } ... }
在這裡,定義了各種樣式,例如背景顏色、z-index、填充和許多其他傳統的 CSS 屬性。
我們使用 styled-components props
來設置背景顏色。 這允許我們設置可以從我們的組件傳遞的動態變量。 此外,我們還傳遞了主題的變量,以使我們能夠充分利用主題切換。
在這裡可以進行主題化,因為我們已經使用樣式組件中的ThemeProvider
包裝了整個應用程序。 我們一會兒再談這個。 此外,我們使用CSS flexbox
正確設置標題樣式並將位置設置為fixed
,以確保它相對於瀏覽器保持固定。 我們還定義了斷點以使標頭移動友好。
這是我們的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}; } `;
請注意,我們在文字中編寫了純 CSS 代碼,但也有一些例外。 Styled-components 允許我們傳遞道具。 您可以在文檔中了解更多相關信息。
除了定義全局樣式外,我們還可以為各個頁面定義樣式。
例如,這裡是styles
文件夾中PersonStyle.js
中定義的PersonListPage.js
的樣式。
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; } } } } `;
我們首先從definition
文件中導入styled
styled-components
中的 styled 和deviceWidth
。 然後我們將PersonsListContainer
定義為一個div
來保存我們的樣式。 使用媒體查詢和已建立的斷點,我們通過設置各種斷點使頁面對移動設備友好。
在這裡,我們只對小屏幕、大屏幕和超大屏幕使用了標準瀏覽器斷點。 我們還充分利用了 CSS flexbox 和 grid 來正確地設置樣式並在頁面上顯示我們的內容。
要在我們的PersonListPage.js
文件中使用這種樣式,我們只需將其導入並添加到我們的頁面中,如下所示。
import React from "react"; const PersonsListPage = () => { return ( <PersonsListContainer> ... </PersonsListContainer> ); }; export default PersonsListPage;
包裝器將輸出一個div
,因為我們在樣式中將其定義為 div。
添加主題並將其包裝起來
向我們的應用程序添加主題總是一個很酷的功能。 為此,我們需要以下內容:
- 我們的自定義主題定義在一個單獨的文件中(在我們的例子中是
definition.js
文件)。 - 在我們的 Redux 操作和 reducer 中定義的邏輯。
- 在我們的應用程序中調用我們的主題並將其傳遞給組件樹。
讓我們檢查一下。
這是我們在definition.js
文件中的theme
對象。
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)" } };
我們為淺色和深色主題添加了各種顏色屬性。 顏色經過精心挑選,可在明暗模式下實現可見性。 您可以根據需要定義主題。 這不是一個硬性規定。
接下來,讓我們將功能添加到 Redux。
我們在 Redux 操作文件夾中創建了globalActions.js
並添加了以下代碼。
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)); };
在這裡,我們只是導入了我們定義的主題。 調度相應的動作,傳遞我們需要的主題的有效載荷。 負載結果存儲在本地存儲中,對淺色和深色主題使用相同的鍵。 這使我們能夠在瀏覽器中保持狀態。
我們還需要為主題定義我們的 reducer。
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; } };
這與我們一直在做的非常相似。 我們使用switch
語句來檢查操作的類型,然後返回適當的payload
。 我們還返回了一個狀態light
,用於確定用戶選擇淺色還是深色主題。 我們將在我們的組件中使用它。
我們還需要將它添加到我們的根 reducer 和 store。 這是我們的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));
由於我們需要在用戶刷新時保持主題,我們必須使用localStorage.getItem()
從本地存儲中獲取它並將其傳遞給我們的初始狀態。
將功能添加到我們的 React 應用程序
樣式化的組件為我們提供了ThemeProvider
,它允許我們通過我們的應用程序傳遞主題。 我們可以修改我們的 App.js 文件來添加這個功能。
讓我們來看看它。
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;
通過ThemeProvider
傳遞主題,我們可以輕鬆地在樣式中使用主題道具。
例如,我們可以將顏色設置為我們的bodyText
自定義顏色,如下所示。
color: ${(props) => props.theme.bodyText};
我們可以在應用程序中任何需要顏色的地方使用自定義主題。
例如,要定義border-bottom
,我們執行以下操作。
border-bottom: 2px solid ${(props) => props.theme.goldish};
結論
我們首先深入研究 Sanity.io,對其進行設置並將其連接到我們的 React 應用程序。 然後我們設置 Redux 並使用 GROQ 語言來查詢我們的 API。 我們看到瞭如何使用react-redux
連接和使用 Redux 到我們的 React 應用程序,使用 styled-components 和主題。
然而,我們只觸及了這些技術可能實現的表面。 我鼓勵您仔細閱讀我的 GitHub 存儲庫中的代碼示例,並嘗試使用這些技術來學習和掌握它們來完成一個完全不同的項目。
資源
- 健全文檔
- Kapehe 如何使用 Sanity.io 構建博客
- Redux 文檔
- 樣式化組件文檔
- GROQ 備忘單
- 材質 UI 文檔
- Redux 中間件和 SideEffects
- Redux Thunk 文檔