集成React Redux Webpack

1,147 阅读2分钟

项目结构

├─ config
│  ├─ webpack.common.config.js      //webpack基础配置
│  ├─ webpack.dev.config.js         //webpack开发配置
│  └─ webpack.prod.config.js        //webpack生产配置
├─ src
│  ├─ css                           //css文件夹
│  │   ├─ counter.css
│  │   └─ user.css 
│  ├─ counter                       // 页面counter文件夹
│  │   ├─ action.js 
│  │   ├─ reducer.js
│  │   └─ view.js
│  ├─ user                          // 页面user文件夹
│  │   ├─ action.js 
│  │   ├─ reducer.js
│  │   └─ view.js
│  ├─ index.html                    //html模板
│  ├─ index.js                      //入口js
│  ├─ router.js                     //路由js
│  └─ store.js                      //redux js
├─ .babelrc                         //babel配置
├─ package.json                     //package.json

React Router例子

中文文档 react router api

import React, {Component} from 'react'
import ReactDom from 'react-dom';
import {
  BrowserRouter as Router,
  Route,
  NavLink,
} from 'react-router-dom'

function UrlParam() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <NavLink exact to="/" activeClassName="hurray">Home</NavLink>
          </li>
          <li>
            <NavLink to="/about/123" activeClassName="hurray">About</NavLink>
          </li>
        </ul>
        <p></p>
        <Route exact path="/" component={Home}/>
        <Route path="/about/:name" component={About}/>
      </div>
    </Router>
  )
}

class About extends Component {
  render() {
    let { match } = this.props
    return (
      <h1>{`${match.params.name} About`}</h1>
    )
  }
}
class Home extends Component {
  render() {
    return (
      <div>
        <h1>Home</h1>
      </div>
    )
  }
}

ReactDom.render(
  <UrlParam />, document.getElementById('root'));

Redux例子

Redux中文文档

/* action */
export const INCREMENT = "counter/INCREMENT"
export const DECREMENT = "counter/DECREMENT"
export const RESET = "counter/RESET"
// action创建函数
export function increment() {
    return {type: INCREMENT}
}
export function decrement() {
    return {type: DECREMENT}
}
export function reset() {
    return {type: RESET}
}

/* reducer */
const initState = {
    count: 0
}
export function counter(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
    }
}
/* store */
import {createStore} from 'redux'
import {combineReducers} from "redux"

const store = createStore(combineReducers({
    counter,
}))

/* test */
// 打印初始状态
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命令 在dist文件夹下,打开index.html可看到console记录

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

Action:描述当前发生了什么的普通对象,是改变state的唯一方法。其中type属性是必须的,表示Action的名称。

Action创建函数: 就是生成Action的方法。

Reducer: 是一个纯函数,接受Action和当前State作为参数,描述了应用如何更新state。

combineReducers函数 处理整棵树, reducer处理树的某一个点。

Store: 是保存数据的地方,可以把它看成一个容器。整个应用只能有一个Store。

  1. 提供getState()方法获取State

  2. 提供dispatch(action)方法出发reducers方法更新State

  3. 通过subscribe(listener)注册监听器

  4. 通过subscribe(listener)返回的注销监听器的函数,注销监听器

redux的数据流:

  1. 调用store.dispatch(action),提交action
  2. store调用创建时传入的reducer函数,把当前的state和action传进去获取新的state。
  3. reducer把多个子reducer的输出合并成一个单一的state树
  4. stoer保存根reducer返回的完整state树,并调用监听器

集成Redux、React Router

创建action

src/counter/action.js

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

src/counter/reducer.js

import { INCREMENT, DECREMENT, RESET } from './action'

const initState = {
  count: 0
}

/**
* 接收旧的state和action, 生成新的state
*/
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
  }

创建view

src/counter/view.js

import React, {Component} from 'react'
import {connect} from 'react-redux'
import {increment, decrement, reset } from './action'

class Counter extends Component {
  render() {
    return (
      <div>
        <div>当前计数为{this.props.counter.count}</div>
        <button
          onClick={() => {
            console.log('调用自增函数')
            this.props.increment()
          }}
        >
          自增
        </button>
        <button
          onClick={() => {
            console.log('调用自减函数')
            this.props.decrement()
          }}
        >
          自减
        </button>
        <button
          onClick={() => {
            console.log('调用重置函数')
            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())
    }
  }
}
/**
 * 利用connect方法生成容器组件。
 * 容器组件就是使用store.subscribe()从Redux State树种读取部分数据,并通过props来把这些数据提供给要渲染的组件。
 * connect接受两个参数,mapStateToProps把redux的state转为组件的props属性字段,mapDispatchToProps把发射actions的方法转为props属性函数。
 */
export default connect(mapStateToProps, mapDispatchToProps)(Counter)

connect函数作用是从redux state树种读取部分数据,并通过props来把这些数据提供给要渲染的组件,也传递dispatch(action)函数到props。

添加counter子reducer并创建store

src/store.js

import { createStore, applyMiddleware } from 'redux'
import {combineReducers} from 'redux'
import counter from './counter/reducer'
// redux提供了一个combineReducers函数来合并reducer
const reducer =  combineReducers({
    counter,
})
// 创建store
const store = createStore(reducer)
export default store

创建router

src/router.js

import React from 'react'
import {
    BrowserRouter as Router,
    Route,
    Switch,
    NavLink,
} from 'react-router-dom'

import index from './css/index.css'
import Counter from './counter/view'

export default function getRouter() {
    return (
        <Router>
            <div className={index.header_nav}>
                <ul>
                    <li>
                        <NavLink exact to="/" activeClassName={index.cur_nav}>
                            Counter
                        </NavLink>
                    </li>
                </ul>
                <Switch>
                    <Route exact path="/" component={Counter}/>
                    <Route render={() => <div>please usd user or counter url!</div>}/>
                </Switch>
            </div>
        </Router>
    )
}

组件访问store

src/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import {Provider} from 'react-redux'
import Store from './store'
import Router from './router'

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

Provider组件是让所有的组件可以访问到store。不用手动去传。也不用手动去监听。

按需加载

每个页面都打包加载自己单独的js。

bundle-loader 实现

  1. 使用require()来进行相应chunk的加载,该方法会返回一个function,这个function接收一个回调函数作为参数。
var waitForChunk = require("bundle-loader!./file.js");
// To wait until the chunk is available (and get the exports)
//  you need to async wait for it.
waitForChunk(function(file) {
	// use file like it was required with
	// var file = require("./file.js");
});
  1. 创建异步加载的包装组件bundle.js。Bundle的主要功能就是接收一个组件异步加载的方法,并返回相应的react组件。
import React, { Component } from 'react';

class LazyLoader extends Component {
    constructor(props) {
        super(props);
        this.state = {
            component: null,
            props: null,
        };
        this._isMounted = false;  // 这个需要考虑
    }

    componentWillMount() {
        this._load();
    }

    componentDidMount() {
        this._isMounted = true;
    }

    componentWillReceiveProps(next) {
        if (next.component === this.props.component) {
            return null;
        }
        this._load();
    }

    componentWillUnmount() {
        this._isMounted = false;
    }

    _load() {
        this.props.component( com => {
            this.setState({
                component: com.default || com,        // 兼容 es6 => commonjs
            });
        });
    }

    render() {
        const LazyComponent = this.state.component;
        const props = Object.assign({}, this.props);    // clone props
        delete props.component;                         // 去掉 component
        return LazyComponent ? (
            <LazyComponent {...props}/>
        ) : null;
    }
}

export default LazyLoader;
  1. 创建需要懒加载的组件, lazyComponent.js
import React, { Component } from 'react';

class LazyComponent extends Component {
    render() {
        return (
            <div>
                this is a lazy Component!!!;
                <br/>
                name: {this.props.name} ===== tips: {this.props.tips}
            </div>
        );
    }
}

export default LazyComponent;
  1. 对组件进行懒加载

引入了需要加载的组件 LazyComponent 和相应的加载器 LazyLoader,通过它来对 LazyComponent 进行按需加载。

import React from 'react';
import ReactDOM from 'react-dom'
import LazyComponent from 'bundle-loader?lazy&name=lazy.[name]!./lazyComponent';
import Bundle from './bundle';

ReactDOM.render((
    <Bundle component={LazyComponent} name={'lazyname...'} tips={'lazytips...'}/>
  ), document.getElementById('root'))

import()会返回一个Promise对象

import('./lazyComponent').then(mod => {
        someOperate(mod);
    }).catch(err => {
        console.log('failed');
    });

require.ensure()

require.ensure(
    // 当前require进来的模块的依赖
    [],
    // 对调函数,参数必须是require,用于动态引入其他模块
    require => {
        let LazyComponent = require('./lazyComponent');
        someOperate(LazyComponent);
    },
    // 处理error的回
    error => {
        console.log('failed');
    },
    // 打包的chunk名称
    'lazyComponent');

打包

配置HTML模版

html-webpack-plugin简化了HTML文件的创建,生成一个 HTML5文件, 其中css在head中的标签中引入,webpack打包生成的js在body中的script标签中引入。

Babel

Babel将ES6代码转成ES5的代码。

  1. 尽可能少的编译文件,是有test或者exclude或者include。设置cacheDirectory为true将转义的结果缓存到文件系统中。
  2. 在转换 ES2015 语法为 ECMAScript 5 的语法时,babel 会需要一些辅助函数,例如 _extend。babel 默认会将这些辅助函数内联到每一个 js 文件里,这样文件多的时候,项目就会很大。为避免重复引用,babel提供了transform-runtime来将这些函数放到一个单独的模块babel-runtime中。

编译css

  1. css内联在html的style中
module: {
        rules: [
            {
                test: /\.css$/,
                loader: "style-loader!css-loader?modules"
            },
        ]
    }
  1. 拆分css,html页面以link的方式引入
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module: {
        rules: [
            {
                test: /\.css$/,
                /* 抽取css */
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            publicPath: path.join(__dirname2, 'dist'),
                            hmr: process.env.NODE_ENV === 'development',
                        }
                    },
                    {
                        loader: 'css-loader',
                        options: {
                            modules: true,
                        }
                    }
                ],
            },
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].[hash].css',
            chunkFilename: '[id].[hash].css',
        }),
    ]

编译图片

img标引用的图片地址也需要url-loader来帮我们处理。

提取公共代码

optimization中的splitChunks。

文件压缩

new UglifyJSPlugin()

打包前自动清理dist文件夹

打包优化 每次打包前自动清理下dist文件

node模块安装

打包相关

  • npm init
  • npm install --save-dev webpack@4 webpack-cli@3 webpack-dev-server webpack-merge
  • npm install --save-dev html-webpack-plugin clean-webpack-plugin
  • 编译JS: npm install --save-dev babel-core@6 babel-loader@7 babel-preset-env babel-preset-react babel-polyfill
  • 编译css: npm install --save-dev css-loader style-loader mini-css-extract-plugin
  • 编译图片: npm install --save-dev url-loader file-loader
  • 按需加载: npm install --save-dev bundle-loader
  • 压缩: npm install --save-dev uglifyjs-webpack-plugin

内容相关

  • npm install --save react react-dom
  • npm install --save redux redux-thunk
  • npm install --save react-redux
  • npm install --save react-router-dom

webpack配置

webpack.common.config.js

const path = require('path')
const __dirname2 = path.join(__dirname, '../')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const commonConfig = {
    /* 入口 */
    entry: path.join(__dirname2, 'src/index.js'),
    /* 输出 */
    output: {
        path: path.join(__dirname2, './dist'),
        filename: '[name].[chunkhash].js', // 每个输出 bundle 的名称
        chunkFilename: '[name].[chunkhash].js', // 非入口chunk文件的名称
        publicPath: "/"
    },
    module: {
        rules: [
            /* webpack会自动调用.babelrc里的babel配置选项 */
            {
                test: /\.js$/,
                /*cacheDirectory是用来缓存编译结果,下次编译加速*/
                use: ['babel-loader?cacheDirectory=true'],
                /* src文件夹下面的以.js结尾的文件,要使用babel解析 */
                include: path.join(__dirname2, 'src')
            },
            /* 编译图片 */
            {
                test: /\.(png|jpg|gif)$/,
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 8192
                    }
                }]
            }
        ]
    },
    optimization: {
        /* 提取代码 */
        splitChunks: {
            chunks: 'all',
            cacheGroups: {                
                /* node modules */
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    chunks: 'all',
                    name: "vendor",
                },
                /* 自定义的共享的utils方法 */
                utils: {
                    name: "utils",
                    chunks: "initial",
                    minSize: 0    // 只要超出0字节就生成一个新包
                }
            }
        },
        //提取webpack运行时的代码,名字是manifest
        runtimeChunk: {
            name: 'manifest'
        }
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: path.join(__dirname2, 'src/index.html')
        }),
    ],
};

module.exports = commonConfig

webpack.dev.config.js

const path = require('path')
const merge = require('webpack-merge')
const __dirname2 = path.join(__dirname, '../')
const commonConfig = require('./webpack.common.config.js')

const devConfig  = {
    mode: 'development',
    devtool: 'cheap-module-source-map',
    /* 输出到dist文件夹,每次打包生成的名字后面增加chunkhash */
    devServer: {
        port: 8080,
        inline: false,
        contentBase: path.join(__dirname2, 'dist'),
        historyApiFallback: true,
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            modules: true,
                            //localIdentName: "[name]__[local]__[hash:base64:5]"
                        }
                    }
                ]
            },
        ]
    }
}

module.exports = merge(commonConfig, devConfig)

webpack.prod.config.js

const path = require('path');
const merge = require('webpack-merge');
const webpack = require('webpack')
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const commonConfig = require('./webpack.common.config.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const __dirname2 = path.join(__dirname, '../')

const publicConfig = {
    /* webpack 使用生产环境的内置优化 */
    mode: 'production',
    devtool: 'none',
    module: {
        rules: [
            {
                test: /\.css$/,
                /* 抽取css */
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            publicPath: path.join(__dirname2, 'dist'),
                            hmr: process.env.NODE_ENV === 'development',
                        }
                    },
                    {
                        loader: 'css-loader',
                        options: {
                            modules: true,
                            //localIdentName: "[name]__[local]__[hash:base64:5]"
                        }
                    }
                ],
            },
        ]
    },
    plugins: [
        /* 文件压缩 */
        new UglifyJSPlugin(),
        /* 指定环境 process.env.NODE_ENV 决定library中应该引用哪些内容 */
        new webpack.DefinePlugin({
            'process.env': {
                'NODE_ENV': JSON.stringify('production')
             }
         }),
        /* 打包优化 每次打包前自动清理下dist文件夹 */
        new CleanWebpackPlugin(),
        new MiniCssExtractPlugin({
            filename: '[name].[hash].css',
            chunkFilename: '[id].[hash].css',
        }),
    ]
}

module.exports = merge(commonConfig, publicConfig)

babel配置

{
    "presets": ["react", "env"],
}

参考