后台小白前端入门--React+React-Router+Redux

导语:

其实demo早在四五月份就已经写好了,只是那段时间后台项目太忙,加上自己拖延症晚期,到最近才写这篇文章。文章结构大概从介绍本文使用的前端技术,使用的原因出发,同时结合代码进行使用讲解,最后在配上如何基于本文demo进行进一步开发,尽量做到浅显易懂,延续系列的特点。这篇文章应该是我近期内写的最后一篇前端文章。写完这篇文章,也算是从React系前端勉强毕业了,同时也是给自己这一年来前端学习的一个交代。文章有点长,大家慢慢看。也可以直接到最底下获取我的github地址,直接下载demo跑起来,只需要一个npm start,童嫂无欺。
React

框架概念介绍

所使用的相关前端技术框架:React+React-Router+Redux+webpack2.0,ui框架用的是Ant Design。异步请求用的是fetch api

1.React:这个就不用介绍了吧,这个还用介绍,估计这篇文章不太适合你。这个不熟的建议先学习一下,可以从我之前写的后台小白前端入门--React看起。

2.Reac-Router:使用React-Router,可以让你轻松使用React进行多页面开发。React—Router是React官方的貌似也是唯一的React路由解决方案。简单来说,就是url改变,但是不经过后台,在前端进行路由,根据url的不同选择并渲染不同的路由组件。React-Router其实没有多少东西,就一个路由容器,还有一些api,现在也出中文文档了,可以跟着官方文档的demo敲一遍就好了。

3.Redux:个人认为这是技术栈里面比较难学的一个。不仅你要学习Redux是什么,也要学习如何和React搭配使用。简而言之,Redux就是在浏览器内存中弄了一个容器,用于存放及管理js状态的地方。然后通过一系列的规范和限制,让你能清晰地管理这些状态,同时用这些状态去改变页面。

使用React做过复杂页面的人可能会深有体会,当一个嵌套了三四层的组件,最底层的子组件想要改变父组件的状态,使得父组件通过这个状态去改变另外一个最底层的子组件的时候,不使用消息订阅方式的时候,无非就是写一个回调一直传到子组件供其调用,来改变这个父组件的状态(其实就是父组件的state),然后父组件又把这个状态一层层传到另外一个子组件,然后另外一个子组件通过props值获取到状态的改变,来渲染页面。这种代码写起来非常地恶心,一多起来,基本没办法阅读和维护。

所以这个时候,你就需要在一个地方去统一管理这些状态了,让所有的组件都去这个地方获取状态的变化,或者改变状态。这个就是Redux做的事情。React最初起步的时候,由官方推出的Flux也是做着类似的事情。不过后来Redux的出现,以更加清晰的store机制和使用方式,取代了Flux成为了主流。我自己也试着写过Flux的代码,写起来Redux会更简洁一些,所以这里就选择使用Redux。

Redux学习的话,可以先从Redux中文文档开始看起,大概了解一下Redux的相关机制。如果英文好一下的话,可以去看一个英文的Redux使用视频,大概20多个,照着敲下来,应该也差不多。当然,本文后续也会介绍Redux结合React,React-Router使用。

4.框架作用小总结:为什么写个React还要加着么多东西呢?简单来说,就是你在最初使用React开发页面的时候,你发现你的页面只有一个url(单页),没办法做到通过url到达不同的页面,然后你使用React-Router,就可以使用React进行“多页面”开发。然后你的网站越来越复杂了,抽出来的公共组件越来越多,页面组件嵌套越来越深,状态(父组件于子组件之间的state)之间的关系影响越来越复杂。这时候你就需要使用Redux来统一管理这些状态了。

上面提到的相关资料站,都可以从本文最底下获取到相关的链接,大家按需获取。本demo的相关代码放到了github里面,链接也放到底下了。

框架使用及说明

下面将介绍框架代码的结构和使用。先附上目录结构,介绍各个文件夹存放的文件及文件的结构:

front
|---actions(使用Redux开发,对应的action都放到这个目录下,稍后详解)
|---build(webpack打包编译后生成的js文件,实际html引用的也是这里的文件)
|---common(一些公共的变量和api都放在这个文件夹下)
|---css
|---data(node服务器mock后台数据的文件放在这)
|---jsx(组件代码文件都放在这)
|---node_modules
|---reducers(使用Redux开发,对应的action都放到这个目录下,稍后详解)
|---store(使用Redux开发,store文件存放路径)
|---index.html(主页面)
|---package.json(npm)
|---proxy.config.js(node服务器请求接口映射配置文件)
|---webpack.config.js

建议大家把demo下下来直接跑起来,先安装好node和webpack,然后在front目录下,使用npm install安装以来,再使用webpack打包,最后使用npm start启动node服务器,然后浏览器访问http://localhost:8000/即可。demo比较简单,只做了登陆页,注册页和主页。不过也够用了。不知道node和webpack用法的可以先在我的上一篇博客搭个简单例子,你就会有所了解了。后台小白前端入门--React前端框架搭建Demo及介绍

启动后,你就能看到这样的页面:


image.png

点击右上角的名字,退出,可以跳到登陆页,登陆页点注册,可到注册页。

关键代码详解

1.jsx/index.jsx

//省略引入第三方库的代码。
import store, {history} from '../store/store.jsx';
class RouterComponent extends React.Component {
    render() {
        return (
            <Provider store={store}>
                <Router history={history}>
                    <Route path={env.homePage} component={ContainerComponent}>
                        <IndexRoute component={MainComponent}/>
                        <Route path={env.login } component={LoginComponent}/>
                        <Route path={env.register } component={RegisterComponent}/>
                    </Route>
                </Router>
            </Provider>
        );
    }
}

前面说到,我们已经将state交给了Redux进行管理,放在了store里面。那么如何在负责渲染页面的组件里面获取到这些state呢?在React-Redux中,提供了Provider组件,把它作为根组件,同时把store作为参数传给它,你就能在嵌套在它下面的组件中拿到state。

接下来就是Router组件了,Router组件使用起来和不用Redux一样,这里就不用详细介绍。不过我们可以把history和store做个绑定,下面有详细介绍。

2.store/store.jsx

//省略引入第三方库的代码。
import * as reducers from '../reducers/rootreducer.jsx';

const rootReducer = combineReducers({
    ...reducers,
    routing: RouterReducer
});

const store = createStore(rootReducer, applyMiddleware(thunk));

export const history = syncHistoryWithStore(browserHistory, store);

export default store;

上面的combineReducers,用于将多个负责改变不同的state的reducer合并成同一个reducer函数。这样你就可以分别根据业务情况编写不同的reducer了,代码结构清晰。

接着是syncHistoryWithStore,用于将History中的变化反映到store里的state中。这里我自己也不是很清楚,demo里面应该是没有用到对应的例子,因为我把这个代码换成export const history = browserHistory; 编译运行都正常,有兴趣的话可以做个深入研究。

3.jsx/component/MainComponent.jsx

接下来,写完上面那两个配置文件。React+React-Router+Redux的框架就简单的搭建起来了。现在开始写一些比较简单的页面组件。下面主页的代码比较简单,相信大家看懂应该不费吹灰之力。如果我们没有用到Redux,其实就是把下面的loadData改成异步请求,同创建组件的相关state,把请求回来的参数传给这个内部的state,通过state组件的更新来更新页面。代码如下:

//以上略去了使用Redux需要声明和绑定和函数,后面会给出。
export default class MainComponent extends React.Component {
    constructor(props) {
        super(props);

    }

    loadData() {
        //这里是Redux的写法,如果是不使用Redux,则直接写异步请求并且将返回的数据setState到本组件的state中。
        const {actions} = this.props;
        actions.fetchArticleList();
    }


    componentDidMount() {
        this.loadData();
    }

    render() {
        var articleListHtml = [];
        //这里是Redux的写法,如果是不使用Redux,则直接从this.state中获得数据。
        const {articleList} = this.props;
        if (common.isNotEmpty(articleList) && articleList.length > 0) {
            for (var i = 0; i < articleList.length; i++) {
                articleListHtml.push(
                    <tr key={i}>
                        <td className="td-align-center" style={{width:"50%"}}>
                            <a href="" target="_blank">{articleList[i].title}</a>
                        </td>
                        <td className="td-align-center" style={{width:"25%"}}>{articleList[i].creator}</td>
                        <td className="td-align-center"
                            style={{width:"25%"}}>{articleList[i].commentNum}/{articleList[i].readedNum}</td>
                    </tr>
                )
            }
        }
        return (
            <div>
                <Row type="flex" justify="center">
                    <h1>博文列表</h1>
                </Row>
                <Row type="flex" className="add-code-lib-row standard-margin-top">
                    <Col span={24}>
                        <div className="code-manage-main">
                            <table>
                                <thead>
                                <tr>
                                    <th className="td-align-center">标题</th>
                                    <th className="td-align-center">作者</th>
                                    <th className="td-align-center">回复/查看</th>
                                </tr>
                                </thead>
                                <tbody>
                                {articleListHtml}
                                </tbody>
                            </table>
                        </div>
                    </Col>
                </Row>
            </div>
        );
    }
}

--.Redux怎么用,用的时候该注意些什么
写完UI组件,这里有必要花一些篇幅简单介绍一下Redux的一些机制和原则,以便于实在不想看Redux是什么以及怎么用的读者理解接下来的代码。

前面有说到,Redux就是用来管理状态(state)的,不过它自己有一套自己的管理机制和规范,大概的示意图如下:

image.png

(1)UI组件通过connet,绑定action和state(即图中的1和2),使得组件可以dispatch action,同时也能在props里面获取到store里对应的state。

(2)组件生成action,并且进行dispatch(即图中的3),这里主要为了调用异步请求,获取服务端数据。

(3)请求成功或者失败后,会再在3的Action里,dispatch一个Action(图中的4),这个action,才是用来通知reducer,让其去改变store中对应的状态的。

(4)所有的Reducer都会收到该请求(图中的5),如果符合条件的reducer会更改store中的state,并且返回新的state到store中,如果不符合,则直接返回旧的state,不做任何更改。

上面大概就是使用Redux的大致开发流程及数据流向了。同时Redux规定了3条原则,来保证store中状态改变和使用足够清晰。1.只有一个store,所有的状态都存在于这个store中。2.通过action来触发store中状态的改变,reducer执行具体的state变化。其它任何时间任何方式的state是只读的。3.reducer函数必须是一个纯函数,即对应于任何两个相同的输入,只会有一种相同的输出。举个例子,你在这个纯函数里面,Redux是不允许你使用诸如Math.random()或者new Date()等函数来改变state的。这样做是为了保证状态变化的可预测性和清晰性。

通过上面一系列原理和原则,Redux让前端开发者和维护者对前端的代码和逻辑更佳清楚易懂,对于状态变化清晰可预测。

4.reducers/articleList.jsx

import * as constants from '../common/constants.jsx'

const initialState = {};

export default function (state = initialState, action) {
    switch (action.type) {
        case constants.RECEIVE_ARTICLELIST:
            const {articleList} = action;
            return {
                ...state,
                articleList
            };
            break;
        default:
            return state
    }
}


//同时在common/constants.jsx中添加如下内容,因为action和reducer都会用到,所以放倒一个公共变量里面比较好。编程习惯编程习惯
export const RECEIVE_ARTICLELIST = 'RECEIVE_ARTICLELIST';

//在reducers/rootreducer.jsx中添加如下代码,使得上面写的reducer能绑定到store中。
export articleList from './articleList.jsx'


Redux部分的代码讲解将按照Redux流程从后到前讲解,即从reducer,action,再到前端UI component 的顺序,方便大家更加清楚地了解如何进行Redux开发。

上面部分的代码就是写一个负责改变文章列表状态的reducer,当有一个type为'RECEIVE_ARTICLELIST'的action被分派的时候,这个reducer会进行响应,同时修改状态里的articleList,重新计算并返回新的state到store中。需要说明的是,当一个action被分派的时候,所有绑定的reducer都会被触发执行。如果符合则更新状态,如果没有则直接返回原state(即default:return state),store里的状态就不会变化。

我们写完了这个reducer,就要把它绑定到store中,前面我们已经通过如下代码进行配置了,所以只需要在reducers/rootreducer.jsx 添加即可绑定到store中。


import * as reducers from '../reducers/rootreducer.jsx';
const rootReducer = combineReducers({
    ...reducers,
    routing: RouterReducer
});

const store = createStore(rootReducer, applyMiddleware(thunk));

5.actions/receiveArticleList.jsx

接着往前写,写到Action,这里分派到Reducer的Action,是一个对象,其中有一个type属性,是必须的,用来和reducer里面接受参数里面的action.type是一致的,也就是constants.jsx中的RECEIVE_ARTICLELIST = 'RECEIVE_ARTICLELIST'。另外你可以在这个对象里面添加一些其它的参数,以便于reducer匹配到以后拿这些参数去改变store中的状态。如下代码:

const constants = require('../common/constants.jsx');

export function receiveArticleList(articleList) {
    return {
        type: constants.RECEIVE_ARTICLELIST,
        articleList
    }
}

6.actions/fetchArticleList.jsx

这里这个Action,其实在我看来,是一个伪“action”,因为它并没有分派到reducer中,而是触发另外一个Action。这里这个Action是由UI组件分派的,为了进行异步请求,在请求成功或者失败后,再分派一个真正的Action,去改变store中的state。

在Redux中,把异步请求数据的代码和逻辑放在Action里面是比较好的。当然你可以直接在UI组件里面直接进行异步请求,然后派发Action去改变store中的状态。但是这么做的话,你的业务代码和你的UI组件又耦合在一起了。代码也不够通用,感觉不太好。这部分的代码如下:

import fetch from 'isomorphic-fetch'
require('es6-promise').polyfill();
import * as actions from './actions.jsx';
import env from '../common/env.js';

export function fetchArticleList() {
    return (dispatch, getState) => {
        fetch(env.getArticleList, {
            credentials: 'same-origin',
            headers: {
                "Content-Type": "application/json"
            }
            // body: formData
        }).then(function (response) {
            if (response.status >= 400) {
                throw new Error("Bad response from server");
            }
            return response.json();
        }).then(function (result) {
            var code = result.code;
            if (code == "0") {
                    //这里才是真正触发Aciton的地方,去store的状态。
                dispatch(actions.receiveArticleList(result.data))
            }
        }.bind(this)).catch(function (error) {
            alert("请求失败,请检查网络");
            //这里才是真正触发Aciton的地方,去store的状态。
            dispatch(actions.receiveArticleList([]))
        });
    }
}


7.jsx/component/MainComponent.jsx

最后,又回到了我们的UI组件。我们在前面已经写好了异步请求的Action,改变Store中状态的Action,以及如何改变Store状态的Reducer。

接下来,就是UI组件如何去获取到store中的状态,如何去直接或者间接(需要异步请求的时候)去派发一个Action,以及store中的state变化时,UI组件是如何过获取到变化的。先贴上代码,下面再进行代码说明。

import env from '../../common/env.js';
import common from '../../common/common.js';
import {fetchArticleList} from '../../actions/actions.jsx';
import {connect} from 'React-Redux';

const mapDispatchToProps = dispatch => {
    return {
        actions: {
            fetchArticleList: () => dispatch(fetchArticleList())
        }
    }
};


// Map Redux state to component props
const mapStateToProps = createSelector(
    state => state.articleList.articleList,
    (articleList) => {
        return {
            articleList
        }
    }
);


@connect(mapStateToProps, mapDispatchToProps)
export default class MainComponent extends React.Component {

//这部分的代码和  3.jsx/component/MainComponent.jsx中贴出的代码一样,不再给出。

}

接下里,我们一一解决上面的问题。

我们首先写一个mapDispatchToProps函数,封装一下需要派发的action,使得UI组件中的代码能比较方便调用action。

其次,再写一个mapStateToProps函数,返回想要订阅的store的状态(在本例子中,就是存储在store中的articleList)。需要额外说明的是,这个state.articleList.articleList,
中间的articleList,是你在rootreducer.jsx中export的值。

最后使用React-Redux库中的connet,将上面两个函数绑定到UI组件上。这样,UI组件就可以通过this.props中拿到store中的状态值了,并且每一次store中的状态值变化,也会触发React组件中props的改变,触发React生命周期的相关函数回调。同时,如果UI组件中想要触发一次Action派发,也是在props里面进行this.props.actions.fetchArticleList();即可。上面两个函数已经把action和store中的state都绑定到props对象里了。

至此,已经把整个Demo的关键代码进行一一说明。详细的代码建议大家去我的github上下载下来看。这里就不提供全量代码了。

基于demo代码进行相关开发步骤

上面代码已经详细到,从零开始搭一套React大礼包了。但是如果你实在不想自己搭一套,去折腾什么依赖包问题,webpack问题,中间件问题,fetch请求兼容性问题等一大堆问题。想在本demo上直接开发,也是可以的。大概步骤如下:

1.写好UI组件。

2.在index.jsx中配置好路由。

如果有异步请求,则还需要以下步骤:

3.在constants.jsx中设置action.type,供reducer和action使用。

4.在reducers文件夹中添加reducer。

5.在rootreducer。jsx中添加4中的文件

6.在actions文件夹中添加相关action,如果有ajax请求,建议分成fetch,receive两个action

7.在actions。jsx中添加6中的文件

8.在对应的组件中编写对应的逻辑。etc:@connect(mapStateToProps, mapDispatchToProps)等

总结

当然,这个demo代码比较简单,只是为了用React大礼包而用React大礼包,太过于复杂的代码也不适合进行讲解说明。而且,这套框架搭起来了,仍然有很多问题,很多规范需要去解决。比如什么时候把组件中的state放到store中,怎样接着精简代码,去掉rootreducer.jsx、actions.jsx等等等等。都值得进行深入的学习和研究,也欢迎大家这篇文章下面评论讨论,提出解决方案。

想把这套框架吃下来,合理运用起来,光是这些代码和学习也是远远不够的。还需要更多的学习和实践,不过希望这篇文章提供一些信心,成为一个好的起点。

临走之前,我还有话要说

其实在15年的时候就已经开始写前端代码了,用的是jQuery。然后到去年,开始认真研究React及其相关生态系统。感觉现在的前端,处于一个群雄割据的年代。老派的还在用jQuery,然后现在比较主流的三大框架React,Vue,Angular,都占有不小的使用比例。这对于前端学习者,我觉得已经算是不小的负担,并且这些技术还在飞速迭代着。当然这也无可厚非,大家都想一统天下,但是,在这种急功近利的趋势,导致很多时候,自己都没统一自己。我在用webpack的时候,刚好经历webpack1过渡到webpack2,然后发现webpack2竟然不兼容webpack1,要做代码升级及兼容。听前端的同时说,Angular2和Angular1,差别也很大。这一系列的变动,也导致了对应的插件和api库跟着不兼容,需要升级。导致这个过程非常的痛苦。暂别前端,希望下次回来,前端有一番新气象。

相关技术资料站

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容