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阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容