内容回顾
前面的篇幅主要介绍了:
- 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(
,
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(
,
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
使用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
:
现在,我们需要在页面上添加一些内容:
- 通过
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 (
{isFetching && }
{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}
)
}
2. Front
Front
这个容器也是一个路由容器,控制显示文章列表页和文章详情页:
class Front extends Component {
render() {
const {url} = this.props.match;
return(
)
}
}
我们要在这个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
来显示:
{this.props.userInfo.userId ?
:
}
Header
里传入一个方法,当点击时,修改state
中的showLogin
,来控制显示和隐藏
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
被渲染出来:
store
中存储了total
这个state
,表示当前文章列表的总页数,我们使用antd
中的Pagination
组件来处理分页问题:
import { Pagination } from 'antd';
{
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(
{title}
{remark().use(reactRenderer).processSync(articleContent).contents}
)
}
5. 后台管理页面
后台管理页面用于数据的管理,需要做一些判断,控制用户权限。
render() {
const { url } = this.props.match;
if(this.props.userInfo&&this.props.userInfo.userType){
return (
{
this.props.userInfo.userType === 'admin' ?
:
}
)
} else {
return
}
}
只要用户登录,并且登录用户的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 部署