React技術棧+Express+Mongodb實現個人博客 -- Part 5

內容回顧

前面的篇幅主要介紹了:

本篇文章主要介紹使用redux將數據渲染到每個頁面,如何使用redux-saga處理異步請求的actions

Redux

隨著 JavaScript 單頁應用開發日趨復雜,JavaScript需要管理比任何時候都要多的 state(狀態)。 這些state 可能包括服務器響應、緩存數據、本地生成尚未持久化到服務器的數據,也包括UI 狀態,如激活的路由,被選中的標簽,是否顯示加載動效或者分頁器等等。如果一個model的變化會引起另一個 model 變化,那么當view 變化時,就可能引起對應 model 以及另一個 model的變化,依次地,可能會引起另一個 view的變化。亂!

這時候Redux就強勢登場了,現在你可以把Reactmodel看作是一個個的子民,每一個子民都有自己的一個狀態,紛紛擾擾,各自維護著自己狀態,我行我素,那哪行啊!太亂了,我們需要一個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, 它主要有提供兩個東西:Providerconnect,具體使用文后說明。

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 樹。

工作流程圖如下:


redux工作流程圖

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方法接受兩個參數:mapStateToPropsmapDispatchToProps。它們定義了 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-sagaRedux的一個中間件,主要集中處理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是怎么設計的:

state設計

reduxstore包含的state分為三個部分:

  • front , 負責博客頁面展示的數據
  • globalState,負責當前網絡請求狀態,登錄用戶信息和消息提示
  • admin,負責后臺管理頁面的數據

先設計好全局的state,下面在創建actionreducer時就更清晰了。

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包含了后臺管理頁面所需要的所有ActionsReducers,這里講文件分離出來,便于管理。里面涉及的代碼,請查看工程源碼,這里就不貼出來了。

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的調試工具需要在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);
        });
    }
  • 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是指非阻塞任務調用,區別于call方法,call可以用來發起異步操作,但是相對于generator函數來說,call操作是阻塞的,只有等promise回來后才能繼續執行,而fork是非阻塞的 ,當調用fork啟動一個任務時,該任務在后臺繼續執行,從而使得我們的執行流能繼續往下執行而不必一定要等待返回。

先來回顧一下redus的工作流,便于我們理解saga是如何運行的

image.png

當一個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_msguser_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需要的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這個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 })
    }

LoginLogined是兩個新添加的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/>
        }

    }

只要用戶登錄,并且登錄用戶的typeadmin時,才有權限進入后臺管理頁面。

6. 用戶管理頁面

用戶管理頁面現階段只用于注冊用戶展示,想擴展的同學,可以加上用戶權限修改和刪除用戶的功能。

7. 新建文章頁面

新建文章和修改文章對應的state,都是state.admin.newArticle,這一點需要注意。頁面展開時,需要將該頁面對應的actionsreducersmap到此頁面。新建和文章分為標題,正文,描述和標簽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)
    }
}

我在文章底部放了三個按鈕:

  • 預覽
    預覽功能可類比于文章詳情,使用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);
   };

保存時,設置文章的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 部署

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容