內(nèi)容回顧
前面的篇幅主要介紹了:
- React技術(shù)棧+Express+Mongodb實(shí)現(xiàn)個(gè)人博客 -- Part 1 博客頁(yè)面展示
- React技術(shù)棧+Express+Mongodb實(shí)現(xiàn)個(gè)人博客 -- Part 2 后臺(tái)管理頁(yè)面
- React技術(shù)棧+Express+Mongodb實(shí)現(xiàn)個(gè)人博客 -- Part 3 Express + Mongodb創(chuàng)建Server端
- React技術(shù)棧+Express+Mongodb實(shí)現(xiàn)個(gè)人博客 -- Part 4 使用Webpack打包博客工程
本篇文章主要介紹使用redux將數(shù)據(jù)渲染到每個(gè)頁(yè)面,如何使用redux-saga處理異步請(qǐng)求的actions
Redux
隨著 JavaScript 單頁(yè)應(yīng)用開(kāi)發(fā)日趨復(fù)雜,JavaScript需要管理比任何時(shí)候都要多的 state(狀態(tài))。 這些state 可能包括服務(wù)器響應(yīng)、緩存數(shù)據(jù)、本地生成尚未持久化到服務(wù)器的數(shù)據(jù),也包括UI 狀態(tài),如激活的路由,被選中的標(biāo)簽,是否顯示加載動(dòng)效或者分頁(yè)器等等。如果一個(gè)model的變化會(huì)引起另一個(gè) model 變化,那么當(dāng)view 變化時(shí),就可能引起對(duì)應(yīng) model 以及另一個(gè) model的變化,依次地,可能會(huì)引起另一個(gè) view的變化。亂!
這時(shí)候Redux就強(qiáng)勢(shì)登場(chǎng)了,現(xiàn)在你可以把React的model看作是一個(gè)個(gè)的子民,每一個(gè)子民都有自己的一個(gè)狀態(tài),紛紛擾擾,各自維護(hù)著自己狀態(tài),我行我素,那哪行啊!太亂了,我們需要一個(gè)King來(lái)領(lǐng)導(dǎo)大家,我們就可以把Redux看作是這個(gè)King。網(wǎng)羅所有的組件組成一個(gè)國(guó)家,掌控著一切子民的狀態(tài)!防止有人叛亂生事!
這個(gè)時(shí)候就把組件分成了兩種:容器組件(redux或者路由)和展示組件(子民)。
- 容器組件:即
redux或是router,起到了維護(hù)狀態(tài),出發(fā)action的作用,其實(shí)就是King高高在上下達(dá)指令。 - 展示組件:不維護(hù)狀態(tài),所有的狀態(tài)由容器組件通過(guò)
props傳給他,所有操作通過(guò)回調(diào)完成。
| 展示組件 | 容器組件 | |
|---|---|---|
| 作用 | 描述如何展現(xiàn)(骨架、樣式) | 描述如何運(yùn)行(數(shù)據(jù)獲取、狀態(tài)更新) |
| 直接使用 Redux | 否 | 是 |
| 數(shù)據(jù)來(lái)源 | props | 監(jiān)聽(tīng) Redux state |
| 數(shù)據(jù)修改 | 從 props 調(diào)用回調(diào)函數(shù) | 向 Redux 派發(fā) actions |
| 調(diào)用方式 | 手動(dòng) | 通常由 React Redux 生成 |
Redux三大部分:store, action, reducer。相當(dāng)于King的直系下屬。
可以看出Redux是一個(gè)狀態(tài)管理方案,在React中維系King和組件關(guān)系的庫(kù)叫做 react-redux, 它主要有提供兩個(gè)東西:Provider 和 connect,具體使用文后說(shuō)明。
1. store
Store 就是保存數(shù)據(jù)的地方,它實(shí)際上是一個(gè)Object tree。整個(gè)應(yīng)用只能有一個(gè) Store。這個(gè)Store可以看做是King的首相,掌控一切子民(組件)的活動(dòng)state。
Redux 提供createStore這個(gè)函數(shù),用來(lái)生成 Store。
import { createStore } from 'redux';
const store = createStore(func);
createStore接受一個(gè)函數(shù)作為參數(shù),返回一個(gè)Store對(duì)象(首相誕生記)
我們來(lái)看一下Store(首相)的職責(zé):
- 維持應(yīng)用的 state;
- 提供 getState() 方法獲取 state;
- 提供 dispatch(action) 方法更新 state;
- 通過(guò) subscribe(listener) 注冊(cè)監(jiān)聽(tīng)器;
- 通過(guò) subscribe(listener) 返回的函數(shù)注銷監(jiān)聽(tīng)器。
2. action
State 的變化,會(huì)導(dǎo)致 View 的變化。但是,用戶接觸不到State,只能接觸到 View。所以,State 的變化必須是View 導(dǎo)致的。Action 就是 View 發(fā)出的通知,表示State 應(yīng)該要發(fā)生變化了。即store的數(shù)據(jù)變化來(lái)自于用戶操作。action就是一個(gè)通知,它可以看作是首相下面的郵遞員,通知子民(組件)改變狀態(tài)。它是store 數(shù)據(jù)的唯一來(lái)源。一般來(lái)說(shuō)會(huì)通過(guò) store.dispatch() 將 action 傳到 store。
Action 是一個(gè)對(duì)象。其中的type屬性是必須的,表示Action的名稱。
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
};
Action創(chuàng)建函數(shù):
Action 創(chuàng)建函數(shù) 就是生成 action 的方法?!癮ction” 和 “action 創(chuàng)建函數(shù)” 這兩個(gè)概念很容易混在一起,使用時(shí)最好注意區(qū)分。
在 Redux 中的 action 創(chuàng)建函數(shù)只是簡(jiǎn)單的返回一個(gè)action:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
這樣做將使 action 創(chuàng)建函數(shù)更容易被移植和測(cè)試。
3. reducer
Action 只是描述了有事情發(fā)生了這一事實(shí),并沒(méi)有指明應(yīng)用如何更新 state。而這正是 reducer 要做的事情。也就是郵遞員(action)只負(fù)責(zé)通知,具體你(組件)如何去做,他不負(fù)責(zé),這事情只能是你們村長(zhǎng)reducer告訴你如何去做。
專業(yè)解釋: Store 收到 Action 以后,必須給出一個(gè)新的 State,這樣 View 才會(huì)發(fā)生變化。這種State 的計(jì)算過(guò)程就叫做Reducer。
Reducer 是一個(gè)函數(shù),它接受 Action 和當(dāng)前 State 作為參數(shù),返回一個(gè)新的 State。
const reducer = function (state, action) {
// ...
return new_state;
};
4. 數(shù)據(jù)流
嚴(yán)格的單向數(shù)據(jù)流是Redux 架構(gòu)的設(shè)計(jì)核心。
Redux 應(yīng)用中數(shù)據(jù)的生命周期遵循下面 4 個(gè)步驟:
- 調(diào)用 store.dispatch(action)。
- Redux store 調(diào)用傳入的 reducer 函數(shù)。
- 根 reducer 應(yīng)該把多個(gè)子 reducer 輸出合并成一個(gè)單一的 state 樹(shù)。
- Redux store 保存了根 reducer 返回的完整 state 樹(shù)。
工作流程圖如下:

5. connect
Redux 默認(rèn)并不包含 React 綁定庫(kù),需要單獨(dú)安裝。
npm install --save react-redux
當(dāng)然,我們這個(gè)實(shí)例里是不需要的,所有需要的依賴已經(jīng)在package.json里配置好了。
React-Redux提供connect方法,用于從UI組件生成容器組件。connect的意思,就是將這兩種組件連起來(lái)。
import { connect } from 'react-redux';
export default connect()(Home);
上面代碼中Home是個(gè)UI組件,TodoList就是由 React-Redux 通過(guò)connect方法自動(dòng)生成的容器組件。
而只是純粹的這樣把Home包裹起來(lái)毫無(wú)意義,完整的connect方法這樣使用:
export default connect(
mapStateToProps,
mapDispatchToProps
)(Home);
上面代碼中,connect方法接受兩個(gè)參數(shù):mapStateToProps和mapDispatchToProps。它們定義了 UI 組件的業(yè)務(wù)邏輯。前者負(fù)責(zé)輸入邏輯,即將state映射到 UI 組件的參數(shù)props,后者負(fù)責(zé)輸出邏輯,即將用戶對(duì)UI組件的操作映射成 Action。
6. Provider
這個(gè)Provider其實(shí)是一個(gè)中間件,它是為了解決讓容器組件拿到King的指令(state對(duì)象)而存在的。
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在根組件外面包了一層,這樣一來(lái),App的所有子組件就默認(rèn)都可以拿到state了。
Redux-Saga
React作為View層的前端框架,自然少不了很多中間件Redux Middleware做數(shù)據(jù)處理, 而redux-saga就是其中之一,下面仔細(xì)介紹一個(gè)這個(gè)中間件的具體使用流程和應(yīng)用場(chǎng)景。
1. 簡(jiǎn)介
Redux-saga是Redux的一個(gè)中間件,主要集中處理react架構(gòu)中的異步處理工作,被定義為generator(ES6)的形式,采用監(jiān)聽(tīng)的形式進(jìn)行工作。
2. 安裝
使用npm進(jìn)行安裝:
npm install --save redux-saga
3. redux Effects
Effect 是一個(gè)javascript 對(duì)象,可以通過(guò) yield 傳達(dá)給 sagaMiddleware 進(jìn)行執(zhí)行在, 如果我們應(yīng)用redux-saga,所有的Effect 都必須被yield才會(huì)執(zhí)行。
舉個(gè)例子,我們要改寫(xiě)下面這行代碼:
yield fetch(url);
應(yīng)用saga:
yield call(fetch, url)
3. take
等待 dispatch 匹配某個(gè) action。
比如下面這個(gè)例子:
....
while (true) {
yield take('CLICK_Action');
yield fork(clickButtonSaga);
}
....
4. put
觸發(fā)某個(gè)action, 作用和dispatch相同:
yield put({ type: 'CLICK' });
舉個(gè)例子:
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 相同。通常會(huì)與reselect庫(kù)配合使用。
6. call
有阻塞地調(diào)用 saga 或者返回 promise 的函數(shù),只在觸發(fā)某個(gè)動(dòng)作。
傳統(tǒng)意義講,我們很多業(yè)務(wù)邏輯要在action中處理,所以會(huì)導(dǎo)致action的處理比較混亂,難以維護(hù),而且代碼量比較大,如果我們應(yīng)用redux-saga會(huì)很大程度上簡(jiǎn)化代碼, redux-saga 本身也有良好的擴(kuò)展性, 非常方便的處理各種復(fù)雜的異步問(wèn)題。
回到博客中
首先回到博客頁(yè)面的入口,引入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是一個(gè)容器,為了配合熱更新,需要在最外層添加這層容器。 -
configureStore返回一個(gè)store,其中引入了redux-saga中間件,會(huì)嗎會(huì)介紹。 -
IndexApp是之前的首頁(yè)路由配置,這里把它分離出來(lái),簡(jiǎn)化代碼結(jié)構(gòu)。
State
在開(kāi)始介紹每個(gè)頁(yè)面之前,先來(lái)看一下博客這個(gè)工程State是怎么設(shè)計(jì)的:

redux的store包含的state分為三個(gè)部分:
- front , 負(fù)責(zé)博客頁(yè)面展示的數(shù)據(jù)
- globalState,負(fù)責(zé)當(dāng)前網(wǎng)絡(luò)請(qǐng)求狀態(tài),登錄用戶信息和消息提示
- admin,負(fù)責(zé)后臺(tái)管理頁(yè)面的數(shù)據(jù)
先設(shè)計(jì)好全局的state,下面在創(chuàng)建action和reducer時(shí)就更清晰了。
Actions and Reducers
在src目錄下新建一個(gè)文件夾reducers,并新建一個(gè)文件index.js。這個(gè)文件是總的reducer,包括上面提到的admin,globalState,front三個(gè)部分。
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"
};
// 生產(chǎn)action的函數(shù)方法
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;
}
}
細(xì)心的同學(xué)會(huì)問(wèn),獲取文章列表的action為什么會(huì)有兩個(gè),都代表什么意思?
GET_ARTICLE_LIST: "GET_ARTICLE_LIST",
RESPONSE_ARTICLE_LIST: "RESPONSE_ARTICLE_LIST",
獲取文章列表時(shí),會(huì)發(fā)起一個(gè)網(wǎng)絡(luò)請(qǐng)求,請(qǐng)求發(fā)起時(shí),會(huì)執(zhí)行get_article_list這個(gè)方法,觸發(fā)GET_ARTICLE_LIST這個(gè)action,這個(gè)action會(huì)在store中被中間件redux-saga接收:
let req = yield take(FrontActionTypes.GET_ARTICLE_LIST);
接收后,會(huì)執(zhí)行方法
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: '網(wǎng)絡(luò)請(qǐng)求錯(cuò)誤', msgType: 0});
} finally {
yield put({type: IndexActionTypes.FETCH_END})
}
}
getArticleList這個(gè)方法會(huì)發(fā)起請(qǐng)求,獲取數(shù)據(jù),如果成功獲取數(shù)據(jù),變觸發(fā)RESPONSE_ARTICLE_LIST這個(gè)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});
}
}
這就是為什么會(huì)有GET_ARTICLE_LIST: "GET_ARTICLE_LIST", RESPONSE_ARTICLE_LIST: "RESPONSE_ARTICLE_LIST",兩個(gè)ActionType的原因。這里涉及到了redux-saga,后面會(huì)做更詳細(xì)的介紹。
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
}
}
這個(gè)文件處理的Action有
-
FETCH_START請(qǐng)求開(kāi)始,更新isFetching這個(gè)state為true,頁(yè)面上開(kāi)始轉(zhuǎn)圈 -
FETCH_END請(qǐng)求結(jié)束,更新isFetching這個(gè)state為false,頁(yè)面上停止轉(zhuǎn)圈 -
USER_LOGIN用戶發(fā)起登錄請(qǐng)求, -
USER_REGISTER用戶發(fā)起注冊(cè)請(qǐng)求 -
RESPONSE_USER_INFO登錄或注冊(cè)成功返回用戶信息 -
SET_MESSAGE通知store更新頁(yè)面的notification信息,顯示消息內(nèi)容,提示用戶,例如登錄失敗等 -
USER_AUTH頁(yè)面打開(kāi)時(shí)獲取用戶歷史登錄信息
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包含了后臺(tái)管理頁(yè)面所需要的所有Actions 和Reducers,這里講文件分離出來(lái),便于管理。里面涉及的代碼,請(qǐng)查看工程源碼,這里就不貼出來(lái)了。

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的調(diào)試工具需要在createStore()步驟中添加一個(gè)中間件:
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可以監(jiān)聽(tīng)我們的組件變化并做出即時(shí)相應(yīng),但卻無(wú)法監(jiān)聽(tīng)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是指非阻塞任務(wù)調(diào)用,區(qū)別于call方法,call可以用來(lái)發(fā)起異步操作,但是相對(duì)于generator函數(shù)來(lái)說(shuō),call操作是阻塞的,只有等promise回來(lái)后才能繼續(xù)執(zhí)行,而fork是非阻塞的 ,當(dāng)調(diào)用fork啟動(dòng)一個(gè)任務(wù)時(shí),該任務(wù)在后臺(tái)繼續(xù)執(zhí)行,從而使得我們的執(zhí)行流能繼續(xù)往下執(zhí)行而不必一定要等待返回。
先來(lái)回顧一下redus的工作流,便于我們理解saga是如何運(yùn)行的

當(dāng)一個(gè)
Action被出發(fā)時(shí),首先會(huì)到達(dá)Middleware處,我們?cè)趧?chuàng)建store時(shí),添加了saga這個(gè)中間件。所以action會(huì)首先到達(dá)saga里面,我們會(huì)在saga里處理這個(gè)action,例如發(fā)送網(wǎng)絡(luò)請(qǐng)求,得到相應(yīng)的數(shù)據(jù),然后再出發(fā)另一個(gè)action,告知reduce去更新state。
舉例看一下get_all_users_flow這個(gè)saga的內(nèi)容,其他的內(nèi)容請(qǐng)查看工程源代碼
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: '網(wǎng)絡(luò)請(qǐng)求錯(cuò)誤', 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方法可以訂閱一個(gè)action:
let request = yield take(ManagerUserActionTypes.GET_ALL_USER);
request其實(shí)是action返回的object,其中包含著actionType和相應(yīng)的參數(shù):
let pageNum = request.pageNum||1;
根據(jù)action傳遞過(guò)來(lái)的參數(shù),請(qǐng)求數(shù)據(jù):
let response = yield call(fetch_users,pageNum);// fetch_users用戶發(fā)起請(qǐng)求,獲取所有用戶列表數(shù)據(jù)
如果請(qǐng)求成功,封裝需要的數(shù)據(jù)格式,觸發(fā)更新state的另一個(gè)action,刷新頁(yè)面
yield put({type:ManagerUserActionTypes.RESOLVE_GET_ALL_USERS,data:data})
開(kāi)始編寫(xiě)頁(yè)面內(nèi)容
通過(guò)上面的內(nèi)容,我們已經(jīng)創(chuàng)建完成了store, action, reducer部分的所有內(nèi)容,下面就是要在每個(gè)頁(yè)面上通過(guò)觸發(fā)相應(yīng)的action完成頁(yè)面里需要的邏輯操作。
1. IndexApp
IndexApp是博客的入口,我們已在這個(gè)頁(yè)面上定義了頁(yè)面展示的所有route:
<Router>
<div>
<Switch>
<Route path='/404' component={NotFound}/>
<Route path='/admin' component={Admin}/>
<Route component={Front}/>
</Switch>
</Router>
現(xiàn)在,我們需要在頁(yè)面上添加一些內(nèi)容:
- 通過(guò)
mapStateToProps方法,從store中取出notification, isFetching, userInfo三個(gè)state用于頁(yè)面上消息的展示,請(qǐng)求狀態(tài),以及當(dāng)前登錄用戶信息
function mapStateToProps(state) {
return {
notification: state.globalState.msg,
isFetching: state.globalState.isFetching,
userInfo: state.globalState.userInfo,
}
}
- 通過(guò)
mapDispatchToProps方法,取出clear_msg,user_auth這兩個(gè)action,用于獲取當(dāng)前用戶信息和處理用戶點(diǎn)擊清除消息通知時(shí)的操作
function mapDispatchToProps(dispatch) {
return {
clear_msg: bindActionCreators(clear_msg, dispatch),
user_auth: bindActionCreators(user_auth, dispatch)
}
}
- 我們希望當(dāng)首頁(yè)加載完成后,就調(diào)用
user_auth的方法,觸發(fā)獲取用戶信息的action,需要用到componentDidMount,該方法在頁(yè)面加載完成后調(diào)用:
componentDidMount() {
this.props.user_auth();
}
-
render方法中添加Loading這個(gè)組件,并根據(jù)消息內(nèi)容控制是否展示消息通知
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這個(gè)容器也是一個(gè)路由容器,控制顯示文章列表頁(yè)和文章詳情頁(yè):
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>
)
}
}
我們要在這個(gè)container里獲取所有的標(biāo)簽,以及默認(rèn)標(biāo)簽下的所有文章內(nèi)容,用戶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這個(gè)containers要處理的內(nèi)容有:
- 用戶點(diǎn)擊
Header部分的圖標(biāo)時(shí),顯示登錄和注冊(cè)的功能 - 顯示所有的標(biāo)簽
- 顯示選中標(biāo)簽對(duì)應(yīng)的文章列表
- 分頁(yè)內(nèi)容
登錄注冊(cè)部分我們使用antd中的Modal來(lái)顯示:
<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里傳入一個(gè)方法,當(dāng)點(diǎn)擊時(shí),修改state中的showLogin,來(lái)控制顯示和隱藏
<Header handleLogin={this.handleLogin}/>
handleLogin = () => {
const current = !this.state.showLogin;
this.setState({ showLogin: current })
}
Login和Logined是兩個(gè)新添加的component用來(lái)顯示登錄注冊(cè)數(shù)據(jù)框和登錄用戶信息。
在componentDidMount方法中,需要調(diào)用獲取文章列表的action方法:
componentDidMount() {
this.props.get_article_list(this.props.match.params.tag || '')
}
store中文章列表對(duì)應(yīng)的state更新后,頁(yè)面會(huì)render,文章列表通過(guò)ArticleList這個(gè)component被渲染出來(lái):
<ArticleList
history={this.props.history}
data={this.props.articleList}
getArticleDetail={this.props.get_article_detail}
/>
store中存儲(chǔ)了total這個(gè)state,表示當(dāng)前文章列表的總頁(yè)數(shù),我們使用antd中的Pagination組件來(lái)處理分頁(yè)問(wèn)題:
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
文章詳情頁(yè)的核心是顯示markdown文本,這里我們使用了remark-react來(lái)渲染頁(yè)面
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. 后臺(tái)管理頁(yè)面
后臺(tái)管理頁(yè)面用于數(shù)據(jù)的管理,需要做一些判斷,控制用戶權(quán)限。
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時(shí),才有權(quán)限進(jìn)入后臺(tái)管理頁(yè)面。
6. 用戶管理頁(yè)面
用戶管理頁(yè)面現(xiàn)階段只用于注冊(cè)用戶展示,想擴(kuò)展的同學(xué),可以加上用戶權(quán)限修改和刪除用戶的功能。
7. 新建文章頁(yè)面
新建文章和修改文章對(duì)應(yīng)的state,都是state.admin.newArticle,這一點(diǎn)需要注意。頁(yè)面展開(kāi)時(shí),需要將該頁(yè)面對(duì)應(yīng)的actions和reducersmap到此頁(yè)面。新建和文章分為標(biāo)題,正文,描述和標(biāo)簽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] === '首頁(yè)') {
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)
}
}
我在文章底部放了三個(gè)按鈕:
- 預(yù)覽
預(yù)覽功能可類比于文章詳情,使用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);
};
保存時(shí),設(shè)置文章的isPublish屬性為false,及表示未發(fā)表狀態(tài)
- 發(fā)表
//發(fā)表
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);
};
總結(jié)
博客的主要頁(yè)面功能就介紹到這里,沒(méi)有提及的頁(yè)面,可以參考工程代碼。該篇文章關(guān)于redux的使用介紹緊緊圍繞最初state的設(shè)計(jì),也是redux比較基本的使用場(chǎng)景。對(duì)于初學(xué)者來(lái)說(shuō)可能會(huì)有點(diǎn)暈,不要怕,對(duì)照著源代碼一步一步的完成每一個(gè)頁(yè)面,完成這個(gè)博客demo后,你對(duì)react的熟練度一定會(huì)有提升。
系列文章
React技術(shù)棧+Express+Mongodb實(shí)現(xiàn)個(gè)人博客
React技術(shù)棧+Express+Mongodb實(shí)現(xiàn)個(gè)人博客 -- Part 1 博客頁(yè)面展示
React技術(shù)棧+Express+Mongodb實(shí)現(xiàn)個(gè)人博客 -- Part 2 后臺(tái)管理頁(yè)面
React技術(shù)棧+Express+Mongodb實(shí)現(xiàn)個(gè)人博客 -- Part 3 Express + Mongodb創(chuàng)建Server端
React技術(shù)棧+Express+Mongodb實(shí)現(xiàn)個(gè)人博客 -- Part 4 使用Webpack打包博客工程
React技術(shù)棧+Express+Mongodb實(shí)現(xiàn)個(gè)人博客 -- Part 5 使用Redux
React技術(shù)棧+Express+Mongodb實(shí)現(xiàn)個(gè)人博客 -- Part 6 部署