内容回顾
前面的篇幅主要介绍了:
- React技术栈+Express+Mongodb实现个人博客 -- Part 1 博客页面展示
- React技术栈+Express+Mongodb实现个人博客 -- Part 2 后台管理页面
- React技术栈+Express+Mongodb实现个人博客 -- Part 3 Express + Mongodb创建Server端
- React技术栈+Express+Mongodb实现个人博客 -- Part 4 使用Webpack打包博客工程
本篇文章主要介绍使用redux
将数据渲染到每个页面,如何使用redux-saga
处理异步请求的actions
Redux
随着 JavaScript 单页应用开发日趋复杂,JavaScript
需要管理比任何时候都要多的 state
(状态)。 这些state
可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括UI
状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。如果一个model
的变化会引起另一个 model
变化,那么当view
变化时,就可能引起对应 model
以及另一个 model
的变化,依次地,可能会引起另一个 view
的变化。乱!
这时候Redux
就强势登场了,现在你可以把React
的model
看作是一个个的子民,每一个子民都有自己的一个状态,纷纷扰扰,各自维护着自己状态,我行我素,那哪行啊!太乱了,我们需要一个King来领导大家,我们就可以把Redux
看作是这个King。网罗所有的组件组成一个国家,掌控着一切子民的状态!防止有人叛乱生事!
这个时候就把组件分成了两种:容器组件(redux或者路由)和展示组件(子民)。
- 容器组件:即
redux
或是router
,起到了维护状态,出发action
的作用,其实就是King高高在上下达指令。 - 展示组件:不维护状态,所有的状态由容器组件通过
props
传给他,所有操作通过回调完成。
展示组件 | 容器组件 | |
---|---|---|
作用 | 描述如何展现(骨架、样式) | 描述如何运行(数据获取、状态更新) |
直接使用 Redux | 否 | 是 |
数据来源 | props | 监听 Redux state |
数据修改 | 从 props 调用回调函数 | 向 Redux 派发 actions |
调用方式 | 手动 | 通常由 React Redux 生成 |
Redux
三大部分:store, action, reducer
。相当于King的直系下属。
可以看出Redux
是一个状态管理方案,在React中维系King和组件关系的库叫做 react-redux
, 它主要有提供两个东西:Provider
和 connect
,具体使用文后说明。
1. store
Store
就是保存数据的地方,它实际上是一个Object tree
。整个应用只能有一个 Store
。这个Store
可以看做是King的首相,掌控一切子民(组件)的活动state
。
Redux
提供createStore
这个函数,用来生成 Store
。
import { createStore } from 'redux';
const store = createStore(func);
createStore接受一个函数作为参数,返回一个Store对象(首相诞生记)
我们来看一下Store
(首相)的职责:
- 维持应用的 state;
- 提供 getState() 方法获取 state;
- 提供 dispatch(action) 方法更新 state;
- 通过 subscribe(listener) 注册监听器;
- 通过 subscribe(listener) 返回的函数注销监听器。
2. action
State
的变化,会导致 View
的变化。但是,用户接触不到State
,只能接触到 View
。所以,State
的变化必须是View
导致的。Action
就是 View
发出的通知,表示State
应该要发生变化了。即store
的数据变化来自于用户操作。action
就是一个通知,它可以看作是首相下面的邮递员,通知子民(组件)改变状态。它是store
数据的唯一来源。一般来说会通过 store.dispatch()
将 action
传到 store
。
Action
是一个对象。其中的type
属性是必须的,表示Action
的名称。
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
};
Action
创建函数:
Action
创建函数 就是生成 action
的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。
在 Redux
中的 action
创建函数只是简单的返回一个action
:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
这样做将使 action 创建函数更容易被移植和测试。
3. reducer
Action
只是描述了有事情发生了这一事实,并没有指明应用如何更新 state
。而这正是 reducer
要做的事情。也就是邮递员(action)只负责通知,具体你(组件)如何去做,他不负责,这事情只能是你们村长reducer
告诉你如何去做。
专业解释: Store
收到 Action
以后,必须给出一个新的 State
,这样 View
才会发生变化。这种State
的计算过程就叫做Reducer
。
Reducer
是一个函数,它接受 Action
和当前 State
作为参数,返回一个新的 State
。
const reducer = function (state, action) {
// ...
return new_state;
};
4. 数据流
严格的单向数据流是Redux
架构的设计核心。
Redux
应用中数据的生命周期遵循下面 4 个步骤:
- 调用 store.dispatch(action)。
- Redux store 调用传入的 reducer 函数。
- 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
- Redux store 保存了根 reducer 返回的完整 state 树。
工作流程图如下:
5. connect
Redux 默认并不包含 React 绑定库,需要单独安装。
npm install --save react-redux
当然,我们这个实例里是不需要的,所有需要的依赖已经在package.json
里配置好了。
React-Redux
提供connect
方法,用于从UI
组件生成容器组件。connect
的意思,就是将这两种组件连起来。
import { connect } from 'react-redux';
export default connect()(Home);
上面代码中Home是个UI组件,TodoList就是由 React-Redux 通过connect方法自动生成的容器组件。
而只是纯粹的这样把Home包裹起来毫无意义,完整的connect方法这样使用:
export default connect(
mapStateToProps,
mapDispatchToProps
)(Home);
上面代码中,connect
方法接受两个参数:mapStateToProps
和mapDispatchToProps
。它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将state
映射到 UI
组件的参数props
,后者负责输出逻辑,即将用户对UI
组件的操作映射成 Action
。
6. Provider
这个Provider
其实是一个中间件,它是为了解决让容器组件拿到King的指令(state
对象)而存在的。
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
上面代码中,Provider
在根组件外面包了一层,这样一来,App
的所有子组件就默认都可以拿到state
了。
Redux-Saga
React
作为View
层的前端框架,自然少不了很多中间件Redux Middleware
做数据处理, 而redux-saga
就是其中之一,下面仔细介绍一个这个中间件的具体使用流程和应用场景。
1. 简介
Redux-saga
是Redux
的一个中间件,主要集中处理react
架构中的异步处理工作,被定义为generator(ES6)
的形式,采用监听的形式进行工作。
2. 安装
使用npm进行安装:
npm install --save redux-saga
3. redux Effects
Effect
是一个javascript
对象,可以通过 yield
传达给 sagaMiddleware
进行执行在, 如果我们应用redux-saga
,所有的Effect
都必须被yield
才会执行。
举个例子,我们要改写下面这行代码:
yield fetch(url);
应用saga:
yield call(fetch, url)
3. take
等待 dispatch
匹配某个 action
。
比如下面这个例子:
....
while (true) {
yield take('CLICK_Action');
yield fork(clickButtonSaga);
}
....
4. put
触发某个action, 作用和dispatch相同:
yield put({ type: 'CLICK' });
举个例子:
export function* getArticlesListFlow () {
while (true){
let req = yield take(FrontActionTypes.GET_ARTICLE_LIST);
console.log(req);
let res = yield call(getArticleList,req.tag,req.pageNum);
if(res){
if(res.code === 0){
res.data.pageNum = req.pageNum;
yield put({type: FrontActionTypes.RESPONSE_ARTICLE_LIST,data:res.data});
}else{
yield put({type: IndexActionTypes.SET_MESSAGE, msgContent: res.message, msgType: 0});
}
}
}
}
5. select
作用和 redux thunk
中的getState
相同。通常会与reselect
库配合使用。
6. call
有阻塞地调用 saga
或者返回 promise
的函数,只在触发某个动作。
传统意义讲,我们很多业务逻辑要在action
中处理,所以会导致action
的处理比较混乱,难以维护,而且代码量比较大,如果我们应用redux-saga
会很大程度上简化代码, redux-saga
本身也有良好的扩展性, 非常方便的处理各种复杂的异步问题。
回到博客中
首先回到博客页面的入口,引入Redux
:
import React from 'react'
import IndexApp from './containers'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { AppContainer } from 'react-hot-loader'
import configureStore from './configureStore'
import 'antd/dist/antd.css';
import './index.css';
const store = configureStore();
render(
<AppContainer>
<Provider store={store}>
<IndexApp/>
</Provider>
</AppContainer>
,
document.getElementById('root')
);
-
AppContainer
是一个容器,为了配合热更新,需要在最外层添加这层容器。 -
configureStore
返回一个store
,其中引入了redux-saga
中间件,会吗会介绍。 -
IndexApp
是之前的首页路由配置,这里把它分离出来,简化代码结构。
State
在开始介绍每个页面之前,先来看一下博客这个工程State
是怎么设计的:
redux
的store
包含的state
分为三个部分:
- front , 负责博客页面展示的数据
- globalState,负责当前网络请求状态,登录用户信息和消息提示
- admin,负责后台管理页面的数据
先设计好全局的state
,下面在创建action
和reducer
时就更清晰了。
Actions and Reducers
在src
目录下新建一个文件夹reducers
,并新建一个文件index.js
。这个文件是总的reducer
,包括上面提到的admin,globalState,front
三个部分。
import {reducer as front} from './frontReducer'
import admin from './admin'
import {reducer as globalState} from './globalStateReducer'
import {combineReducers} from 'redux'
export default combineReducers({
front,
globalState,
admin
})
1. front
// 初始化state
const initialState = {
category: [],
articleList: [],
articleDetail: {},
pageNum: 1,
total: 0
};
// 定义所有的action类型
export const actionTypes = {
GET_ARTICLE_LIST: "GET_ARTICLE_LIST",
RESPONSE_ARTICLE_LIST: "RESPONSE_ARTICLE_LIST",
GET_ARTICLE_DETAIL: "GET_ARTICLE_DETAIL",
RESPONSE_ARTICLE_DETAIL: "RESPONSE_ARTICLE_DETAIL"
};
// 生产action的函数方法
export const actions = {
get_article_list: function (tag = '', pageNum = 1) {
return {
type: actionTypes.GET_ARTICLE_LIST,
tag,
pageNum
}
},
get_article_detail: function (id) {
return {
type: actionTypes.GET_ARTICLE_DETAIL,
id
}
}
};
// 处理action的reducer
export function reducer(state = initialState, action) {
switch (action.type) {
case actionTypes.RESPONSE_ARTICLE_LIST:
return {
...state, articleList: [...action.data.list], pageNum: action.data.pageNum, total: action.data.total
};
case actionTypes.RESPONSE_ARTICLE_DETAIL:
return {
...state, articleDetail: action.data
};
default:
return state;
}
}
细心的同学会问,获取文章列表的action为什么会有两个,都代表什么意思?
GET_ARTICLE_LIST: "GET_ARTICLE_LIST",
RESPONSE_ARTICLE_LIST: "RESPONSE_ARTICLE_LIST",
获取文章列表时,会发起一个网络请求,请求发起时,会执行get_article_list
这个方法,触发GET_ARTICLE_LIST
这个action
,这个action
会在store中被中间件redux-saga
接收:
let req = yield take(FrontActionTypes.GET_ARTICLE_LIST);
接收后,会执行方法
let res = yield call(getArticleList,req.tag,req.pageNum);
export function* getArticleList (tag,pageNum) {
yield put({type: IndexActionTypes.FETCH_START});
try {
return yield call(get, `/getArticles?pageNum=${pageNum}&isPublish=true&tag=${tag}`);
} catch (err) {
yield put({type: IndexActionTypes.SET_MESSAGE, msgContent: '网络请求错误', msgType: 0});
} finally {
yield put({type: IndexActionTypes.FETCH_END})
}
}
getArticleList
这个方法会发起请求,获取数据,如果成功获取数据,变触发RESPONSE_ARTICLE_LIST
这个action
通知store更新state。
if(res){
if(res.code === 0){
res.data.pageNum = req.pageNum;
yield put({type: FrontActionTypes.RESPONSE_ARTICLE_LIST,data:res.data});
}else{
yield put({type: IndexActionTypes.SET_MESSAGE, msgContent: res.message, msgType: 0});
}
}
这就是为什么会有GET_ARTICLE_LIST: "GET_ARTICLE_LIST", RESPONSE_ARTICLE_LIST: "RESPONSE_ARTICLE_LIST",
两个ActionType的原因。这里涉及到了redux-saga
,后面会做更详细的介绍。
2. globalState
const initialState = {
isFetching: true,
msg: {
type: 1,//0失败 1成功
content: ''
},
userInfo: {}
};
export const actionsTypes = {
FETCH_START: "FETCH_START",
FETCH_END: "FETCH_END",
USER_LOGIN: "USER_LOGIN",
USER_REGISTER: "USER_REGISTER",
RESPONSE_USER_INFO: "RESPONSE_USER_INFO",
SET_MESSAGE: "SET_MESSAGE",
USER_AUTH:"USER_AUTH"
};
export const actions = {
get_login: function (username, password) {
return {
type: actionsTypes.USER_LOGIN,
username,
password
}
},
get_register: function (data) {
return {
type: actionsTypes.USER_REGISTER,
data
}
},
clear_msg: function () {
return {
type: actionsTypes.SET_MESSAGE,
msgType: 1,
msgContent: ''
}
},
user_auth:function () {
return{
type:actionsTypes.USER_AUTH
}
}
};
export function reducer(state = initialState, action) {
switch (action.type) {
case actionsTypes.FETCH_START:
return {
...state, isFetching: true
};
case actionsTypes.FETCH_END:
return {
...state, isFetching: false
};
case actionsTypes.SET_MESSAGE:
return {
...state,
isFetching: false,
msg: {
type: action.msgType,
content: action.msgContent
}
};
case actionsTypes.RESPONSE_USER_INFO:
return {
...state, userInfo: action.data
};
default:
return state
}
}
这个文件处理的Action有
-
FETCH_START
请求开始,更新isFetching这个state为true,页面上开始转圈 -
FETCH_END
请求结束,更新isFetching这个state为false,页面上停止转圈 -
USER_LOGIN
用户发起登录请求, -
USER_REGISTER
用户发起注册请求 -
RESPONSE_USER_INFO
登录或注册成功返回用户信息 -
SET_MESSAGE
通知store更新页面的notification信息,显示消息内容,提示用户,例如登录失败等 -
USER_AUTH
页面打开时获取用户历史登录信息
3. admin
import { combineReducers } from 'redux'
import { users } from './adminManagerUser'
import { reducer as tags } from './adminManagerTags'
import { reducer as newArticle } from "./adminManagerNewArticle";
import { articles } from './adminManagerArticle'
export const actionTypes = {
ADMIN_URI_LOCATION:"ADMIN_URI_LOCATION"
};
const initialState = {
url:"/"
};
export const actions = {
change_location_admin:function (url) {
return{
type:actionTypes.ADMIN_URI_LOCATION,
data:url
}
}
};
export function reducer(state=initialState,action) {
switch (action.type){
case actionTypes.ADMIN_URI_LOCATION:
return {
...state,url:action.data
};
default:
return state
}
}
const admin = combineReducers({
adminGlobalState:reducer,
users,
tags,
newArticle,
articles
});
export default admin;
admin
包含了后台管理页面所需要的所有Actions
和Reducers
,这里讲文件分离出来,便于管理。里面涉及的代码,请查看工程源码,这里就不贴出来了。
store
import {createStore,applyMiddleware,compose} from 'redux'
import rootReducer from './reducers'
import createSagaMiddleware from 'redux-saga'
import rootSaga from './sagas'
const win = window;
const sagaMiddleware = createSagaMiddleware();
const middlewares = [];
let storeEnhancers ;
if(process.env.NODE_ENV==='production'){
storeEnhancers = compose(
applyMiddleware(...middlewares,sagaMiddleware)
);
}else{
storeEnhancers = compose(
applyMiddleware(...middlewares,sagaMiddleware),
(win && win.devToolsExtension) ? win.devToolsExtension() : (f) => f,
);
}
export default function configureStore(initialState={}) {
const store = createStore(rootReducer, initialState,storeEnhancers);
sagaMiddleware.run(rootSaga);
if (module.hot && process.env.NODE_ENV!=='production') {
// Enable Webpack hot module replacement for reducers
module.hot.accept( './reducers',() => {
const nextRootReducer = require('./reducers/index');
store.replaceReducer(nextRootReducer);
});
}
return store;
}
- 要使用
redux
的调试工具需要在createStore()步骤中添加一个中间件:
if(process.env.NODE_ENV==='production'){
storeEnhancers = compose(
applyMiddleware(...middlewares,sagaMiddleware)
);
}else{
storeEnhancers = compose(
applyMiddleware(...middlewares,sagaMiddleware),
(win && win.devToolsExtension) ? win.devToolsExtension() : (f) => f,
);
}
- webpack可以监听我们的组件变化并做出即时相应,但却无法监听reducers的改变,所以在store.js中增加一下代码:
if (module.hot && process.env.NODE_ENV!=='production') {
// Enable Webpack hot module replacement for reducers
module.hot.accept( './reducers',() => {
const nextRootReducer = require('./reducers/index');
store.replaceReducer(nextRootReducer);
});
}
-
rootSaga
是redux-saga
的配置文件:
import {fork} from 'redux-saga/effects'
import {loginFlow, registerFlow, user_auth} from './homeSaga'
import {get_all_users_flow} from './adminManagerUsersSaga'
import {getAllTagsFlow, addTagFlow, delTagFlow} from './adminManagerTagsSaga'
import {saveArticleFlow} from './adminManagerNewArticleSaga'
import {getArticleListFlow,deleteArticleFlow,editArticleFlow} from './adminManagerArticleSaga'
import {getArticlesListFlow,getArticleDetailFlow} from './frontSaga'
export default function* rootSaga() {
yield fork(loginFlow);
yield fork(registerFlow);
yield fork(user_auth);
yield fork(get_all_users_flow);
yield fork(getAllTagsFlow);
yield fork(addTagFlow);
yield fork(delTagFlow);
yield fork(saveArticleFlow);
yield fork(getArticleListFlow);
yield fork(deleteArticleFlow);
yield fork(getArticlesListFlow);
yield fork(getArticleDetailFlow);
yield fork(editArticleFlow);
}
这里fork
是指非阻塞任务调用,区别于call
方法,call
可以用来发起异步操作,但是相对于generator
函数来说,call
操作是阻塞的,只有等promise
回来后才能继续执行,而fork
是非阻塞的 ,当调用fork
启动一个任务时,该任务在后台继续执行,从而使得我们的执行流能继续往下执行而不必一定要等待返回。
先来回顾一下redus
的工作流,便于我们理解saga
是如何运行的
当一个
Action
被出发时,首先会到达Middleware
处,我们在创建store
时,添加了saga
这个中间件。所以action
会首先到达saga
里面,我们会在saga
里处理这个action
,例如发送网络请求,得到相应的数据,然后再出发另一个action
,告知reduce
去更新state
。
举例看一下get_all_users_flow
这个saga
的内容,其他的内容请查看工程源代码
import {put, take, call, select} from 'redux-saga/effects'
import {get} from '../fetch/fetch'
import {actionsTypes as IndexActionTypes} from '../reducers/globalStateReducer'
import {actionTypes as ManagerUserActionTypes} from '../reducers/adminManagerUser'
export function* fetch_users(pageNum) {
yield put({type: IndexActionTypes.FETCH_START});
try {
return yield call(get, `/admin/getUsers?pageNum=${pageNum}`);
} catch (err) {
yield put({type: IndexActionTypes.SET_MESSAGE, msgContent: '网络请求错误', msgType: 0});
} finally {
yield put({type: IndexActionTypes.FETCH_END})
}
}
export function* get_all_users_flow() {
while (true) {
let request = yield take(ManagerUserActionTypes.GET_ALL_USER);
let pageNum = request.pageNum||1;
let response = yield call(fetch_users,pageNum);
if(response&&response.code === 0){
for(let i = 0;i<response.data.list.length;i++){
response.data.list[i].key = i;
}
let data = {};
data.total = response.data.total;
data.list = response.data.list;
data.pageNum = Number.parseInt(pageNum);
yield put({type:ManagerUserActionTypes.RESOLVE_GET_ALL_USERS,data:data})
}else{
yield put({type:IndexActionTypes.SET_MESSAGE,msgContent:response.message,msgType:0});
}
}
}
使用take
方法可以订阅一个action
:
let request = yield take(ManagerUserActionTypes.GET_ALL_USER);
request
其实是action
返回的object
,其中包含着actionType
和相应的参数:
let pageNum = request.pageNum||1;
根据action
传递过来的参数,请求数据:
let response = yield call(fetch_users,pageNum);// fetch_users用户发起请求,获取所有用户列表数据
如果请求成功,封装需要的数据格式,触发更新state
的另一个action
,刷新页面
yield put({type:ManagerUserActionTypes.RESOLVE_GET_ALL_USERS,data:data})
开始编写页面内容
通过上面的内容,我们已经创建完成了store, action, reducer
部分的所有内容,下面就是要在每个页面上通过触发相应的action
完成页面里需要的逻辑操作。
1. IndexApp
IndexApp
是博客的入口,我们已在这个页面上定义了页面展示的所有route
:
<Router>
<div>
<Switch>
<Route path='/404' component={NotFound}/>
<Route path='/admin' component={Admin}/>
<Route component={Front}/>
</Switch>
</Router>
现在,我们需要在页面上添加一些内容:
- 通过
mapStateToProps
方法,从store
中取出notification, isFetching, userInfo
三个state
用于页面上消息的展示,请求状态,以及当前登录用户信息
function mapStateToProps(state) {
return {
notification: state.globalState.msg,
isFetching: state.globalState.isFetching,
userInfo: state.globalState.userInfo,
}
}
- 通过
mapDispatchToProps
方法,取出clear_msg
,user_auth
这两个action
,用于获取当前用户信息和处理用户点击清除消息通知时的操作
function mapDispatchToProps(dispatch) {
return {
clear_msg: bindActionCreators(clear_msg, dispatch),
user_auth: bindActionCreators(user_auth, dispatch)
}
}
- 我们希望当首页加载完成后,就调用
user_auth
的方法,触发获取用户信息的action
,需要用到componentDidMount
,该方法在页面加载完成后调用:
componentDidMount() {
this.props.user_auth();
}
-
render
方法中添加Loading
这个组件,并根据消息内容控制是否展示消息通知
render() {
let {isFetching} = this.props;
return (
<Router>
<div>
<Switch>
<Route path='/404' component={NotFound}/>
<Route path='/admin' component={Admin}/>
<Route component={Front}/>
</Switch>
{isFetching && <Loading/>}
{this.props.notification && this.props.notification.content ?
(this.props.notification.type === 1 ?
this.openNotification('success', this.props.notification.content) :
this.openNotification('error', this.props.notification.content)) :
null}
</div>
</Router>
)
}
2. Front
Front
这个容器也是一个路由容器,控制显示文章列表页和文章详情页:
class Front extends Component {
render() {
const {url} = this.props.match;
return(
<div>
<div >
<Switch>
<Route exact path={url} component={Home}/>
<Route path={`/detail/:id`} component={Detail}/>
<Route path={`/:tag`} component={Home}/>
<Route component={NotFound}/>
</Switch>
</div>
<BackTop />
</div>
)
}
}
我们要在这个container
里获取所有的标签,以及默认标签下的所有文章内容,用户Home
容器下文章的展示,首先引用需要的模块:
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { actions } from '../../reducers/adminManagerTags'
import { actions as FrontActinos } from '../../reducers/frontReducer'
const { get_all_tags } = actions;
const { get_article_list } = FrontActinos;
map
需要的state
和action
function mapStateToProps(state) {
return{
categories:state.admin.tags,
userInfo: state.globalState.userInfo
}
}
function mapDispatchToProps(dispatch) {
return{
get_all_tags:bindActionCreators(get_all_tags,dispatch),
get_article_list:bindActionCreators(get_article_list,dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Front)
3. Home
map
需要的state
和actions
function mapStateToProps(state) {
return {
tags: state.admin.tags,
pageNum: state.front.pageNum,
total: state.front.total,
articleList: state.front.articleList,
userInfo: state.globalState.userInfo
}
}
function mapDispatchToProps(dispatch) {
return {
get_article_list: bindActionCreators(get_article_list, dispatch),
get_article_detail:bindActionCreators(get_article_detail,dispatch),
login: bindActionCreators(IndexActions.get_login, dispatch),
register: bindActionCreators(IndexActions.get_register, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Home);
Home
这个containers
要处理的内容有:
- 用户点击
Header
部分的图标时,显示登录和注册的功能 - 显示所有的标签
- 显示选中标签对应的文章列表
- 分页内容
登录注册部分我们使用antd
中的Modal
来显示:
<Modal visible={this.state.showLogin} footer={null} onCancel={this.onCancel}>
{this.props.userInfo.userId ?
<Logined history={this.props.history} userInfo={this.props.userInfo}/> :
<Login login={this.props.login} register={this.props.register}/>}
</Modal>
Header
里传入一个方法,当点击时,修改state
中的showLogin
,来控制显示和隐藏
<Header handleLogin={this.handleLogin}/>
handleLogin = () => {
const current = !this.state.showLogin;
this.setState({ showLogin: current })
}
Login
和Logined
是两个新添加的component
用来显示登录注册数据框和登录用户信息。
在componentDidMount
方法中,需要调用获取文章列表的action
方法:
componentDidMount() {
this.props.get_article_list(this.props.match.params.tag || '')
}
store
中文章列表对应的state
更新后,页面会render
,文章列表通过ArticleList
这个component
被渲染出来:
<ArticleList
history={this.props.history}
data={this.props.articleList}
getArticleDetail={this.props.get_article_detail}
/>
store
中存储了total
这个state
,表示当前文章列表的总页数,我们使用antd
中的Pagination
组件来处理分页问题:
import { Pagination } from 'antd';
<Pagination
defaultPageSize={5}
onChange={(pageNum) => {
this.props.get_article_list(this.props.match.params.tag || '', pageNum);
}}
current={this.props.pageNum}
total={this.props.total}
/>
4. Detail
文章详情页的核心是显示markdown
文本,这里我们使用了remark-react
来渲染页面
render() {
const {articleContent,title,author,viewCount,commentCount,time} = this.props;
return(
<div className={style.container}>
<div className={style.header}>
<h1>{title}</h1>
</div>
<div className={style.main}>
<div id='preview' >
<div className={style.markdown_body}>
{remark().use(reactRenderer).processSync(articleContent).contents}
</div>
</div>
</div>
</div>
)
}
5. 后台管理页面
后台管理页面用于数据的管理,需要做一些判断,控制用户权限。
render() {
const { url } = this.props.match;
if(this.props.userInfo&&this.props.userInfo.userType){
return (
<div>
{
this.props.userInfo.userType === 'admin' ?
<div className={style.container}>
<div className={style.menuContainer}>
<AdminMenu history={this.props.history} />
</div>
<div className={style.contentContainer}>
<Switch>
<Route exact path={url} component={AdminIndex}/>
<Route path={`${url}/managerUser`} component={AdminManagerUser}/>
<Route path={`${url}/managerTags`} component={AdminManagerTags}/>
<Route path={`${url}/newArticle`} component={AdminNewArticle}/>
<Route path={`${url}/managerArticle`} component={AdminManagerArticle}/>
<Route path={`${url}/managerComment`} component={AdminManagerComment}/>
<Route path={`${url}/detail`} component={Detail}/>
<Route component={NotFound}/>
</Switch>
</div>
</div>
:
<Redirect to='/' />
}
</div>
)
} else {
return <NotFound/>
}
}
只要用户登录,并且登录用户的type
为admin
时,才有权限进入后台管理页面。
6. 用户管理页面
用户管理页面现阶段只用于注册用户展示,想扩展的同学,可以加上用户权限修改和删除用户的功能。
7. 新建文章页面
新建文章和修改文章对应的state
,都是state.admin.newArticle
,这一点需要注意。页面展开时,需要将该页面对应的actions
和reducers
map到此页面。新建和文章分为标题,正文,描述和标签4部分,牵扯到的action
比较多:
function mapStateToProps(state) {
const {title, content, desc, tags} = state.admin.newArticle;
let tempArr = state.admin.tags;
for (let i = 0; i < tempArr.length; i++) {
if (tempArr[i] === '首页') {
tempArr.splice(i, 1);
}
}
return {
title,
content,
desc,
tags,
tagsBase: tempArr
}
}
function mapDispatchToProps(dispatch) {
return {
update_tags: bindActionCreators(update_tags, dispatch),
update_title: bindActionCreators(update_title, dispatch),
update_content: bindActionCreators(update_content, dispatch),
update_desc: bindActionCreators(update_desc, dispatch),
get_all_tags: bindActionCreators(get_all_tags, dispatch),
save_article: bindActionCreators(save_article, dispatch)
}
}
我在文章底部放了三个按钮:
- 预览
预览功能可类比于文章详情,使用Modal
和remark-react
渲染。 - 保存
//保存
saveArticle() {
let articleData = {};
articleData.title = this.props.title;
articleData.content = this.props.content;
articleData.desc = this.props.desc;
articleData.tags = this.props.tags;
articleData.time = dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss');
articleData.isPublish = false;
this.props.save_article(articleData);
};
保存时,设置文章的isPublish
属性为false
,及表示未发表状态
- 发表
//发表
publishArticle() {
let articleData = {};
articleData.title = this.props.title;
articleData.content = this.props.content;
articleData.desc = this.props.desc;
articleData.tags = this.props.tags;
articleData.time = dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss');
articleData.isPublish = true;
this.props.save_article(articleData);
};
总结
博客的主要页面功能就介绍到这里,没有提及的页面,可以参考工程代码。该篇文章关于redux
的使用介绍紧紧围绕最初state
的设计,也是redux
比较基本的使用场景。对于初学者来说可能会有点晕,不要怕,对照着源代码一步一步的完成每一个页面,完成这个博客demo后,你对react的熟练度一定会有提升。
系列文章
React技术栈+Express+Mongodb实现个人博客
React技术栈+Express+Mongodb实现个人博客 -- Part 1 博客页面展示
React技术栈+Express+Mongodb实现个人博客 -- Part 2 后台管理页面
React技术栈+Express+Mongodb实现个人博客 -- Part 3 Express + Mongodb创建Server端
React技术栈+Express+Mongodb实现个人博客 -- Part 4 使用Webpack打包博客工程
React技术栈+Express+Mongodb实现个人博客 -- Part 5 使用Redux
React技术栈+Express+Mongodb实现个人博客 -- Part 6 部署