# 从零搭建React全家桶框架教程

# 说明

技术栈均是目前最新的。本文借鉴 从零搭建React全家桶框架教程在新窗口打开

  • webpack 4.41.2
  • Babel 7.x
  • redux 4.0.4
  • React 16.12.0

# init项目

  1. 创建文件夹并进入

    mkdir react-family && cd react-family

  2. init npm

    npm init 按照提示填写项目基本信息

# webpack

  1. 安装 webpack

    npm install --save-dev webpack@3

    Q: 什么时候用--save-dev,什么时候用--save

    A: --save-dev 是你开发时候依赖的东西,--save 是你发布之后还依赖的东西。看这里在新窗口打开

  2. 根据webpack文档在新窗口打开编写最基础的配置文件

    新建webpack开发配置文件 touch webpack.dev.config.js

    webpack.dev.config.js

    const path = require('path');
    
    module.exports = {
     
        /*入口*/
        entry: path.join(__dirname, 'src/index.js'),
        
        /*输出到dist文件夹,输出文件名字为bundle.js*/
        output: {
            path: path.join(__dirname, './dist'),
            filename: 'bundle.js'
        }
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  3. 学会使用webpack编译文件

    新建入口文件

    mkdir src && touch ./src/index.js

    src/index.js 添加内容

    document.getElementById('app').innerHTML = "Webpack works"

    现在我们执行命令 webpack --config webpack.dev.config.js

    webpack如果没有全局安装,这里会报错哦。命令建议全局安装,同时需要全局安装 webpack-cli

    我们可以看到生成了dist文件夹和bundle.js

  4. 现在我们测试下~

    dist文件夹下面新建一个index.html

    touch ./dist/index.html

    dist/index.html填写内容

    <!doctype html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    <body>
    <div id="app"></div>
    <script type="text/javascript" src="./bundle.js" charset="utf-8"></script>
    </body>
    </html>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    用浏览器打开index.html,可以看到Webpack works!

    webpack在新窗口打开

    现在回头看下,我们做了什么或者说webpack做了什么。

    把入口文件 index.js 经过处理之后,生成 bundle.js。就这么简单。

# 命令优化

Q:每次打包都得在根目录执行这么一长串命令webpack --config webpack.dev.config.js,能不打这么长吗?

A:修改package.json里面的script,增加dev-build

package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev-build": "webpack --config webpack.dev.config.js"
  }
1
2
3
4
5

现在我们打包只需要执行npm run dev-build就可以啦!

参考地址:

http://www.ruanyifeng.com/blog/2016/10/npm_scripts.html

# babel

Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。 这一过程叫做“源码到源码”编译, 也被称为转换编译。

通俗的说,就是我们可以用ES6, ES7等来编写代码,Babel会把他们统统转为ES5。

**注:babel 7 使用了 @babel 命名空间来区分官方包,因此以前的官方包 babel-xxx 改成了 @babel/xxx

npm install --save-dev @babel/core babel-loader @babel/preset-env @babel/preset-react

新建babel配置文件.babelrc

touch .babelrc

.babelrc

  {
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}
1
2
3

修改webpack.dev.config.js,增加babel-loader

    /*src文件夹下面的以.js结尾的文件,要使用babel解析*/
    /*cacheDirectory是用来缓存编译结果,下次编译加速*/
    module: {
        rules: [{
            test: /\.js$/,
            use: ['babel-loader?cacheDirectory=true'],
            include: path.join(__dirname, 'src')
        }]
    }
1
2
3
4
5
6
7
8
9

现在我们简单测试下,是否能正确转义ES6~

# react

npm install --save react react-dom
1

修改 src/index.js使用react

import React from 'react';
import ReactDom from 'react-dom';

ReactDom.render(
    <div>Hello React!</div>, document.getElementById('app'));
1
2
3
4
5

执行打包命令webpack --config webpack.dev.config.js

打开index.html 看效果。

我们简单做下改进,把Hello React放到组件里面。体现组件化~

cd src
mkdir components
cd components
touch Hello.js
1
2
3
4

按照React语法,写一个Hello组件

import React, {Component} from 'react';

export default class Hello extends Component {
    render() {
        return (
            <div>
                Hello,React!
            </div>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11

然后让我们修改src/index.js,引用Hello组件!

src/index.js
import React from 'react';
import ReactDom from 'react-dom';
import Hello from './component/Hello';

ReactDom.render(
    <Hello/>, document.getElementById('app'));
1
2
3
4
5
6
7

根目录执行打包命令

webpack --config webpack.dev.config.js
1

打开index.html看效果咯~

# react-router

npm install --save react-router-dom
1

新建router文件夹和组件

cd src
mkdir router && touch router/router.js
1
2

按照react-router文档在新窗口打开编辑一个最基本的router.js。包含两个页面homepage1

src/router/router.js
import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Home from '../pages/Home/Home';
import Page1 from '../pages/Page1/Page1';


const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page1">Page1</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route path="/page1" component={Page1}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

新建页面文件夹

cd src
mkdir pages
1
2

新建两个页面 Home,Page1

cd src/pages
mkdir Home && touch Home/Home.js
mkdir Page1 && touch Page1/Page1.js
1
2
3

填充内容:

src/pages/Home/Home.js
import React, {Component} from 'react';

export default class Home extends Component {
    render() {
        return (
            <div>
                this is home~
            </div>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

Page1.js

import React, {Component} from 'react';

export default class Page1 extends Component {
    render() {
        return (
            <div>
                this is Page1~
            </div>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11

现在路由和页面建好了,我们在入口文件src/index.js引用Router。

修改src/index.js

import React from 'react';
import ReactDom from 'react-dom';

import getRouter from './router/router';

ReactDom.render(
    getRouter(), document.getElementById('app'));
1
2
3
4
5
6
7

现在执行打包命令npm run dev-build。打开index.html查看效果啦!

那么问题来了~我们发现点击‘首页’和‘Page1’没有反应。不要惊慌,这是正常的。

我们之前一直用这个路径访问index.html,类似这样:file:///F:/react/react-family/dist/index.html。 这种路径了,不是我们想象中的路由那样的路径http://localhost:3000~我们需要配置一个简单的WEB服务器,指向 index.html~有下面两种方法来实现

  1. Nginx, Apache, IIS等配置启动一个简单的的WEB服务器。
  2. 使用webpack-dev-server来配置启动WEB服务器。

下一节,我们来使用第二种方法启动服务器。这一节的DEMO,先放这里。

# webpack-dev-server

简单来说,webpack-dev-server就是一个小型的静态文件服务器。使用它,可以为webpack打包生成的资源文件提供Web服务。

npm install webpack-dev-server --save-dev
1

2017.11.16补充:这里webpack-dev-server需要全局安装,要不后面用的时候要写相对路径。需要再执行这个 npm install webpack-dev-server@2 -g

修改webpack.dev.config.js,增加webpack-dev-server的配置。

webpack.dev.config.js
    devServer: {
        contentBase: path.join(__dirname, './dist')
    }
1
2
3
4

现在执行

webpack-dev-server --config webpack.dev.config.js
1

浏览器打开http://localhost:8080在新窗口打开,OK,现在我们可以点击首页,Page1了, 看URL地址变化啦!我们看到react-router已经成功了哦。

Q: --content-base是什么?

A:URL的根目录。如果不设定的话,默认指向项目根目录。

重要提示:webpack-dev-server编译后的文件,都存储在内存中,我们并不能看见的。你可以删除之前遗留的文件dist/bundle.js,仍然能正常打开网站!

每次执行webpack-dev-server --config webpack.dev.config.js,要打很长的命令,我们修改package.json,增加script->start:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev-build": "webpack --config webpack.dev.config.js",
    "start": "webpack-dev-server --config webpack.dev.config.js"
  }
1
2
3
4
5

下次执行npm start就可以了。

既然用到了webpack-dev-server,我们就看看它的其他的配置项在新窗口打开。 看了之后,发现有几个我们可以用的。

  • color(CLI only) console中打印彩色日志
  • historyApiFallback 任意的404响应都被替代为index.html。有什么用呢?你现在运行 npm start,然后打开浏览器,访问http://localhost:8080,然后点击Page1到链接http://localhost:8080/page1, 然后刷新页面试试。是不是发现刷新后404了。为什么?dist文件夹里面并没有page1.html,当然会404了,所以我们需要配置 historyApiFallback,让所有的404定位到index.html
  • host 指定一个host,默认是localhost。如果你希望服务器外部可以访问,指定如下:host: "0.0.0.0"。比如你用手机通过IP访问。
  • hot 启用Webpack的模块热替换特性。关于热模块替换,我下一小节专门讲解一下。
  • port 配置要监听的端口。默认就是我们现在使用的8080端口。
  • proxy 代理。比如在 localhost:3000 上有后端服务的话,你可以这样启用代理:
    proxy: {
      "/api": "http://localhost:3000"
    }
1
2
3
  • progress(CLI only) 将编译进度输出到控制台。

根据这几个配置,修改下我们的webpack-dev-server的配置~

webpack.dev.config.js
    devServer: {
        port: 8080,
        contentBase: path.join(__dirname, './dist'),
        historyApiFallback: true,
        host: '0.0.0.0'
    }
1
2
3
4
5
6
7

CLI ONLY的需要在命令行中配置

package.json
"start": "webpack-dev-server --config webpack.dev.config.js --color --progress"
1
2

现在我们执行npm start 看看效果。是不是看到打包的时候有百分比进度?在http://localhost:8080/page1页面刷新是不是没问题了? 用手机通过局域网IP是否可以访问到网站?

# 模块热替换(Hot Module Replacement)

到目前,当我们修改代码的时候,浏览器会自动刷新,不信你可以去试试。(如果你的不会刷新,看看这个调整文本编辑器在新窗口打开

我相信看这个教程的人,应该用过别人的框架。我们在修改代码的时候,浏览器不会刷新,只会更新自己修改的那一块。我们也要实现这个效果。

我们看下webpack模块热替换在新窗口打开教程。

我们接下来要这么修改

package.json` 增加 `--hot
"start": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"
1
2

src/index.js 增加module.hot.accept(),如下。当模块更新的时候,通知index.js

src/index.js
import React from 'react';
import ReactDom from 'react-dom';

import getRouter from './router/router';

if (module.hot) {
    module.hot.accept();
}

ReactDom.render(
    getRouter(), document.getElementById('app'));
1
2
3
4
5
6
7
8
9
10
11
12

现在我们执行npm start,打开浏览器,修改Home.js,看是不是不刷新页面的情况下,内容更新了?惊不惊喜?意不意外?

做模块热替换,我们只改了几行代码,非常简单的。纸老虎一个~

现在我需要说明下我们命令行使用的--hot,可以通过配置webpack.dev.config.js来替换, 向文档上那样,修改下面三处。但我们还是用--hot吧。下面的方式我们知道一下就行,我们不用。同样的效果。

const webpack = require('webpack');

devServer: {
    hot: true
}

plugins:[
     new webpack.HotModuleReplacementPlugin()
]
1
2
3
4
5
6
7
8
9

HRM配置其实有两种方式,一种CLI方式,一种Node.js API方式。我们用到的就是CLI方式,比较简单。 Node.js API方式,就是建一个server.js等等,网上大部分教程都是这种方式,这里不做讲解了。

你以为模块热替换到这里就结束了?nonono~

上面的配置对react模块的支持不是很好哦。

例如下面的demo,当模块热替换的时候,state会重置,这不是我们想要的。

修改Home.js,增加计数state

src/pages/Home/Home.js
import React, {Component} from 'react';

export default class Home extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }

    _handleClick() {
        this.setState({
            count: ++this.state.count
        });
    }

    render() {
        return (
            <div>
                this is home~<br/>
                当前计数:{this.state.count}<br/>
                <button onClick={() => this._handleClick()}>自增</button>
            </div>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

你可以测试一下,当我们修改代码的时候,webpack在更新页面的时候,也把count初始为0了。

为了在react模块更新的同时,能保留state等页面中其他状态,我们需要引入react-hot-loader在新窗口打开~

Q: 请问webpack-dev-serverreact-hot-loader两者的热替换有什么区别?

A: 区别在于webpack-dev-server自己的--hot模式只能即时刷新页面,但状态保存不住。因为React有一些自己语法(JSX)是HotModuleReplacementPlugin搞不定的。 而react-hot-loader--hot基础上做了额外的处理,来保证状态可以存下来。(来自segmentfault在新窗口打开

下面我们来加入react-hot-loader v4,

安装依赖

npm install react-hot-loader --save-dev
1

根据文档在新窗口打开, 我们要做如下几个修改~

  1. .babelrc 增加 react-hot-loader/babel
.babelrc
{
  "presets": [
    "es2015",
    "react",
    "stage-0"
  ],
  "plugins": [
    "react-hot-loader/babel"
  ]
}
1
2
3
4
5
6
7
8
9
10
11
  1. webpack.dev.config.js入口增加react-hot-loader/patch
webpack.dev.config.js
    entry: [
        'react-hot-loader/patch',
        path.join(__dirname, 'src/index.js')
    ]
1
2
3
4
5
  1. src/index.js修改如下
src/index.js
import React from 'react';
import ReactDom from 'react-dom';

import getRouter from './router/router';

if (module.hot) {
  module.hot.accept();
}
ReactDom.render(
  getRouter(), document.getElementById('app'));

1
2
3
4
5
6
7
8
9
10
11
12

现在,执行npm start,试试。是不是修改页面的时候,state不更新了?

  1. src/Page1/Page1.js修改如下

    import React, {Component} from 'react';
    import { hot } from 'react-hot-loader/root';
    
    class Home extends Component {
      render() {
        return (
          <div>
            this is page1~
          </div>
        )
      }
    }
    export default hot(Home)
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  2. src/Home/Home.js 修改如下

    import React, {Component} from 'react';
    import { hot } from 'react-hot-loader/root';
    class Home extends Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0
        }
      }
    
      _handleClick() {
        this.setState({
          count: ++this.state.count
        });
      }
    
      render() {
        return (
          <div>
            this is home~<br/>
            当前计数:{this.state.count}<br/>
            <button onClick={() => this._handleClick()}>自增</button>
          </div>
        )
      }
    }
    export default hot(Home)
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

# 使用 hooks

文档在新窗口打开

  1. src/Pages/Hooks/Hooks.js

    import React, { useState, useEffect } from 'react';
    import { hot } from 'react-hot-loader/root';
    const useGithub = userName => {
      const [user, setUser] = useState();
      useEffect(() => {
        fetch(`https://api.github.com/users/${userName}`)
          .then(r => r.json())
          .then(setUser);
      }, [userName]);
      return user;
    };
    function App() {
      const user = useGithub('weibozzz');
      if (!user) {
        return <div>Loading...</div>;
      }
      return (
        <div>
          Hello <b>{user.login}</b>
          <p>{user.bio}</p>
        </div>
      );
    }
    export default hot(App);
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
  2. src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';
import Home from '../pages/Home/Home';
import Page1 from '../pages/Page1/Page1';
import Hook from '../pages/Hooks/Hooks'

const getRouter = () => (
  <Router>
    <div>
      <ul>
        <li><Link to="/">首页</Link></li>
        <li><Link to="/page1">Page1</Link></li>
        <li><Link to="/hook">Hook2</Link></li>
      </ul>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/page1" component={Page1}/>
        <Route path="/hook" component={Hook}/>
      </Switch>
    </div>
  </Router>
);

export default getRouter;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  1. webpack.dev.config.js 避免出现黄色警告
const path = require('path');
const webpack = require('webpack')

module.exports = {
	...
  resolve: {
    alias: {
      'react-dom': '@hot-loader/react-dom',
    },
  },
 ...
};

1
2
3
4
5
6
7
8
9
10
11
12
13
  • https://github.com/gatsbyjs/gatsby/issues/11934

注意:您不能在类组件中使用 Hooks ,但是您绝对可以在组件树中,将类和函数式组件混合在一起使用。组件是类还是使用Hook的函数是该组件的实现细节 http://react.html.cn/docs/hooks-faq.html

# 文件路径优化

做到这里,我们简单休息下。做下优化~

在之前写的代码中,我们引用组件,或者页面时候,写的是相对路径~

比如src/router/router.js里面,引用Home.js的时候就用的相对路径

import Home from '../pages/Home/Home';
1

webpack提供了一个别名配置,就是我们无论在哪个路径下,引用都可以这样

import Home from 'pages/Home/Home';
1

下面我们来配置下,修改webpack.dev.config.js,增加别名~

webpack.dev.config.js
    resolve: {
        alias: {
            pages: path.join(__dirname, 'src/pages'),
            component: path.join(__dirname, 'src/component'),
            router: path.join(__dirname, 'src/router')
        }
    }
1
2
3
4
5
6
7
8

然后我们把之前使用的绝对路径统统改掉。

src/router/router.js
import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
src/index.js
import getRouter from 'router/router';
1
2
3
4
5

我们这里约定,下面,我们会默认配置需要的别名路径,不再做重复的讲述哦。

# redux

npm install redux -S

接下来,我们就要就要就要集成redux了。

要对redux有一个大概的认识,可以阅读阮一峰前辈的Redux 入门教程(一):基本用法在新窗口打开

如果要对redux有一个非常详细的认识,我推荐阅读中文文档在新窗口打开,写的非常好。读了这个教程,有一个非常深刻的感觉,redux并没有任何魔法。

不要被各种关于 reducers, middleware, store 的演讲所蒙蔽 ---- Redux 实际是非常简单的。

当然,我这篇文章是写给新手的,如果看不懂上面的文章,或者不想看,没关系。先会用,多用用就知道原理了。

开始整代码!我们就做一个最简单的计数器。自增,自减,重置。

先安装redux npm install --save redux

初始化目录结构

cd src
mkdir redux
cd redux
mkdir actions
mkdir reducers
touch reducers.js
touch store.js
touch actions/counter.js
touch reducers/counter.js
1
2
3
4
5
6
7
8
9

先来写action创建函数。通过action创建函数,可以创建action~src/redux/actions/counter.js

/*action*/

export const INCREMENT = "counter/INCREMENT";
export const DECREMENT = "counter/DECREMENT";
export const RESET = "counter/RESET";

export function increment() {
    return {type: INCREMENT}
}

export function decrement() {
    return {type: DECREMENT}
}

export function reset() {
    return {type: RESET}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

再来写reducer,reducer是一个纯函数,接收action和旧的state,生成新的state.

src/redux/reducers/counter.js
import {INCREMENT, DECREMENT, RESET} from '../actions/counter';

/*
* 初始化state
 */

const initState = {
    count: 0
};
/*
* reducer
 */
export default function reducer(state = initState, action) {
    switch (action.type) {
        case INCREMENT:
            return {
                count: state.count + 1
            };
        case DECREMENT:
            return {
                count: state.count - 1
            };
        case RESET:
            return {count: 0};
        default:
            return state
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

一个项目有很多的reducers,我们要把他们整合到一起

src/redux/reducers.js
import counter from './reducers/counter';

export default function combineReducers(state = {}, action) {
    return {
        counter: counter(state.counter, action)
    }
}
1
2
3
4
5
6
7
8

到这里,我们必须再理解下一句话。

reducer就是纯函数,接收state 和 action,然后返回一个新的 state。

看看上面的代码,无论是combineReducers函数也好,还是reducer函数也好,都是接收stateaction, 返回更新后的state。区别就是combineReducers函数是处理整棵树,reducer函数是处理树的某一点。

接下来,我们要创建一个store

前面我们可以使用 action 来描述“发生了什么”,使用action创建函数来返回action

还可以使用 reducers 来根据 action 更新 state

那我们如何提交action?提交的时候,怎么才能触发reducers呢?

store 就是把它们联系到一起的对象。store 有以下职责:

  • 维持应用的 state
  • 提供 getState() 方法获取 state
  • 提供 dispatch(action) 触发reducers方法更新 state
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。
src/redux/store.js
import {createStore} from 'redux';
import combineReducers from './reducers.js';

let store = createStore(combineReducers);

export default store;
1
2
3
4
5
6
7

到现在为止,我们已经可以使用redux了~

下面我们就简单的测试下

cd src
cd redux
touch testRedux.js
src/redux/testRedux.js
import {increment, decrement, reset} from './actions/counter';

import store from './store';

// 打印初始状态
console.log(store.getState());

// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
let unsubscribe = store.subscribe(() =>
    console.log(store.getState())
);

// 发起一系列 action
store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(reset());

// 停止监听 state 更新
unsubscribe();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

当前文件夹执行命令

webpack testRedux.js 

node dist/main.js
1
2
3

是不是看到输出了state变化?

{ counter: { count: 0 } }
{ counter: { count: 1 } }
{ counter: { count: 0 } }
{ counter: { count: 0 } }
1
2
3
4

做这个测试,就是为了告诉大家,reduxreact没关系,虽说他俩能合作。

到这里,我建议你再理下redux的数据流,看看这里在新窗口打开

  1. 调用store.dispatch(action)提交action
  2. redux store调用传入的reducer函数。把当前的stateaction传进去。
  3. reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  4. Redux store 保存了根 reducer 返回的完整 state 树。

就是酱紫~~

这会webpack.dev.config.js路径别名增加一下,后面好写了。

webpack.dev.config.js
          alias: {
              ...
              actions: path.join(__dirname, 'src/redux/actions'),
              reducers: path.join(__dirname, 'src/redux/reducers')
          }
  
1
2
3
4
5
6
7

把前面的相对路径都改改。

# react-redux

npm install --save react-redux

下面我们开始搭配react使用。

写一个Counter页面

cd src/pages
mkdir Counter
touch Counter/Counter.js
src/pages/Counter/Counter.js

1
2
3
4
5
import React, {Component} from 'react';

export default class Counter extends Component {
    render() {
        return (
            <div>
                <div>当前计数为(显示redux计数)</div>
                <button onClick={() => {
                    console.log('调用自增函数');
                }}>自增
                </button>
                <button onClick={() => {
                    console.log('调用自减函数');
                }}>自减
                </button>
                <button onClick={() => {
                    console.log('调用重置函数');
                }}>重置
                </button>
            </div>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

修改路由,增加Counter

// src/router/router.js

1
2
import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';
import Home from '../pages/Home/Home';
import Page1 from '../pages/Page1/Page1';
import Hook from '../pages/Hooks/Hooks'
import Counter from 'pages/Counter/Counter';


const getRouter = () => (
  <Router>
    <div>
      <ul>
        <li><Link to="/">首页</Link></li>
        <li><Link to="/page1">Page1</Link></li>
        <li><Link to="/hook">Hook2</Link></li>
        <li><Link to="/counter">Counter</Link></li>
      </ul>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/page1" component={Page1}/>
        <Route path="/hook" component={Hook}/>
        <Route path="/counter" component={Counter}/>
      </Switch>
    </div>
  </Router>
);

export default getRouter;


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

npm start`看看效果。

下一步,我们让Counter组件和Redux联合起来。使Counter能获得到Reduxstate,并且能发射action

当然我们可以使用刚才测试testRedux的方法,手动监听~手动引入store~但是这肯定很麻烦哦。

react-redux提供了一个方法connect

容器组件就是使用 store.subscribe() 从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。你可以手工来开发容器组件,但建议使用 React Redux 库的 connect() 方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。

connect接收两个参数,一个mapStateToProps,就是把reduxstate,转为组件的Props,还有一个参数是mapDispatchToprops, 就是把发射actions的方法,转为Props属性函数。

先来安装react-redux

// src/pages/Counter/Counter.js
import React, {Component} from 'react';
import {increment, decrement, reset} from 'actions/counter';
import {connect} from 'react-redux'

class Counter extends Component {
  render() {
    return (
      <div>
        <div>当前计数为({this.props.counter.count})</div>
        <button onClick={this.props.increment}>自增
        </button>
        <button onClick={this.props.decrement}>自减
        </button>
        <button onClick={this.props.reset}>重置
        </button>
      </div>
    )
  }
}
const mapStateToProps = (state) => {
  return {
    counter: state.counter
  }
};

const mapDispatchToProps = (dispatch) => {
  return {
    increment: () => {
      dispatch(increment())
    },
    decrement: () => {
      dispatch(decrement())
    },
    reset: () => {
      dispatch(reset())
    }
  }
};
export default connect(mapStateToProps, mapDispatchToProps)(Counter)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

下面我们要传入store

所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因为必须要用 store 把展示组件包裹一层,仅仅是因为恰好在组件树中渲染了一个容器组件。

建议的方式是使用指定的 React Redux 组件 来 魔法般的 让所有容器组件都可以访问 store,而不必显示地传递它。只需要在渲染根组件时使用即可。

// src/index.js
1
import React from 'react';
import ReactDom from 'react-dom';
import {Provider} from 'react-redux'
import store from './redux/store'

import getRouter from './router/router';

if (module.hot) {
  module.hot.accept();
}
renderWithHotReload(getRouter())
function renderWithHotReload(RootElement) {
  ReactDom.render(
    <Provider store={store}>
      {RootElement}
    </Provider>,
    document.getElementById('app')
  )
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

到这里我们就可以执行npm start,打开localhost:8080/counter看效果了。

这里我们再缕下(可以读React 实践心得:react-redux 之 connect 方法详解在新窗口打开

  1. Provider组件是让所有的组件可以访问到store。不用手动去传。也不用手动去监听。
  2. connect函数作用是从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。也传递dispatch(action)函数到props

接下来,我们要说异步action

参考地址: http://cn.redux.js.org/docs/advanced/AsyncActions.html

想象一下我们调用一个异步get请求去后台请求数据:

  1. 请求开始的时候,界面转圈提示正在加载。isLoading置为true
  2. 请求成功,显示数据。isLoading置为false,data填充数据。
  3. 请求失败,显示失败。isLoading置为false,显示错误信息。

下面,我们以向后台请求用户基本信息为例。

  1. 我们先创建一个user.json,等会请求用,相当于后台的API接口。
cd dist
mkdir api
cd api
touch user.json
dist/api/user.json
{
  "name": "brickspert",
  "intro": "please give me a star"
}
1
2
3
4
5
6
7
8
9
  1. 创建必须的action创建函数。
cd src/redux/actions
touch userInfo.js
src/redux/actions/userInfo.js
export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "userInfo/GET_USER_INFO_FAIL";

function getUserInfoRequest() {
    return {
        type: GET_USER_INFO_REQUEST
    }
}

function getUserInfoSuccess(userInfo) {
    return {
        type: GET_USER_INFO_SUCCESS,
        userInfo: userInfo
    }
}

function getUserInfoFail() {
    return {
        type: GET_USER_INFO_FAIL
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

我们创建了请求中,请求成功,请求失败三个action创建函数。

  1. 创建reducer

再强调下,reducer是根据stateaction生成新state纯函数

cd src/redux/reducers
touch userInfo.js
src/redux/reducers/userInfo.js
import {GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL} from 'actions/userInfo';


const initState = {
    isLoading: false,
    userInfo: {},
    errorMsg: ''
};

export default function reducer(state = initState, action) {
    switch (action.type) {
        case GET_USER_INFO_REQUEST:
            return {
                ...state,
                isLoading: true,
                userInfo: {},
                errorMsg: ''
            };
        case GET_USER_INFO_SUCCESS:
            return {
                ...state,
                isLoading: false,
                userInfo: action.userInfo,
                errorMsg: ''
            };
        case GET_USER_INFO_FAIL:
            return {
                ...state,
                isLoading: false,
                userInfo: {},
                errorMsg: '请求错误'
            };
        default:
            return state;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

这里的...state语法,是和别人的Object.assign()起同一个作用,合并新旧state。我们这里是没效果的,但是我建议都写上这个哦

组合reducer

src/redux/reducers.js
import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';

export default function combineReducers(state = {}, action) {
    return {
        counter: counter(state.counter, action),
        userInfo: userInfo(state.userInfo, action)
    }
}
1
2
3
4
5
6
7
8
9
10
  1. 现在有了

    action
    
    1

    ,有了

    reducer
    
    1

    ,我们就需要调用把

    action
    
    1

    里面的三个

    action
    
    1

    函数和网络请求结合起来。

    • 请求中 dispatch getUserInfoRequest
    • 请求成功 dispatch getUserInfoSuccess
    • 请求失败 dispatch getUserInfoFail

src/redux/actions/userInfo.js增加

export function getUserInfo() {
    return function (dispatch) {
        dispatch(getUserInfoRequest());

        return fetch('http://localhost:8080/api/user.json')
            .then((response => {
                return response.json()
            }))
            .then((json) => {
                    dispatch(getUserInfoSuccess(json))
                }
            ).catch(
                () => {
                    dispatch(getUserInfoFail());
                }
            )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

我们这里发现,别的action创建函数都是返回action对象:

{type: xxxx}
1

但是我们现在的这个action创建函数 getUserInfo则是返回函数了。

为了让action创建函数除了返回action对象外,还可以返回函数,我们需要引用redux-thunk

npm install --save redux-thunk
1

这里涉及到redux中间件middleware,我后面会讲到的。你也可以读这里Middleware在新窗口打开

简单的说,中间件就是action在到达reducer,先经过中间件处理。我们之前知道reducer能处理的action只有这样的{type:xxx},所以我们使用中间件来处理 函数形式的action,把他们转为标准的actionreducer。这是redux-thunk的作用。 使用redux-thunk中间件

我们来引入redux-thunk中间件

src/redux/store.js
import {createStore, applyMiddleware} from 'redux';
import thunkMiddleware from 'redux-thunk';
import combineReducers from './reducers.js';

let store = createStore(combineReducers, applyMiddleware(thunkMiddleware));

export default store;
1
2
3
4
5
6
7
8

到这里,redux这边OK了,我们来写个组件验证下。

cd src/pages
mkdir UserInfo
cd UserInfo
touch UserInfo.js
src/pages/UserInfo/UserInfo.js
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {getUserInfo} from "actions/userInfo";

class UserInfo extends Component {

    render() {
        const {userInfo, isLoading, errorMsg} = this.props.userInfo;
        return (
            <div>
                {
                    isLoading ? '请求信息中......' :
                        (
                            errorMsg ? errorMsg :
                                <div>
                                    <p>用户信息:</p>
                                    <p>用户名:{userInfo.name}</p>
                                    <p>介绍:{userInfo.intro}</p>
                                </div>
                        )
                }
                <button onClick={() => this.props.getUserInfo()}>请求用户信息</button>
            </div>
        )
    }
}

export default connect((state) => ({userInfo: state.userInfo}), {getUserInfo})(UserInfo);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

这里你可能发现connect参数写法不一样了,mapStateToProps函数用了es6简写,mapDispatchToProps用了react-redux提供的简单写法。

增加路由 src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';
import Home from '../pages/Home/Home';
import Page1 from '../pages/Page1/Page1';
import Hook from '../pages/Hooks/Hooks'
import UserInfo from 'pages/UserInfo/UserInfo';
import Counter from 'pages/Counter/Counter';


const getRouter = () => (
  <Router>
    <div>
      <ul>
        <li><Link to="/">首页</Link></li>
        <li><Link to="/page1">Page1</Link></li>
        <li><Link to="/hook">Hook2</Link></li>
        <li><Link to="/counter">Counter</Link></li>
        <li><Link to="/userInfo">UserInfo</Link></li>
      </ul>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/page1" component={Page1}/>
        <Route path="/hook" component={Hook}/>
        <Route path="/counter" component={Counter}/>
        <Route path="/userInfo" component={UserInfo}/>
      </Switch>
    </div>
  </Router>
);

export default getRouter;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

现在你可以执行npm start去看效果啦!

到这里redux集成基本告一段落了,后面我们还会有一些优化。

# combinReducers优化

redux提供了一个combineReducers函数来合并reducer,不用我们自己合并哦。写起来简单,但是意思和我们 自己写的combinReducers也是一样的。

src/redux/reducers.js
import {combineReducers} from "redux";

import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';


export default combineReducers({
    counter,
    userInfo
});
1
2
3
4
5
6
7
8
9
10
11

# devtool优化

现在我们发现一个问题,代码哪里写错了,浏览器报错只报在build.js第几行。

这让我们分析错误无从下手。看这里在新窗口打开

我们增加webpack配置devtool

webpack.dev.config.js增加

devtool: 'inline-source-map'
1

# 编译css

npm install css-loader style-loader --save-dev
1

css-loader使你能够使用类似@importurl(...)的方法实现 require()的功能;

style-loader将所有的计算后的样式加入页面中; 二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中。

webpack.dev.config.js rules增加

{
   test: /\.css$/,
   use: ['style-loader', 'css-loader']
}
1
2
3
4

我们用Page1页面来测试下

cd src/pages/Page1
touch Page1.css
src/pages/Page1/Page1.css
.page-box {
    border: 1px solid red;
}
src/pages/Page1/Page1.js
import React, {Component} from 'react';

import './Page1.css';

export default class Page1 extends Component {
    render() {
        return (
            <div className="page-box">
                this is page1~
            </div>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

好了,现在npm start去看效果吧。

# 编译less

https://webpack.docschina.org/loaders/less-loader/#%E8%A6%81%E6%B1%82在新窗口打开

先说这里为什么不用scss,因为Windows使用node-sass,需要先安装 Microsoft Windows SDK for Windows 7 and .NET Framework 4在新窗口打开。 我怕有些人copy这份代码后,没注意,运行不起来。所以这里不用scss了,如果需要,自行编译哦。

npm install less less-loader --save-dev
1

css-loader使你能够使用类似@importurl(...)的方法实现 require()的功能;

style-loader将所有的计算后的样式加入页面中; 二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中。

webpack.dev.config.js rules增加

{
   test: /\.less$/,
   use: ['style-loader', 'css-loader', 'less-loader']
}
1
2
3
4

我们用Page1页面来测试下

cd src/pages/Page1
touch Page1.css
src/pages/Page1/Page1.less
.page-box {
    border: 1px solid green;
}
src/pages/Page1/Page1.js
import React, {Component} from 'react';

import './Page1.css';

export default class Page1 extends Component {
    render() {
        return (
            <div className="page-box">
                this is page1~
            </div>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

好了,现在npm start去看效果吧。

# 编译图片

npm install --save-dev url-loader file-loader
1

webpack.dev.config.js rules增加

{
    test: /\.(png|jpg|jpeg|gif)$/,
    use: [{
        loader: 'url-loader',
        options: {
            limit: 8192
        }
    }]
}
1
2
3
4
5
6
7
8
9

options limit 8192意思是,小于等于8K的图片会被转成base64编码,直接插入HTML中,减少HTTP请求。

我们来用Page1 测试下

cd src/pages/Page1
mkdir images
1
2

images文件夹放一个图片。

修改代码,引用图片

src/pages/Page1/Page1.js
import React, {Component} from 'react';

import './Page1.css';

import image from './images/weibozzz.jpeg';

export default class Page1 extends Component {
    render() {
        return (
            <div className="page-box">
                this is page1~
                <img src={image}/>
            </div>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

可以去看看效果啦。

# 按需加载

为什么要实现按需加载?

我们现在看到,打包完后,所有页面只生成了一个build.js,当我们首屏加载的时候,就会很慢。因为他也下载了别的页面的js了哦。

如果每个页面都打包了自己单独的JS,在进入自己页面的时候才加载对应的js,那首屏加载就会快很多哦。

react-router 2.0时代, 按需加载需要用到的最关键的一个函数,就是require.ensure(),它是按需加载能够实现的核心。

在4.0版本,官方放弃了这种处理按需加载的方式,选择了一个更加简洁的处理方式。

传送门在新窗口打开

根据官方示例,我们开搞

  1. npm install bundle-loader --save-dev
  2. 新建bundle.js
cd src/router
touch Bundle.js
src/router/Bundle.js
import React, {Component} from 'react'

class Bundle extends Component {
    state = {
        // short for "module" but that's a keyword in js, so "mod"
        mod: null
    };

    componentWillMount() {
        this.load(this.props)
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.load !== this.props.load) {
            this.load(nextProps)
        }
    }

    load(props) {
        this.setState({
            mod: null
        });
        props.load((mod) => {
            this.setState({
                // handle both es imports and cjs
                mod: mod.default ? mod.default : mod
            })
        })
    }

    render() {
        return this.props.children(this.state.mod)
    }
}

export default Bundle;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
  1. 改造路由器
src/router/router.js
import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Bundle from './Bundle';

import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';
import Page1 from 'bundle-loader?lazy&name=page1!pages/Page1/Page1';
import Counter from 'bundle-loader?lazy&name=counter!pages/Counter/Counter';
import UserInfo from 'bundle-loader?lazy&name=userInfo!pages/UserInfo/UserInfo';

const Loading = function () {
    return <div>Loading...</div>
};

const createComponent = (component) => (props) => (
    <Bundle load={component}>
        {
            (Component) => Component ? <Component {...props} /> : <Loading/>
        }
    </Bundle>
);

const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page1">Page1</Link></li>
                <li><Link to="/counter">Counter</Link></li>
                <li><Link to="/userinfo">UserInfo</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={createComponent(Home)}/>
                <Route path="/page1" component={createComponent(Page1)}/>
                <Route path="/counter" component={createComponent(Counter)}/>
                <Route path="/userinfo" component={createComponent(UserInfo)}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

现在你可以npm start,打开浏览器,看是不是进入新的页面,都会加载自己的JS的~

但是你可能发现,名字都是0.bundle.js这样子的,这分不清楚是哪个页面的js呀!

我们修改下webpack.dev.config.js,加个chunkFilenamechunkFilename是除了entry定义的入口js之外的js~

    output: {
        path: path.join(__dirname, './dist'),
        filename: 'bundle.js',
        chunkFilename: '[name].js'
    }
1
2
3
4
5

现在你运行发现名字变成home.js,这样的了。棒棒哒!

那么问题来了home是在哪里设置的?webpack怎么知道他叫home

其实在这里我们定义了,router.js里面

import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';
1

看到没。这里有个name=home。嘿嘿。

**报错: Support for the experimental syntax 'classProperties' isn't currently enabled **

解决: https://github.com/babel/babel/issues/8655

// .babelrc 增加此插件
"plugins": [
      "@babel/plugin-proposal-class-properties"
    ]
1
2
3
4

参考地址:

  1. http://www.jianshu.com/p/8dd98a7028e0
  2. https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/guides/code-splitting.md
  3. https://segmentfault.com/a/1190000007949841
  4. http://react-china.org/t/webpack-react-router/10123
  5. https://juejin.im/post/58f9717e44d9040069d06cd6

# 其他

其他的配置就相对简单,可参考这个 从零搭建React全家桶框架教程在新窗口打开,或者看最新 webpack配置在新窗口打开