使用 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 文档