React技術(shù)棧+Express+Mongodb實(shí)現(xiàn)個(gè)人博客 -- Part 5

內(nèi)容回顧

前面的篇幅主要介紹了:

本篇文章主要介紹使用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)在你可以把Reactmodel看作是一個(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è)東西:Providerconnect,具體使用文后說(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ù)。

工作流程圖如下:


redux工作流程圖

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ù):mapStateToPropsmapDispatchToProps。它們定義了 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-sagaRedux的一個(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ì)的:

state設(shè)計(jì)

reduxstore包含的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)建actionreducer時(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è)面所需要的所有ActionsReducers,這里講文件分離出來(lái),便于管理。里面涉及的代碼,請(qǐng)查看工程源碼,這里就不貼出來(lái)了。

reducer文件目錄

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);
        });
    }
  • rootSagaredux-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)行的

image.png

當(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_msguser_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需要的stateaction

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需要的stateactions

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 })
    }

LoginLogined是兩個(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/>
        }

    }

只要用戶登錄,并且登錄用戶的typeadmin時(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)的actionsreducersmap到此頁(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ù)覽功能可類比于文章詳情,使用Modalremark-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 部署

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容