npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-family

v1.0.0

Published

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

Downloads

9

Readme

从零搭建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'
        }
    };
  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>

    用浏览器打开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"
  }

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

参考地址:

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

babel

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

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

  • babel-loader:使用 Babel 转换 JavaScript依赖关系的 Webpack 加载器
  • @babel/core:即 babel-core,将 ES6 代码转换为 ES5
  • @babel/preset-env:即 babel-preset-env,根据您要支持的浏览器,决定使用哪些 transformations / plugins 和 polyfills,例如为旧浏览器提供现代浏览器的新特性
  • @babel/preset-react:即 babel-preset-react,针对所有 React 插件的 Babel 预设,例如将 JSX 转换为函数

**注: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"]
}

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

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

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

react

npm install --save react react-dom

修改 src/index.js使用react

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

ReactDom.render(
    <div>Hello React!</div>, document.getElementById('app'));

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

打开index.html 看效果。

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

cd src
mkdir components
cd components
touch Hello.js

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

import React, {Component} from 'react';

export default class Hello extends Component {
    render() {
        return (
            <div>
                Hello,React!
            </div>
        )
    }
}

然后让我们修改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'));

根目录执行打包命令

webpack --config webpack.dev.config.js

打开index.html看效果咯~

react-router

npm install --save react-router-dom

新建router文件夹和组件

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

按照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;

新建页面文件夹

cd src
mkdir pages

新建两个页面 Home,Page1

cd src/pages
mkdir Home && touch Home/Home.js
mkdir Page1 && touch Page1/Page1.js

填充内容:

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

export default class Home extends Component {
    render() {
        return (
            <div>
                this is home~
            </div>
        )
    }
}

Page1.js

import React, {Component} from 'react';

export default class Page1 extends Component {
    render() {
        return (
            <div>
                this is Page1~
            </div>
        )
    }
}

现在路由和页面建好了,我们在入口文件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'));

现在执行打包命令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

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')
    }

现在执行

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

浏览器打开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"
  }

下次执行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"
    }
  • 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'
    }

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

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

现在我们执行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"

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'));

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

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

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

const webpack = require('webpack');

devServer: {
    hot: true
}

plugins:[
     new webpack.HotModuleReplacementPlugin()
]

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

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

上面的配置对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>
        )
    }
}

你可以测试一下,当我们修改代码的时候,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. .babelrc 增加 react-hot-loader/babel
.babelrc
{
  "presets": [
    "es2015",
    "react",
    "stage-0"
  ],
  "plugins": [
    "react-hot-loader/babel"
  ]
}
  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. 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'));

现在,执行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)
       
  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)
       

使用 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);
       
  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. webpack.dev.config.js 避免出现黄色警告
const path = require('path');
const webpack = require('webpack')

module.exports = {
	...
  resolve: {
    alias: {
      'react-dom': '@hot-loader/react-dom',
    },
  },
 ...
};
  • 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';

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

import Home from 'pages/Home/Home';

下面我们来配置下,修改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')
        }
    }

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

src/router/router.js
import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
src/index.js
import getRouter from 'router/router';

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

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

先来写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}
}

再来写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
    }
}

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

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

export default function combineReducers(state = {}, action) {
    return {
        counter: counter(state.counter, action)
    }
}

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

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;

到现在为止,我们已经可以使用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();

当前文件夹执行命令

webpack testRedux.js 

node dist/main.js

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

{ counter: { count: 0 } }
{ counter: { count: 1 } }
{ counter: { count: 0 } }
{ counter: { count: 0 } }

做这个测试,就是为了告诉大家,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')
          }
  

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

react-redux

npm install --save react-redux

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

写一个Counter页面

cd src/pages
mkdir Counter
touch Counter/Counter.js
src/pages/Counter/Counter.js
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>
        )
    }
}

修改路由,增加Counter

// 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 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;

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)

下面我们要传入store

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

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

// src/index.js
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')
  )
}

到这里我们就可以执行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. 创建必须的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
    }
}

我们创建了请求中,请求成功,请求失败三个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;
    }
}

这里的...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. 现在有了

    action

    ,有了

    reducer

    ,我们就需要调用把

    action

    里面的三个

    action

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

    • 请求中 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());
                }
            )
    }
}

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

{type: xxxx}

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

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

npm install --save redux-thunk

这里涉及到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;

到这里,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);

这里你可能发现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;

现在你可以执行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
});

devtool优化

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

这让我们分析错误无从下手。看这里

我们增加webpack配置devtool

webpack.dev.config.js增加

devtool: 'inline-source-map'

编译css

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

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

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

webpack.dev.config.js rules增加

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

我们用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>
        )
    }
}

好了,现在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

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

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

webpack.dev.config.js rules增加

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

我们用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>
        )
    }
}

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

编译图片

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

webpack.dev.config.js rules增加

{
    test: /\.(png|jpg|jpeg|gif)$/,
    use: [{
        loader: 'url-loader',
        options: {
            limit: 8192
        }
    }]
}

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

我们来用Page1 测试下

cd src/pages/Page1
mkdir images

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>
        )
    }
}

可以去看看效果啦。

按需加载

为什么要实现按需加载?

我们现在看到,打包完后,所有页面只生成了一个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. 改造路由器
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;

现在你可以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'
    }

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

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

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

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

看到没。这里有个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. 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配置](