內容回顧
前面的篇幅主要介紹了:
- 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 部署