转载请注明出处,未经同意,不可修改文章内容。
🔥🔥🔥“前端一万小时”两大明星专栏——“从零基础到轻松就业”、“前端面试刷题”,已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。
1 取得并显示“数据”
对于一个正经的实战项目而言,Redux 这种“数据层”的框架是肯定要用的。
而一旦用了 Redux,那我们最好是把所有“数据”都放在它里边去管理。
不要管这个“数据”在是组件“私有”的,还是各组件“共享”的。因为,随着页面的增多,功能变得越来越复杂,你根本没办法保证之前的“私有”数据还依然是“私有”。故,统统都放到 Redux 中进行管理是最佳实践,这也为我们后期代码的维护减轻了很大的负担!
本篇我们再仔细走走 Redux 和 React-redux 的流程,慢一点,本篇将整个项目的架子搭起来,后续的开发就跟着套路用就是了。
1️⃣在项目 qdywxs-jianshu 中分别安装 Redux 和 React-redux:
2️⃣接着,在 src 目录下创建一个“文件夹” store ,并在 store 文件夹下创建一个“文件” index.js ——即,存放 store 代码的位置:
3️⃣有了存放 store 代码的位置 index.js ,我们就可以在里边用代码来创建一个数据的“公共存储仓库”:
import { createStore } from "redux"; /*
3️⃣-①:从 redux 这个第三方模块中,
引入 createStore 方法;
*/
const store = createStore(); // 3️⃣-②:调用这个方法创建一个 store;
export default store; // 3️⃣-③:将创建的 store 导出!
4️⃣❗️❗️❗️由于 store 仅仅是一个“图书管理员”,它记不住到底应该怎么去管理数据。故,它需要一个“记录本”reducer 来辅助它管理数据。因此,我们在创建 store 的同时,必须要把这个“记录本”一并传给 store,否则 store 什么也不知道。
4️⃣-①:所以,我们还得在 store 目录下创建 reducer.js 文件;
4️⃣-②:在 Redux 中, reducer.js 文件需要返回一个“函数”;
const defaultState = { /*
4️⃣-④:既然 reducer 的一个重要功能是“放置数据”,
那么根据之前做 TodoList 的经验,
本项目 Header 组件此时有一项“默认数据”:refresh;
*/
refresh: false // ❗️默认 refresh 的初始值为 false,表示 span 标签没有这个 class 名!
}
// 4️⃣-⑤:上边有了“默认数据”,这里记得让“参数”state 等于 defaultState;
export default (state = defaultState, action) => { /*
4️⃣-③:返回的“函数”接受两个固定参数:
state 和 action。
state 指:整个仓库里存储的数据(可以形象的理解为,“记录本”
里记录的“图书馆”中所有的书籍信息);
action 指:传过来的那句话。
*/
return state; // ❗️默认返回 state。
}
4️⃣-⑥:同时,在前边创建 store 的时候( index.js ),我们要把“记录本”reducer 传递给 store,并将 reducer 作为第一个“参数”传递给“方法” createStore() ;
import { createStore } from "redux";
import reducer from "./reducer"; // 🚀从当前目录下的 reducer.js 引入 reducer。
const store = createStore(reducer); /*
❗️❗️❗️将 reducer 作为第一个“参数”
传递给“方法”createStore!
*/
export default store;
4️⃣-⑦:既然把“记录本”reducer 传递给了 store,那么 store 就知道这个仓库里边有 refresh 这个数据了;
5️⃣通过以上的步骤,store 里边就有数据。按照 Redux 和 React-redux 的工作流程,一旦有了数据,各“组件”就可以连接 store,去 store 里边取数据并显示出来!
5️⃣-①:打开 src 目录下的 App.js 文件;
import React, { Component } from "react";
import {GlobalStyle} from "./style";
import {GlobalIconStyle} from "./statics/iconfont/iconfont";
import Header from "./common/header";
/*
❗️❗️❗️5️⃣-②:有了 React-redux 后,我们会首先从 react-redux 引入一个 Provider 组件(
它是 React-redux 的核心 API 之一);
*/
import { Provider } from "react-redux";
// 5️⃣-③:同时,在本文件中引入 store;
import store from "./store";
class App extends Component {
render() {
return (
<div>
<GlobalStyle />
<GlobalIconStyle />
{/* 5️⃣-④:用 Provider 组件包裹“组件”Header; */}
<Provider store={store}> {/*
❗️❗️❗️5️⃣-⑤:给 Provider 添加一个属性,
使其等于“5️⃣-③”中引入的 store。
这一步的意思为:
“提供器 Provider”连接了 store,那么 Provider 里边的所有
“组件”(如这里的 Header)都有能力获取到 store 里的数据了!
*/}
<Header />
</Provider>
</div>
);
}
}
export default App;
6️⃣通过上边的操作后,Header 组件已经拥有了获取 store 中数据的“能力”。可光有“能力”可不行,要具体怎么去获取呢?
6️⃣-①:打开 header 目录下的 index.js 文件;
import React, {Component} from "react";
import {
HeaderWrapper,
Logo,
Navbar,
ItemList,
LinkList,
SearchArea,
SearchInput,
SearchPanel,
PanelTitle,
PanelChange,
PanelLabels,
LabelLink,
Extra,
ExtraLink
} from "./style";
/*
❗️❗️❗️6️⃣-②:从 react-redux 中引入 connect 方法(它也是 React-redux 的核心 API 之一),
connect 的作用很明确——就是“连接”的意思!
*/
import { connect } from "react-redux";
class Header extends Component {
/*
❗️6️⃣-①-1:在上边的“4️⃣-④”中,“数据”已经被定义到了 reducer 中,
故本文件中删除掉定义“数据”的代码;
constructor(props) {
super(props);
this.state = {
refresh: false
}
❗️React-redux 的引入,与“交互”相关的代码都不需要这样写了,故删除!
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this)
}
*/
render() {
return (
<HeaderWrapper>
<Logo>
<img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
</Logo>
<Navbar className="clearfix">
<ItemList className="active">
<LinkList href="/">
首页
</LinkList>
</ItemList>
<ItemList>
<LinkList href="/">
下载APP
</LinkList>
</ItemList>
</Navbar>
<SearchArea>
<SearchInput />
<span className="iconfont icon-search"></span>
<SearchPanel>
<PanelTitle>
热门搜索
{/*
❗️6️⃣-①-2:先不用写“交互”事件,稍后会按 React-redux 的流程来重写!
<PanelChange onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
*/}
<PanelChange>
{/*
❗️❗️❗️6️⃣-⑨:“映射”上了过后,我们就可以通过调用 this.props.refresh 来
“调用”store 中的 refresh 了!
删除下边的代码,重新用 this.props 来改写~
<span className={this.state.refresh ? "iconfont refresh" : "iconfont"}></span>
*/}
<span className={this.props.refresh ? "iconfont refresh" : "iconfont"}></span>
换一批
</PanelChange>
</PanelTitle>
<PanelLabels className="clearfix">
<LabelLink href="/">
区块链
</LabelLink>
<LabelLink href="/">
故事
</LabelLink>
<LabelLink href="/">
小程序
</LabelLink>
<LabelLink href="/">
前端一万小时
</LabelLink>
</PanelLabels>
</SearchPanel>
</SearchArea>
<Extra>
<span className="iconfont icon-textsize" ></span>
<ExtraLink className="login" href="/">
登录
</ExtraLink>
<ExtraLink className="register" href="/">
注册
</ExtraLink>
<ExtraLink className="writing" href="/">
<span className="iconfont icon-pen"></span>
写文章
</ExtraLink>
</Extra>
</HeaderWrapper>
)
}
/*
❗️6️⃣-①-3:React-redux 的引入,与“交互”相关的代码都不需要这样写了,故删除,稍后重写!
handleMouseDown() {
this.setState({
refresh: true
})
}
handleMouseUp() {
this.setState({
refresh: false
})
}
*/
}
// 6️⃣-⑥:接下来,我们先定义“连接”的“规则”;
const mapStateToProps = (state) => { /*
6️⃣-⑦:把 store 里的“数据 state”作为“参数”
传递给 mapStateToProps;
*/
return { // ❗️这个“规则”会返回一个“对象”出去!
refresh: state.refresh /*
❗️❗️❗️6️⃣-⑧:“规则”的具体做法为——将 store 里的 refresh
映射到“Header 组件”里的 props 的 refresh 中去;
*/
}
}
/*
❗️❗️❗️6️⃣-③:之前我们直接导出的是 Header,可用了 React-redux 后,就不能这样写了!
export default Header;
*/
/*
6️⃣-④:取而代之,我们是导出 connect 方法(
❗️注意看我们给 connect 方法传递了哪些参数!);
*/
export default connect(mapStateToProps, null)(Header); /*
6️⃣-⑤:我们一共要给 connect 传递 3 个参数!
Header 表示:connect 会让“Header 组件”和 store
进行“连接”(由“5️⃣-⑤”可知,Header 已经拥有“能力”
连接 store);
mapStateToProps 表示:“Header 组件”和 store
进行“连接”是需要“规则”的,而具体的“规则”就在这个
mapStateToProps 里边(❗️直译为:把 store 里边的
“数据 state”映射到“Header 组件”的 props 里);
null 表示:这里还会接收一个名叫
mapDispatchToProps 的参数,等下“改变数据”时讲解,
这里先用 null 占位。
*/
返回页面查看效果(为了测试是否取得了“数据”,可以将 reducer.js 中的 refresh 初始值改为 true ,让 span 标签一开始就有“旋转动画效果”):
通过上边的操作,Header 组件就可以获得并显示“数据”了。但,“旋转动画”是一个交互(涉及了用户的操作 action)的过程,只有“鼠标按压下去”的时候,“旋转动画”才会生效(“数据 refresh ”从 false 变成 true )。故,我们需要来走走 React -redux 中“改变数据”的流程!
2 配置 redux devtools
7️⃣🚀既然涉及到“改变数据”,我们就应该先来配置好 redux devtools:
打开 instructions 后,由于本项目后续肯定会用到“中间件”,故配置的时候要将“中间件”考虑进来(❗️但此时不用配置,后边用到“中间件”时再配置此项):
将其粘贴至 src 目录下 store 中的 index.js 文件中:
// 7️⃣-④:将 compose 函数从 redux 中引入进来;
import { createStore, applyMiddleware, compose } from "redux";
import reducer from "./reducer";
// 7️⃣-①:直接拷贝官方文档里的代码;
const composeEnhancers =
/*
❗️7️⃣-②:这行代码可以注释掉,因为浏览器的应用,故 window 的 object 是肯定存在的!
typeof window === 'object' &&
*/
/*
❗️7️⃣-③:下面这行代码的意思为:
如果 window 下边有 __REDUX_DEVTOOLS_EXTENSION__ 这个变量的话,
就执行这个变量对应的方法 window.__REDUX_DEVTOOLS_EXTENSION__()。
否则,
就执行 compose 函数;
*/
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
// 7️⃣-⑤:继续拷贝官网的代码;
const enhancer = composeEnhancers( /*
7️⃣-⑥:表示将 composeEnhancers 执行后的
结果赋值给 enhancer;
*/
/*
❗️我们暂时不配置“中间件”~
applyMiddleware(...middleware)
*/
);
const store = createStore(
reducer,
enhancer // 7️⃣-⑦:直接将 enhancer 传递进来即可!
);
export default store;
再次在页面查看(很多数据信息都一目了然了,接下来再做更复杂的 Redux 调试就很方便了):
3 改变“数据”
8️⃣本项目中,我要做的事是“改变 span 标签的 class 名”来控制执行“旋转动画”与否。
在编写 header 目录下的 index.js 文件之前,我们得按照实际项目的开发流程,先拆分 actionTypes 和 actionCreators :
actionTypes.js 文件:
export const CHANGE_CLASS_NAME = "change_class_name";
export const RESUME_CLASS_NAME ="resume_class_name";
actionCreators.js 文件:
import {CHANGE_CLASS_NAME, RESUME_CLASS_NAME} from "./actionTypes";
export const changeClassNameAction = () => ({
type: CHANGE_CLASS_NAME
})
export const resumeClassNameAction = () => ({
type: RESUME_CLASS_NAME
})
9️⃣打开 header 目录下的 index.js 文件:
import React, {Component} from "react";
import {
HeaderWrapper,
Logo,
Navbar,
ItemList,
LinkList,
SearchArea,
SearchInput,
SearchPanel,
PanelTitle,
PanelChange,
PanelLabels,
LabelLink,
Extra,
ExtraLink
} from "./style";
import { connect } from "react-redux";
// ❗️❗️❗️从 src 目录下的 store 中引入 actionCreators 中创建好的“方法”!
import {changeClassNameAction, resumeClassNameAction} from "../../store/actionCreators";
class Header extends Component {
render() {
return (
<HeaderWrapper>
<Logo>
<img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
</Logo>
<Navbar className="clearfix">
<ItemList className="active">
<LinkList href="/">
首页
</LinkList>
</ItemList>
<ItemList>
<LinkList href="/">
下载APP
</LinkList>
</ItemList>
</Navbar>
<SearchArea>
<SearchInput />
<span className="iconfont icon-search"></span>
<SearchPanel>
<PanelTitle>
热门搜索
<PanelChange
onMouseDown={this.props.handleMouseDown}
onMouseUp={this.props.handleMouseUp}
> {/*
9️⃣-④:给 PanelChange 样式组件绑定一个“事件 onMouseDown”,
❓可这个“事件”应该怎样被调用呢?
*/}
{/*
9️⃣-⑥:因此可以通过 this.props.handleMouseDown 和
this.props.handleMouseUp 来分别调用 store 的 handleMouseDown 和
handleMouseUp;
*/}
<span className={this.props.refresh ? "iconfont refresh" : "iconfont"}></span>
换一批
</PanelChange>
</PanelTitle>
<PanelLabels className="clearfix">
<LabelLink href="/">
区块链
</LabelLink>
<LabelLink href="/">
故事
</LabelLink>
<LabelLink href="/">
小程序
</LabelLink>
<LabelLink href="/">
前端一万小时
</LabelLink>
</PanelLabels>
</SearchPanel>
</SearchArea>
<Extra>
<span className="iconfont icon-textsize" ></span>
<ExtraLink className="login" href="/">
登录
</ExtraLink>
<ExtraLink className="register" href="/">
注册
</ExtraLink>
<ExtraLink className="writing" href="/">
<span className="iconfont icon-pen"></span>
写文章
</ExtraLink>
</Extra>
</HeaderWrapper>
)
}
}
const mapStateToProps = (state) => {
return {
refresh: state.refresh
}
}
/*
❗️❗️❗️9️⃣-②:接下来,我们定义哪些“用户的操作”
应该当作 action,并传给 store;
*/
const mapDispatchToProps = (dispatch) => { /*
9️⃣-③:把 store 里的“dispatch 方法”
作为“参数”传递给 mapDispatchToProps;
*/
return {
handleMouseDown() { /*
9️⃣-⑤:在这里定义用户的“onMouseDown 操作”会被当作 action
传给 store;
*/
const action = changeClassNameAction();
dispatch(action)
},
handleMouseUp() {
const action = resumeClassNameAction();
dispatch(action)
}
}
}
/*
❗️9️⃣-①:给 connect 传递第 3 个参数——mapDispatchToProps。
mapDispatchToPropos 直译为:我们把 store 的 dispatch 方法“挂载”到
Header 组件的 props 上。
即,我们可以定义哪些“用户的操作”应该当作 action,并传给 store!
*/
export default connect(mapStateToProps, mapDispatchToProps)(Header); /*
❗️导出一个
“容器组件”!
*/
9️⃣-⑦:打开 src 目录中 store 下的 reducer.js 文件:
// 9️⃣-⑧:引入“常量”;
import {CHANGE_CLASS_NAME, RESUME_CLASS_NAME} from "./actionTypes";
const defaultState = {
refresh: false
}
export default (state=defaultState, action) => {
if(action.type === CHANGE_CLASS_NAME) {
return {
refresh: true
}
}
if(action.type === RESUME_CLASS_NAME) {
return {
refresh: false
}
}
return state;
}
返回页面查看效果(一切正常运行):
4 combineReducers 对数据进行拆分管理
理论上说,上文的一系列操作已成功实现了效果。但,其中依然有一些非最佳实践的操作。
上文中,我们把所有数据都放在 src 目录下 store 文件夹里的 reducer.js 中进行维护。在“数据”和“处理逻辑”较少的情况下没什么问题,但随着页面的增多,项目更加复杂,“数据”量会越来越大,“处理逻辑”相关的代码也会占据 reducer.js 文件很大的篇幅!
在实际工作中,我们一般不要在同一个文件中写超过 300 行的代码,不然后期维护起来将变得特别棘手!
故,本项目中我们要想办法不要让 reducer.js 文件变得那么大。
🚀幸运的是,Redux 为我们提供了一个很好的思路和方法——combineReducers(形象地解释为:“图书管理员”在查询“小册子”时,这个小册子不再是厚厚的一大本,而是分门别类的一小本。“管理员”就可以很容易地在分类下找到需要的书籍~)。
🔟-①:打开 src 目录 common 下的文件夹 header ,header 下有很多只属于 Header 组件的“数据”,我们就可以将这些“数据”放在 header 里进行管理。
在 header 目录下新建一个文件夹 store :
🔟-②:紧接着,我们在 store 文件夹下创建一个 index.js 文件,它的作用为——❗️它是这整个 store 的“出口”文件(“出口”文件的好处为:它可以简化引用文件时的“路径”,也便于后期维护~);
🔟-③:在 store 文件夹下创建 reducer.js 文件;
🔟-④:同理,在 store 文件夹下创建 actionTypes.js 和 actionCreators.js 文件;
架子搭起后,我们来挨个修改各文件里的代码:
🔟-⑤:将原本 src 目录下 store 文件夹里的 actionTypes.js 和 actionCreators 这两个文件中的代码移至 header 目录下 store 文件夹中,并同时删除 src 目录下 store 文件夹里的 actionTypes.js 和 actionCreators 这两个文件。
🔟-⑥:将原本 src 目录下 store 文件夹里 reducer.js 文件中的代码“剪切”至 header 目录下 store 中的 reducer.js 中;
import {CHANGE_CLASS_NAME, RESUME_CLASS_NAME} from "./actionTypes";
const defaultState = {
refresh: false
}
export default (state=defaultState, action) => {
if(action.type === CHANGE_CLASS_NAME) {
return {
refresh: true
}
}
if(action.type === RESUME_CLASS_NAME) {
return {
refresh: false
}
}
return state;
}
🔟-⑦:❗️打开 header 目录下 store 中的 index.js 这个 store 的“出口”文件,我们将需要传递出去的东西在里边定义好;
import reducer from "./reducer";
import * as actionTypes from "./actionTypes";
import * as actionCreators from "./actionCreators";
export {reducer, actionTypes, actionCreators};
header 的 store 下各文件都写好了,并有了自己的“出口”文件,我们就需要去修改其他文件中的引用“路径”:
🔟-⑧:打开 header 目录下的 index.js 文件;
import React, {Component} from "react";
import {
HeaderWrapper,
Logo,
Navbar,
ItemList,
LinkList,
SearchArea,
SearchInput,
SearchPanel,
PanelTitle,
PanelChange,
PanelLabels,
LabelLink,
Extra,
ExtraLink
} from "./style";
import { connect } from "react-redux";
/*
🔟-⑧-1:简化这里的引用!
import {changeClassNameAction, resumeClassNameAction} from "../../store/actionCreators";
*/
import {actionCreators} from "./store";
class Header extends Component {
render() {
return (
<HeaderWrapper>
<Logo>
<img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
</Logo>
<Navbar className="clearfix">
<ItemList className="active">
<LinkList href="/">
首页
</LinkList>
</ItemList>
<ItemList>
<LinkList href="/">
下载APP
</LinkList>
</ItemList>
</Navbar>
<SearchArea>
<SearchInput />
<span className="iconfont icon-search"></span>
<SearchPanel>
<PanelTitle>
热门搜索
<PanelChange
onMouseDown={this.props.handleMouseDown}
onMouseUp={this.props.handleMouseUp}
>
<span className={this.props.refresh ? "iconfont refresh" : "iconfont"}></span>
换一批
</PanelChange>
</PanelTitle>
<PanelLabels className="clearfix">
<LabelLink href="/">
区块链
</LabelLink>
<LabelLink href="/">
故事
</LabelLink>
<LabelLink href="/">
小程序
</LabelLink>
<LabelLink href="/">
前端一万小时
</LabelLink>
</PanelLabels>
</SearchPanel>
</SearchArea>
<Extra>
<span className="iconfont icon-textsize" ></span>
<ExtraLink className="login" href="/">
登录
</ExtraLink>
<ExtraLink className="register" href="/">
注册
</ExtraLink>
<ExtraLink className="writing" href="/">
<span className="iconfont icon-pen"></span>
写文章
</ExtraLink>
</Extra>
</HeaderWrapper>
)
}
}
const mapStateToProps = (state) => {
return {
/*
❗️❗️❗️🔟-⑧-2:既然“数据”已经放到了自己的 Header 组件里,这里“映射”的时候就需要多加一层!
refresh: state.refresh
*/
refresh: state.header.refresh
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleMouseDown() {
const action = actionCreators.changeClassNameAction(); /*
🔟-⑧-3:这里需要在
changeClassNameAction 前边加上
actionCreators;
*/
dispatch(action)
},
handleMouseUp() {
const action = actionCreators.resumeClassNameAction(); /*
🔟-⑧-4:同理,
加上前缀;
*/
dispatch(action)
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);
🔟-⑨:❗️❗️❗️“路径”都改好了过后,仅表示你的这个小分类目录跑通了,但“小分类”必须依托于“大分类”才起作用。即,需要将各“组件”的“小 reducer”合并到项目的“大 reducer”中!
打开 src 目录下 store 文件夹里 reducer.js 文件,里边之前的代码已经被剪切,我们来编写新的代码:
import {combineReducers} from "redux"; /*
🔟-⑨-2:从 redux 中引入一个名为
combineReducers 的函数,其用于“合并”那些
小的 reducer;
*/
import {reducer as headerReducer} from "../common/header/store"; /*
🔟-⑨-1:先将小的 reducer
拿到大的 reducer 中;
*/
// ❓🔟-⑨-3:怎么“合并”呢?
const reducer = combineReducers({ // 🔟-⑨-4:直接调用这个“函数”,传入小的 reducer;
header: headerReducer
})
// 🔟-⑨-4:最后,将这个“合并”好的 reducer 导出!
export default reducer;
返回页面查看效果:
OK,本篇篇幅较长,但可以说是非常重要的一篇,请一定花时间去多写几次!
祝好,qdywxs ♥ you!