React全家桶之初体验

Author Avatar
黄广杰 4月 15, 2017
  • 在其它设备中阅读本文章

接触React也有段时间了,但是从初学者的角度来看,官网的资料实在是少,看起来比较吃力,并且是英文的,而那中文的网站却也是翻译的乱七八糟的,无力吐槽。我作为一个初学者,我谈不上有什么高深见解,偶尔有一些心得与体会,将之分享给与我一样的刚入门的同学,如果您已经比较熟悉这些知识了,请忽视此篇。另外我想说明的是,此文不是教程或者文档,关于理解也不够深刻,有偏差或者误解的地方欢迎指正。下面是从我的角度去解释如何实现一些简单的例子。如果有些概念不理解可以看React学习资源汇总,这里有详细的文档教大家如何去学习React。
因为facebook已经完全掏拥抱ES6标准,所以我们也遵循官网的标准进行编写我们的代码。我是以案例去说明这个react全家桶(我在此案例代码编辑的过程中,不在乎文件夹的划分,全部代码写在一个文件里面,实际开发中请注意区分)。

React全家桶成员及初步项目搭建

关于全家桶的理解

大家都知道react.js仅仅是一个UI库,说白了,它只关心views的渲染,其它的都不管。为了做出大型web应用,他还需要其它插件帮忙才行。
React全家桶里有非常多的技术及内容要学。在此讲不完所有的成员,因为根据项目需要,会有增减。所以,我只从核心组成方面讲几个,以达到学习react的组合开发的效果。
我列举来讲的全家桶是:ES6 + react + redux + immutable.js + webpack + yarn

搭建项目

利用yarn(也可以使用npm)包管理工具搭建我们的项目,前提是必须先安装yarn。

$ npm i -g yarn-cli

安装完yarn之后,利用yarn进行初始化项目

$ yarn init -y

安装包依赖,也就是我们的核心的技术包支持

$ yarn add react react-dom babel babel-core babel-loader babel-preset-es2015 babel-preset-stage-0 babel-preset-react webpack webpack-dev-server

安装完所有的依赖包,在package.json文件中再添加一个配置项,用于热布署与实时监测修改自动刷新浏览器:

"scripts": {
    "watch": "webpack-dev-server --port 8080 --colors"
}

则此package.json文件为:

{
  "name": "react-family",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "babel": "^6.23.0",
    "babel-core": "^6.24.1",
    "babel-loader": "^6.4.1",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-0": "^6.24.1",
    "react": "^15.5.4",
    "react-dom": "^15.5.4",
    "webpack": "^2.4.1",
    "webpack-dev-server": "^2.4.2"
  },
  "scripts": {
    "watch": "webpack-dev-server --port 8080 --colors"
  }
}

然后添加一个文件webpack.config.js,具体怎么进行各项配置在此不做过多解释,直接上代码:

var webpack = require('webpack');

module.exports = {
    entry: {
        index: ['./index.js'],
        vendor: [
            'react',
            'react-dom'
        ]
    },
    output: {
        filename: '[name].bundle.js',
        path: __dirname + '/',
        publicPath: '/public'
    },
    module: {
        loaders: [{
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            query: {
                presets: ['react','es2015','stage-0']
            }
        }]
    },
    resolve: {
        extensions: [' ', '.js', '.json', '.scss']
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({name: 'vendor',filename:'vendor.bundle.js'})
    ]
}

OK,我们的项目脚手架已经搭建完成。接下来就可以开始coding了。
添加一个入口文件index.html则可进行我们react的开发。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>react-family</title>
</head>
<body>
    <div id="root"></div>
    <script type="text/javascript" src="/public/vendor.bundle.js"></script>
    <script type="text/javascript" src="/public/index.bundle.js"></script>
</body>
</html>

React组件

需求一

请根据如下需求实现一个计数器:

  1. 利用reactjs实现一个加减计器Couter
  2. 点“+”加1,点“-”减1

基本形式

因为需求非常简单,我们可以只利用react的一个单独的组件就可以完成这个需求。

import React, { Component } from 'react';
import { render } from 'react-dom';

class Counter extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }
    handleSub(number) {
        this.setState({count: --number });
    }
    handleAdd(number) {
        this.setState({count: ++number })
    }
    render() {
        let count = this.state.count;
        let input;
        return (
            <div>
                <input type="button" value="-" onClick={() => this.handleSub(input.value)} />
                <input type="text" value={count} style={{width: "50px", textAlign: "center"}} ref={node => {input = node}} />
                <input type="button" value="+" onClick={() => this.handleAdd(input.value)} />
            </div>
        )
    }
}

render(
    <Counter />,
    document.getElementById('root')
)

利用React的状态state可变的特性,可以持续修改状态值,并实时更新UI。

精简形式

可以将代码写得更精简一些:

...
class Counter extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }
    render() {
        let count = this.state.count;
        return (
            <div>
                <input type="button" value="-" onClick={() => this.setState({count: --count})} />
                <input type="text" value={count} style={{width: "50px", textAlign: "center"}} />
                <input type="button" value="+" onClick={() => this.setState({count: ++count})} />
            </div>
        )
    }
}
...

我们的核心就是通过更新state来改变数据状态。这样子,我们就可以直接去掉ref属性,我们不需要拿到数据,只需要在state的原基础之上进行修改就可以达到数据的更新。而事件绑定,我们更不需要另外多写更多逻辑,直接利用es6的方式就可以进行快速实现。

无状态组件

实际开发中,我们写的React组件都是分成两种形式,一种是纯组件,叫无状态组件,只用于显示,不做数据操作逻辑,一般存放在components目录。另一种是就是数据操作组件,是将数据与无状态组件绑定在一起连接的组件,一般放在containers目录。当然我们因为例子太简单,所以不进行目录划分,但要分开来写。
上面的需求,我们完全可以利用无状态组件进行操作。划分为逻辑组件与无状态组件。便于开发维护,抽出来的无状态组件为:

const CounterDOM = ({ count, sub, add }) => (
    <div>
        <input type="button" value="-" onClick={sub} />
        <input type="text" value={count} style={{width: "50px", textAlign: "center"}} />
        <input type="button" value="+" onClick={add} />
    </div>
)

而得到的数据组件为:

class Counter extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }
    render() {
        let count = this.state.count;
        return <CounterDOM
                count={count}
                sub={() => this.setState({count: --count})}
                add={() => this.setState({count: ++count})} />
    }
}

在维护组件的时候,如果做显示修改,我们只要修改显示组件,如果做数据更新只做数据组件的更改。

上面的例子中涉及到父子组件之间进行通信,而子组件实际上只负责显示,则在子组件只取props组件进行渲染,些案件中({ count, sub, add })中的内容,里面的对象即为props的解构过程,而做交互、做数据修改则在父组件上做更新。“无状态组件没有实例”,即不能使用任何诸如 componentWillMount 或 shouldComponentUpdate 的生命周期方法。

Redux架构

我们知道,组件分为数据与显示,那么我们应该把数据的逻辑也分开来。官方推荐使用redux进行数据层的处理。我们根据上面的需求,使用redux构架进行拆分与组装,将数据层的结构交给redux去管理,让react负责渲染层。这样子可以让显示与数据再加的松藕合了。

我们在使用redux的时候,你可能不需要它,Redux 是一个有用的架构,但不是非用不可。事实上,大多数情况,你可以不用它,只用 React 就够了。“只有遇到 React 实在解决不了的问题,你才需要 Redux 。”

使用之前得先安装redux:

$ yarn add redux

使用redux必须要理解的概念:action, reducer, store, dispatch, subscribe。关于redux的资料可以看redux中文教程

createStore

而redux提供了一个createStore方法,用于创建一个唯一的store树,是整个应用的数据仓库。我们就可以将整个应用的数据结构与处理过程交由store来管理。
先来尝试一下redux:

import { createStore } from 'redux';

// Actions
const SUB = 'SUB';
const sub = () => ({type: SUB});
const ADD = 'ADD';
const add = () => ({type: ADD});

// Reducers
const counter = (state = 0, action) => {
    switch(action.type) {
        case SUB: return state - 1;
        case ADD: return state + 1;
        default: return state;
    }
}

const store = createStore(counter);
store.dispatch(sub())
console.log(store.getState())
// -1

我们知道state数据是被存在一个叫做store的对象中。而store是唯一的,所有的state对象都是经由这个store进行处理之后,再返回一个新的state。
而引发更新这一事件过程的对象就叫做action。由它触发事件之后,交给纯函数reducers处理之后返回一个新的state对象。
我们学会了使用 action 来描述“发生了什么”,和使用 reducers 来根据 action 更新 state 的用法。
store就是把它们联系到一起的对象。Store 有以下职责:

  1. 维持应用的 state;
  2. 提供 getState() 方法获取 state;
  3. 提供 dispatch(action) 方法更新 state;
  4. 通过 subscribe(listener) 注册监听器;
  5. 通过 subscribe(listener) 返回的函数注销监听器。

如此,我们结合到显示组件上去,就实现了显示与数据分离管理。

import React, { Component } from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';

// Actions
const SUB = 'SUB';
const sub = () => ({type: SUB});
const ADD = 'ADD';
const add = () => ({type: ADD});

// Reducers
const counter = (state = 0, action) => {
    switch(action.type) {
        case SUB: return state - 1;
        case ADD: return state + 1;
        default: return state;
    }
}

// store
const store = createStore(counter);

// conponents
const CounterDOM = ({ count, sub, add }) => (
    <div>
        <input type="button" value="-" onClick={sub} />
        <input type="text" value={count} style={{width: "50px", textAlign: "center"}} />
        <input type="button" value="+" onClick={add} />
    </div>
)
// containers
class Counter extends Component {
    render() {
        let count = store.getState();
        return (
            <CounterDOM
                count={count}
                sub={() => store.dispatch(sub())}
                add={() => store.dispatch(add())}
            />
        )
    }
}
// render
const renderDOM = () => render(
    <Counter />,
    document.getElementById('root')
)

renderDOM();
store.subscribe(renderDOM);

如此,我们的state就不是直接在原来的基础之上修改了。而是交给dispatch发起一个action,由reducer监听到action触发的事件就运算处理,返回一个新的state,再由react组件中的自动state自动渲染机制更新UI。整个过程都是数据单向流动的,过程中如下:
views -> action -> reducer -> views
我们觉得上面的做法还是有些麻烦,虽然我们把数据与显示分开了,但是在数据组件的时候,还要传很多参数,并且在组件里面直接调用store对象进行数据操作了。虽然react与redux结合起来使用了,但还需要一些额外的工具,其中最重要的莫过于react-redux了。

Provider、connect

react-redux 提供了两个重要的对象,Providerconnect,前者使 React 组件可被连接(connectable),后者把 React 组件和 Redux 的 store 真正连接起来。

import React, { Component } from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';

// Actions
const SUB = 'SUB';
const sub = () => ({type: SUB});
const ADD = 'ADD';
const add = () => ({type: ADD});

// Reducers
const counter = (state = 0, action) => {
    switch(action.type) {
        case SUB: return state - 1;
        case ADD: return state + 1;
        default: return state;
    }
}

// store
const store = createStore(counter);

// conponents
const CounterDOM = ({ count, sub, add }) => (
    <div>
        <input type="button" value="-" onClick={sub} />
        <input type="text" value={count} style={{width: "50px", textAlign: "center"}} />
        <input type="button" value="+" onClick={add} />
    </div>
)

// containers
const Counter = connect(
    state => ({count: state}),
    dispatch => {
        return {
            sub: () => dispatch(sub()),
            add: () => dispatch(add())
        }
    }
)(CounterDOM)

// render
render(
    <Provider store={store}>
        <Counter />
    </Provider>,
    document.getElementById('root')
)

我们可以利用Provider将组件包起来。然后利用connect将其与数据层连接在一起。这样子。我们就不需要在组件中定义与编写我们的发布与订阅的监听了。然后,我们可以将这些逻辑写到我们的mapDispatchToProps函数中去。而我们的获取到的初始化值可以在mapStateToProps函数中写,返回一个纯js对象。这两个函数可以当作参数传给connect复合函数进行连接。更利于连接react与redux的便捷,也解决了赋值的方便性。这种做法的好处不言而喻了。

combineReducers、bindActionCreators

假设,产品并不满足现在的需求。他想在的当前的基础之上再添加一个功能。

  1. 添加一个输入框,用户只能输入数字,作为商品单价;
  2. 点击计数器的时候,自动计算数量与单位的积;
  3. 添加一个显示积的标签用于显示积的值。

这时我们来添加一个action用于输入单价并且再添加一个reducer用于处理修改单价之后的积。我们现在有多个reducer,这时我们就必须将这些reducers都合并在一起,使用一个combineReducers的函数进行整合。

import React, { Component } from 'react';
import { render } from 'react-dom';
import { createStore, combineReducers } from 'redux';
import { Provider, connect } from 'react-redux';

// Actions
const SUB = 'SUB';
const sub = () => ({type: SUB});
const ADD = 'ADD';
const add = () => ({type: ADD});

const PRICE = 'PRICE';
const inputPrice = price => ({type: PRICE, price});

// Reducers
const counter = (state = 0, action) => {
    switch(action.type) {
        case SUB: return state - 1;
        case ADD: return state + 1;
        default: return state;
    }
}

const price = (state = 1, action) => {
    switch(action.type) {
        case PRICE: return action.price;
        default: return state;
    }
}

// store
const store = createStore(combineReducers({counter, price}));

// conponents
const DOM = ({ counter, sub, add, price, result, inputPrice }) => (
    <div>
        <div>
            <input type="button" value="-" onClick={sub} />
            <input type="text" value={counter} style={{width: "50px", textAlign: "center"}} />
            <input type="button" value="+" onClick={add}/>
        </div>
        <input type="text" value={price} style={{width: "94px"}} onChange={e => inputPrice(e.target.value)} />
        <p>{result}</p>
    </div>
)

// containers
const Counter = connect(
    state => ({
        ...state,
        result: state.counter * state.price
    }),
    dispatch => {
        return {
            sub: () => dispatch(sub()),
            add: () => dispatch(add()),
            inputPrice: price => dispatch(inputPrice(price))
        }
    }
)(DOM)

// render
render(
    <Provider store={store}>
        <Counter />
    </Provider>,
    document.getElementById('root')
)

而上面的写法还可以将actions都合并在一起,包装成一个个被dispatch处理的actions。

...
// containers
const Counter = connect(
    state => ({
        ...state,
        result: state.counter * state.price
    }),
    dispatch => bindActionCreators({sub,add,inputPrice}, dispatch)
)(DOM)
...

@connect指令

我们知道,上面的写法虽然很直白了。不过我们还可以省掉一步,将定义的组件传入指令@connect,作为一个参数。这样指令在执行的过程就自动将紧接着的一组件包装起来,将数据与其联系在一起,然后返回一个新的类(组件)。这样做更形象一些,并且达到了更松的藕合度。
使用之前别忘了先安装插件babel-plugin-transform-decorators-legacy

$ yarn add babel-plugin-transform-decorators-legacy

然后在webpack.config.js中添加一个配置项:

...
query: {
    presets: ['react','es2015','stage-0'],
    plugins: ['transform-decorators-legacy']
}
...

如此,代码可以这样写:

import React, { Component } from 'react';
import { render } from 'react-dom';
import { createStore, combineReducers, bindActionCreators } from 'redux';
import { Provider, connect } from 'react-redux';

// Actions
const SUB = 'SUB';
const sub = () => ({type: SUB});
const ADD = 'ADD';
const add = () => ({type: ADD});

const PRICE = 'PRICE';
const inputPrice = price => ({type: PRICE, price});

// Reducers
const counter = (state = 0, action) => {
    switch(action.type) {
        case SUB: return state - 1;
        case ADD: return state + 1;
        default: return state;
    }
}

const price = (state = 1, action) => {
    switch(action.type) {
        case PRICE: return action.price;
        default: return state;
    }
}

// store
const store = createStore(combineReducers({counter, price}));

// containers
@connect(
    state => ({
        ...state,
        result: state.counter * state.price
    }),
    dispatch => bindActionCreators({sub,add,inputPrice}, dispatch)
)
class Counter extends Component {
    render() {
        const { counter,price,result,sub,add,inputPrice } = this.props;
        return (
            <div>
                <div>
                    <input type="button" value="-" onClick={sub} />
                    <input type="text" value={counter} style={{width: "50px", textAlign: "center"}} />
                    <input type="button" value="+" onClick={add}/>
                </div>
                <input type="text" value={price} style={{width: "94px"}} onChange={e => inputPrice(e.target.value)} />
                <p>{result}</p>
            </div>
        )
    }
}

// render
render(
    <Provider store={store}>
        <Counter />
    </Provider>,
    document.getElementById('root')
)

注意,这里的@connect()指令,必须写在react组件之前,紧接着组件。@connect指令实际上等价于connect(mapStateToPropes,mapDispatchToProps)(DOM)这种调用方式。

中间件及包装函数applyMiddleware

我们的应用经过层层包装,从维护与可读性上都已经提高了好多。不过现在产品又加了个需求,就是监测中间的数据的变化过程,输出一个操作日志。介是目前action与reducer都不适合做这个逻辑操作,而react组件的功能也是主要用于显示的,也不适合。唯一可以做处理的就是在store插入中间个去处理。
我们的中间件相当于一个开箱即用的商品。即要就插入,不需要就拆除。操作简单,使用方便,不影响组件的正常功能。并且使用中间件还可以控制组件的变化,并且可以直接控制组件的变化与流程。
常用的中间件有redux-logger、redux-thunk、redux-promise、redux-saga等。
那需求中要求打印日志。则可以在createStore函数添加最后的参数做为中间件传入,接管createStore的功能,并返回一个store对象。
引入中间件及中间件包装函数applyMiddleware

import { createStore, bindActionCreators, combineReducers, applyMiddleware } from 'redux';
import loggerMiddleware from 'redux-logger';

只要修改createStore函数则可:

...
const store = createStore(combineReducers({counter, price}),applyMiddleware(loggerMiddleware));
...

关于中间件,也可以自己定义一个,感兴趣的同学可以看自定义中间件。而关于中间件的应用,在此不详细介绍。

Chrome开发工具插件react、redux devtools

使用Chrome浏览作为我们的react应用的开发浏览器,可以安装react devtools与redux devtools,安装过程需要翻墙。有需要的同学可以去下载安装Chrome扩展。
安装完成了react devtools可以直接查看页面的结构变成了

<Provider store={...}>
    <Connenct(Counter)>...</Connenct(Counter)>
</Provider>

安装完成了redux devtools,则还需要在我们的代码里面配置一个全局属性。
需要引入复合函数compose进行将其与中间件拼合:

...
const store = createStore(
    combineReducers({counter, price}),
    compose(
        applyMiddleware(loggerMiddleware),
        window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
    )
);
...

immutable.js

对于react来说,引入了redux架构,已经解决了显示与数据分开处理问题。但是还有一个问题就是,当一个应用是由很多组件组成的。如果父组件数据更新了,但子件数据并没有更新。这时所以组件都重新渲染一次,这将是一项非常浪费性能的问题。如果我们能做到谁数据变化,就只更新谁,那就完美地解决了性能的问题。
毫无疑问,immutable.js将是我们最好的帮手。而immutable是何物?官网上称是:不可以被改变的数据,我的理解就是一旦创建,它将是不可以改变的。就好像定义一个恒量一样。比如es6中利用const定义的变量。利用其数据的不可变性,我们就可以实现做数据层面的state更新的时候,就不需要进行深拷贝,我们都知道深拷贝是非常耗内存的事(因为复杂对象的拷贝是要多层循环嵌套)。immutable的使用方式及实现原理可以去官网查看文档。
先安装immutable.js

$ yarn add immutable

这时我不在用对象的合成方式返回新的纯对象。而是返回一个immutable对象。整个数据层的流动,只传immutable对象,操作与修改都是不可变数据,将运行的效率达到极致。

import React, { Component } from 'react';
import { render } from 'react-dom';
import { createStore, bindActionCreators, combineReducers } from 'redux';
import { Provider, connect } from 'react-redux';
import { Map } from 'immutable';

// Actions
const SUB = 'SUB';
const sub = () => ({type: SUB});
const ADD = 'ADD';
const add = () => ({type: ADD});

const PRICE = 'PRICE';
const inputPrice = price => ({type: PRICE, price});

// Reducers
const counter = (state = Map({counter: 0}), action) => {
    switch(action.type) {
        case SUB: return state.update('counter', v => v - 1);
        case ADD: return state.update('counter', v => v + 1);
        default: return state;
    }
}

const price = (state = Map({price: 1}), action) => {
    switch(action.type) {
        case PRICE: return state.update('price', () => action.price);
        default: return state;
    }
}

// store
const store = createStore(combineReducers({counter, price}));

// containers
@connect(
    state => state,
    dispatch => bindActionCreators({sub,add,inputPrice}, dispatch)
)
class Counter extends Component {
    render() {
        const { counter, price, sub, add, inputPrice } = this.props;
        const result = counter.get('counter') * price.get('price');
        return (
            <div>
                <div>
                    <input type="button" value="-" onClick={sub} />
                    <input type="text" value={counter.get('counter')} style={{width: "50px", textAlign: "center"}} />
                    <input type="button" value="+" onClick={add}/>
                </div>
                <input type="text" value={price.get('price')} style={{width: "94px", textAlign: "center"}} onChange={e => inputPrice(e.target.value)} />
                <p style={{width: "94px", textAlign: "center"}}>{result}</p>
            </div>
        )
    }
}

// render
render(
    <Provider store={store}>
        <Counter />
    </Provider>,
    document.getElementById('root')
)

异步Action数据流处理

上面所有的需求都是在不请求服务器的基础上做的,明显这种情况是很少的。实际开发中,基本上都与服务器请求有关,所以处理异步Action是不可避免的。
处理异步数据流,首先得定义异步的Action。上面的中间件中已经提到过applyMiddleware这个函数,此函数实际上就是将中间件做重新包装,最终在中间件里面实现同步的action发出。

需求二

现有需求如下:

  1. 设计一个手机号码属地查询功能;
  2. 输入手机号,点查询,在下方显示电话归属地信息;
  3. 手机归属地开放数据api接口:http://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=15850781443,其中tel为传入的请求参数。

异步Action

关于异步action的处理及原理,不多说了。下面说一下如何做这个需求。
根据要求先设计出同步Actions

const GET_MOBILE = 'GET_MOBILE';
const getMobile = mobile => ({type: GET_MOBILE, mobile});

const FETCHING = 'FETCHING';
const fetchingMobile = fetching => ({type: FETCHING, fetching});

设计一个异步Action,用于请求接口数据,在此异步请求方式我们使用了fetch-jsonp插件

$ yarn add fetch-jsonp

引入fetch-jsonp可以用于处理跨域请求数据(注意:跨域请求的数据必须要服务器提供jsonp格式的数据结构,否则会报token错误)。

const fetchMobile = mobile => dispatch => {
    dispatch(fetchingMobile(true));
    return fetchJsonp(`https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=${mobile}`)
        .then(response => response.json())
        .then(json => json)
        .then(mobile => dispatch(getMobile(mobile)))
        .then(() => dispatch(fetchingMobile(false)))
}

异步中间件

fetchMobile这个Action返回的结果实际上不在是一个单纯的js对象,而是一个函数。官方针对异步数据的处理提供了几个插件:redux-thunk、redux-promise、redux-saga,我们用比较简单的redux-thunk实现其功能。
安装redux-thunk插件:

$ yarn add redux-thunk

利用此中间件包装我们的reducer,就可以实现处理异步action的方法。最终实现的逻辑为:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { render } from 'react-dom';
import { createStore, combineReducers, bindActionCreators, applyMiddleware, compose } from 'redux';
import { Provider, connect } from 'react-redux';
import thunkMiddleware from 'redux-thunk';
import { Map } from 'immutable';
import fetchJsonp from 'fetch-jsonp';
import 'bootstrap/dist/css/bootstrap.css';

const GET_MOBILE = 'GET_MOBILE';
const getMobile = mobile => ({type: GET_MOBILE, mobile});

const FETCHING = 'FETCHING';
const fetchingMobile = fetching => ({type: FETCHING, fetching});


const fetchMobile = mobile => dispatch => {
    dispatch(fetchingMobile(true));
    return fetchJsonp(`https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=${mobile}`)
        .then(response => response.json())
        .then(json => json)
        .then(mobile => dispatch(getMobile(mobile)))
        .then(() => dispatch(fetchingMobile(false)))
}

const mobile = (state = Map({mobile: {}, fetching: false, list: []}), action) => {
    switch(action.type) {
        case GET_MOBILE: return state.update('mobile',() => action.mobile);
        case FETCHING: return state.update('fetching',() => action.fetching);
        default: return state;
    }
}


const store = createStore(combineReducers({mobile}), compose(applyMiddleware(thunkMiddleware), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()));


const Search = ({ fetchMobile, isFetching }) => {
    let input;
    return (
        <div className="col-lg-12">
            <div className="input-group">
                <input type="text" className="form-control" placeholder="输入手机号码" ref={node => {input = node}}/>
                <span className="input-group-btn">
                    <button className="btn btn-default" type="button" onClick={() => fetchMobile(input.value)}>{ isFetching ? "查询中..." : "查询" }</button>
                </span>
            </div>
        </div>
    )
}
Search.propTypes = {
    fetchMobile: PropTypes.func.isRequired
}

const Infos = ({ mts, province, catName, telString, areaVid, ispVid, carrier }) => (
    <div className="col-lg-12" style={{marginTop: "30px"}}>
        <div className="jumbotron">
            <h1>{telString}</h1>
            <p>号码归属地:{province}</p>
            <p>运营商:{catName}</p>
        </div>
    </div>
)

@connect(state => state, dispatch => bindActionCreators({getMobile,fetchingMobile,fetchMobile},dispatch))
class App extends Component {
    render() {
        const { mobile, fetchMobile, fetchingMobile } = this.props;
        return (
            <div className="container" style={{marginTop: "20px"}}>
                <Search
                    fetchMobile={fetchMobile}
                    isFetching={mobile.get('fetching')}/>
                {!Map(mobile.get('mobile')).isEmpty() && <Infos {...mobile.get('mobile')} />}
            </div>
        )
    }
}

render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

总结

关于react的全家桶,我在此省略了react-routor,因为我简略掉了页面结构。如果有兴趣的同学可以另外找资料看。
我对react+redux+immutable+webpack+css(less/sass)的介绍都比较简略,主要说的是如何实现我的需求,初学者可能有许多地方看得不是明了的,请移步React学习资源汇总去看明白这些概念。学习的路还很长,我对react可以说算入了个门,还有很多概念及原理我也说不清楚,只知道这么用,我在些只是将我学会的东西都应用到我的demo中去,算是对自己有一个交待而已,也拿出来让大家吐槽一下。文中我用到了好多关于react家族成员或者沾边的知识点:react组件,无状态组件,redux架构,中间件,异步数据流,指令,不可变数据immutable的应用,复合函数等。说白了,就是要了解关于如何去学好函数式编程(FP),这些需要不断地练习才能慢慢从代码中体会。