用webpack4从零开始构建react脚手架

使用脚手架

git clone git@github.com:xiehaitao0229/react-wepack4-xht.git cd react-webpack4-xhtcss

`npm run dev`  //  启动本地
`npm run build`  //  打包线上环境
`npm run clean` //  清除线上环境打包出来的文件
`npm run test` //  单元测试的工具库
`npm run fix` //  修复eslint的写法
`npm run format` //  格式化代码
`npm run precommit` //  commit 代码到git仓库的检查
复制代码

webpack今年推出的4这个版本就一直关注很学习它,webpack4这个版本借鉴了parcel的零配置,打包速度变得更快,值得你们去跟进学习。html

既然咱们已经迎接了webpack4的到来了,那么就一块儿来使用一下,即便你没用过以前的版本,不要紧,咱们从新出发,将工做中经常使用到的配置写给你们来看vue

安装webpack

  • 须要先在项目中npm init初始化一下,生成package.json
  • 建议node版本安装到8.2以上
// webpack4中除了正常安装webpack以外,须要再单独安一个webpack-cli
npm i webpack webpack-cli -D
复制代码

webpack是基于Node的

在项目下建立一个webpack.config.js(默认,可修改)文件来配置webpacknode

module.exports = {
    entry: '',               // 入口文件
    output: {},              // 出口文件
    module: {},              // 处理对应模块
    plugins: [],             // 对应的插件
    devServer: {},           // 开发服务器配置
    mode: 'development'      // 模式配置
}
复制代码

以上就是webpack的正常配置模块 启动devServer须要安装一下webpack-dev-serverreact

npm i webpack-dev-server -D
复制代码

image.png
接下来咱们按照项目的结构,咱们就从0开始去写一下配置

// webpack.config.js

const path = require('path');

module.exports = {
    entry: './src/index.js',    // 入口文件
    output: {
        filename: 'bundle.js',      // 打包后的文件名称
        path: path.resolve('dist')  // 打包后的目录,必须是绝对路径
    }
}
复制代码

上面就能够说是实现了最简单的webpack配置了,那接下来就打包一下看看 webpack

image.png

配置执行文件

工做当中咱们打包编译的时候通常都执行npm run dev这样的命令,既然是经过npm执行的命令,咱们就应该找到package.json里的执行脚本去配置一下命令,这里以下图所示 git

image.png
npm run build就是咱们打包后的文件,这是生产环境下,上线须要的文件

npm run dev是咱们开发环境下打包的文件,固然因为devServer帮咱们把文件放到内存中了,因此并不会输出打包后的dist文件夹es6

配置Html模板

文件都打包好了,可是咱们在使用的时候不能在dist目录下去建立一个html文件,而后去引用打包后的js吧,这不合理,实际开发中也不会这样 咱们须要实现html打包功能,能够经过一个模板实现打包出引用好路径的html来 这就须要用到一个经常使用的插件了,< html-webpack-plugin >,用以前咱们来安一下它github

npm i html-webpack-plugin -D
复制代码
let path = require('path');
// 插件都是一个类,因此咱们命名的时候尽可能用大写开头
let HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/index.js',
    output: {
        // 添加hash能够防止文件缓存,每次都会生成4位的hash串
        filename: 'bundle.js',   
        path: path.resolve('dist')
    },
    plugins: [
        // 经过new一下这个类来使用插件
        new HtmlWebpackPlugin({
            // 用哪一个html做为模板
            // 在src目录下建立一个index.html页面当作模板来用
            template: './src/index.html',
            hash: true, // 会在打包好的bundle.js后面加上hash串
        })
    ]
}
复制代码

经过上面的配置后,咱们再npm run build打包看一下如今是个什么样子了 web

image.png
多页面开发,怎么配置多页面 若是开发的时候不仅一个页面,咱们须要配置多页面,那么须要怎么来搞呢?不用担忧,html-webpack-plugin插件自有办法,咱们来观望一下

let path = require('path');
let HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    // 多页面开发,怎么配置多页面
    entry: {
        index: './src/index.js',
        login: './src/login.js'
    },
    // 出口文件  
    output: {                       
        filename: '[name].js',
        path: path.resolve('dist')
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',   
            filename: 'index.html',
            chunks: ['index']   // 对应关系,index.js对应的是index.html
        }),
        new HtmlWebpackPlugin({
            template: './src/index2.html',
            filename: 'login.html',
            chunks: ['login']   // 对应关系,login.js对应的是login.html
        })
    ]
}
复制代码

image.png
上面基本介绍完了html和js的打包配置了,webpack对css的解析须要用到loader,因此咱们先提早安装好,待会好方便使用

引用CSS文件

须要下载一些解析css样式的loader

npm i style-loader css-loader -D
// 引入less文件的话,也须要安装对应的loader
npm i less less-loader -D
npm i node-sass sass-loader -D
复制代码

下面咱们来看一下如何配置css文件的解析

// index.js
import './css/style.css';   // 引入css
import './less/style.less'; // 引入less

console.log('这里是打包文件入口-index.js');

// webpack.config.js
module.exports = {
    entry: {
        index: './src/index.js'
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve('dist')
    },
    module: {
        rules: [
            {
                test: /\.css$/,     // 解析css
                use: ['style-loader', 'css-loader'] // 从右向左解析
                /* 
                    也能够这样写,这种方式方便写一些配置参数
                    use: [
                        {loader: 'style-loader'},
                        {loader: 'css-loader'}
                    ]
                */
            }
        ]
    }
}
复制代码
  • 此时打包后的css文件是以行内样式style的标签写进打包后的html页面中,若是样式不少的话,咱们更但愿直接用link的方式引入进去,这时候须要把css拆分出来
  • extract-text-webpack-plugin插件它的功效就在于会将打包到js里的css文件进行一个拆分,单独提取css

拆分CSS

// @next表示能够支持webpack4版本的插件
npm i extract-text-webpack-plugin@next -D
复制代码
let path = require('path');
let HtmlWebpackPlugin = require('html-webpack-plugin');
// 拆分css样式的插件
let ExtractTextWebpackPlugin = require('extract-text-webpack-plugin');

module: {
        rules: [
            {
                test: /\.less$/,     // 解析less
                use: ExtractTextWebpackPlugin.extract({
                    // 将css用link的方式引入就再也不须要style-loader了
                    fallback: "style-loader",
                    use: ['css-loader', 'less-loader'] // 从右向左解析
                })
            },
            {
                test: /\.scss$/,     // 解析scss
                use: ExtractTextWebpackPlugin.extract({
                    // 将css用link的方式引入就再也不须要style-loader了
                    fallback: "style-loader",
                    use: ['css-loader', 'sass-loader'] // 从右向左解析
                })
            },
            {
                test: /\.css$/,     // 解析css
                use: ExtractTextWebpackPlugin.extract({
                    // 将css用link的方式引入就再也不须要style-loader了
                    fallback: "style-loader",
                    use: ['css-loader']
                })
            }
        ]
    },
   plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html',
        }),
        // 拆分后会把css文件放到dist目录下的css/style.css
        new ExtractTextWebpackPlugin('css/style.css')  
    ]
复制代码

image.png
另外一个插件mini-css-extract-plugin也是能够办到的,它能够说是为webpack4而生的, 在这里就简单的提一下

npm i mini-css-extract-plugin -D
复制代码
let MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader']
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'css/a.css'   // 指定打包后的css
        })
    ]
}
复制代码

拆分红多个css

这里要着重说一下上面两个插件的区别了,我的仍是建议用extract-text-webpack-plugin的,毕竟从以前的版本承接下来的,虽然在安包的时候须要@next,可是仍是值得信赖的

并且如今的extract-text-webpack-plugin也支持了拆分红多个css,而目前mini-css-extract-plugin还不支持此功能

// 正常写入的less
let styleLess = new ExtractTextWebpackPlugin('css/style.css');
// reset
let resetCss = new ExtractTextWebpackPlugin('css/reset.css');

module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/,
                use: resetCss.extract({
                    fallback: "style-loader",  
                    use: 'css-loader'
                })
            },
            {
                test: /\.less$/,
                use: styleLess.extract({
                    fallback: "style-loader",
                     use: ['css-loader', 'less-loader'] // 从右向左解析
                })
            }
        ]
    },
    plugins: [
        styleLess,
        resetCss
    ]
}
复制代码

经过这样操做后能够打包成两个不一样的css文件,以下图

image.png

引用图片

npm i file-loader url-loader -D
复制代码

若是是在css文件里引入的如背景图之类的图片,就须要指定一下相对路径

module.exports = {
    module: {
        rules: [
            {
                test: /\.(jpe?g|png|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 8192,    // 小于8k的图片自动转成base64格式,而且不会存在实体图片
                            outputPath: 'images/'   // 图片打包后存放的目录
                        }
                    }
                ]
            }
        ]
    }
}
复制代码

在css中指定了publicPath路径这样就能够根据相对路径引用到图片资源了,以下图所示

image.png

页面img引用图片

页面中常常会用到img标签,img引用的图片地址也须要一个loader来帮咱们处理好

npm i html-withimg-loader -D
复制代码
module.exports = {
    module: {
        rules: [
            {
                test: /\.(htm|html)$/,
                use: 'html-withimg-loader'
            }
        ]
    }
}
复制代码

这样再打包后的html文件下img就能够正常引用图片路径了

image.png

引用字体图片和svg图片

字体图标和svg图片均可以经过file-loader来解析

module.exports = {
    module: {
        rules: [
            {
                test: /\.(eot|ttf|woff|svg)$/,
                use: 'file-loader'
            }
        ]
    }
}
复制代码

这样即便样式中引入了这类格式的图标或者图片都没有问题了,img若是也引用svg格式的话,配合上面写好的html-withimg-loader就都没有问题了

添加CSS3前缀

经过postcss中的autoprefixer能够实现将CSS3中的一些须要兼容写法的属性添加响应的前缀,这样省去咱们很多的时间

因为也是一个loader加载器,咱们也须要先安装一下

npm i postcss-loader autoprefixer -D
复制代码

安装后,咱们还须要像webpack同样写一个config的配置文件,在项目根目录下建立一个postcss.config.js文件,配置以下:

module.exports = {
    plugins: [
        require('autoprefixer')({
            "browsers": [
                "defaults",
                "not ie < 11",
                "last 2 versions",
                "> 1%",
                "iOS 7",
                "last 3 iOS versions"
            ]
        })
    ]
};
复制代码

而后在webpack里配置postcss-loader

module.exports = {
    module: {
        rules: [
            {
                test: /\.less$/,     // 解析less
                use: ExtractTextWebpackPlugin.extract({
                    // 将css用link的方式引入就再也不须要style-loader了
                    fallback: "style-loader",
                    use: ['css-loader', 'postcss-loader', 'less-loader'] // 从右向左解析
                })
            },
            {
                test: /\.scss$/,     // 解析scss
                use: ExtractTextWebpackPlugin.extract({
                    // 将css用link的方式引入就再也不须要style-loader了
                    fallback: "style-loader",
                    use: ['css-loader', 'postcss-loader', 'sass-loader'] // 从右向左解析
                })
            },
            {
                test: /\.css$/,     // 解析css
                use: ExtractTextWebpackPlugin.extract({
                    // 将css用link的方式引入就再也不须要style-loader了
                    fallback: "style-loader",
                    use: ['css-loader', 'postcss-loader']
                })
            },
        ]
    }
}
复制代码

转义ES6和react

在实际开发中,咱们在大量的使用着ES6及以后的api去写代码,这样会提升咱们写代码的速度,不过因为低版本浏览器的存在,不得不须要转换成兼容的代码,因而就有了经常使用的Babel了

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

npm i babel-core babel-loader babel-preset-env babel-preset-stage-3  babel-preset-react babel-polyfill babel-plugin-import babel-loader babel-register -D
babel-preset-stage-3  使用这个插件来编译为了后面使用...state扩展运算符可使用
复制代码

当把这些都安好后,咱们就开始配置,因为要兼容的代码不只仅包含ES6还有以后的版本和那些仅仅是草案的内容,因此咱们能够经过一个.babelrc文件来配置一下,对这些版本的支持

// .babelrc
{
    "presets": [
        [
            "env",
            {
                "loose": true,  
                "modules": false 
            }
        ],
        "es2015",
        "react",
      "babel-preset-stage-3"
    ]
}
复制代码

咱们再在webpack里配置一下babel-loader既能够作到代码转成ES5了

module.exports = {
    module: {
        rules: [
            {
                test:/\.js$/,
                use: 'babel-loader',
                include: /src/,          // 只转化src目录下的js
                exclude: /node_modules/  // 排除掉node_modules,优化打包速度
            }
        ]
    }
}
复制代码

加入 babel-plugin-transform-runtime 和 babel-polyfill

1.先来讲说babel-plugin-transform-runtime

在转换 ES2015 语法为 ECMAScript 5 的语法时,babel 会须要一些辅助函数,例如 _extend。babel 默认会将这些辅助函数内联到每个 js 文件里,这样文件多的时候,项目就会很大。

因此 babel 提供了 transform-runtime 来将这些辅助函数“搬”到一个单独的模块 babel-runtime 中,这样作能减少项目文件的大小。

npm install --save-dev babel-plugin-transform-runtime
复制代码

修改.babelrc配置文件,增长配置 .babelrc

"plugins": [
       "transform-runtime"
     ]
复制代码

2.再来看babel-polyfill 为何要集成babel-polyfill?

Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,好比Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(好比Object.assign)都不会转码。 举例来讲,ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。若是想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。

npm install --save-dev babel-polyfill
复制代码
//  修改入口文件index.js
import 'babel-polyfill';
复制代码

呀呀呀,在咱们每次npm run build的时候都会在dist目录下建立不少打好的包,若是积累过多可能也会混乱

因此应该在每次打包以前将dist目录下的文件都清空,而后再把打好包的文件放进去,主人们,接下来咱们使用clean-webpack-plugin这个插件吧

npm i clean-webpack-plugin -D
复制代码
let CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
    plugins: [
        // 打包前先清空
        new CleanWebpackPlugin('dist')  
    ]
}
复制代码

resolve解析

resolve: {
        // 别名
        alias: {
          pages:path.join(__dirname,'src/pages'),
          component:path.join(__dirname,'src/component'),
          actions:path.join(__dirname,'src/redux/actions'),
          reducers:path.join(__dirname,'src/redux/reducers'),
        },
        // 省略后缀
        extensions: ['.js', '.jsx', '.json', '.css', '.scss', '.less']
    }
复制代码

提取公共代码

在webpack4以前,提取公共代码都是经过一个叫CommonsChunkPlugin的插件来办到的。到了4之后,内置了一个如出一辙的功能 optimization

下面咱们就来看看如何提取公共代码

optimization: {
        splitChunks: {
            cacheGroups: {
                vendor: {   // 抽离第三方插件
                    test: /node_modules/,   // 指定是node_modules下的第三方包
                    chunks: 'initial',
                    name: 'vendor',  // 打包后的文件名,任意命名    
                    // 设置优先级,防止和自定义的公共代码提取时被覆盖,不进行打包
                    priority: 10
                },
                utils: {
                    // 抽离本身写的公共代码,utils里面是一个公共类库
                    chunks: 'initial',
                    name: 'utils',  //  任意命名
                    minSize: 0    // 只要超出0字节就生成一个新包
                }
            }
        }
    },
还要在plugins里面引入须要单独打包出来的chunk
      new HtmlWebpackPlugin({
            template: './src/index.html',
            chunks: ['vendor', 'index', 'utils']  //  引入须要的chunk   
        }),
复制代码

image.png

webpack-dev-server

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

npm install webpack-dev-server --save-dev
npm install webpack-dev-server -g
复制代码
devServer: {
        port: 3000,             // 端口
        open: true,             // 自动打开浏览器
        hot: true,               // 开启热更新
        overlay: true, // 浏览器页面上显示错误
        historyApiFallback: true
    },
复制代码

devtool优化

如今咱们发现一个问题,代码哪里写错了,浏览器报错只报在build.js第几行。这让咱们排查错误无从下手,传送门。 在开发环境下配置

devtool: 'inline-source-map'
复制代码

同时,咱们在srouce里面能看到咱们写的代码,也能打断点调试代码

热更新和自动刷新的区别

在配置devServer的时候,若是hot为true,就表明开启了热更新,可是这并无那么简单,由于热更新还须要配置一个webpack自带的插件而且还要在主要js文件里检查是否有module.hot

// webpack.config.js
let webpack = require('webpack');
module.exports = {
    plugins: [
        // 热更新,热更新不是刷新
        new webpack.HotModuleReplacementPlugin()
    ],
    devServer: {
        hot: true,  //  加上这一行
    }
}

//  在入口文件index.js
// 还须要在主要的js文件里写入下面这段代码
if (module.hot) {
  // 实现热更新
  module.hot.accept();
}
复制代码

热更新的好处在开发vue或者react的时候,其中某个组件修改的时候就会针对这个组件进行更新,超级好用,提高开发效率

集成react-router

`npm install --save react-router-dom`
复制代码

新建router文件夹和组件

`cd src`
`mkdir router && touch router/router.js`
复制代码

按照react-router文档编辑一个最基本的router.js。包含两个页面homepage1src/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 ~hi xht
            </div>
        )
    }
}
复制代码

Page1.js

import React, {Component} from 'react';

export default class Page1 extends Component {
    render() {
        return (
            <div>
                this is Page1~hi xht
            </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('root'));
复制代码

如今执行打包命令npm run dev。打开index.html查看效果啦!

集成react-redux

接下来,咱们就要就要就要集成redux了。 要对redux有一个大概的认识,能够阅读阮一峰前辈的Redux 入门教程(一):基本用法

若是要对redux有一个很是详细的认识,我推荐阅读中文文档,写的很是好。读了这个教程,有一个很是深入的感受,redux并无任何魔法。 咱们就作一个最简单的计数器。自增,自减,重置。 先安装redux

`npm install --save redux`
复制代码

初始化目录结构

cd src
mkdir redux
cd redux
mkdir actions
mkdir reducers
touch reducers.js
touch store.js
touch actions/couter.js
touch reducers/couter.js
复制代码

先来写action建立函数。经过action建立函数,能够建立action src/redux/actions/counter.js

/*action*/

export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
export const RESET = "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/couter.js

import { INCREMENT, DECREMENT, RESET } from '../actions/couters';

/*
* 初始化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/couter';

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

reducer就是纯函数,接收state 和 action,而后返回一个新的 state。 看上面的代码,不管是combineReducers函数也好,仍是reducer函数也好,都是接收state和action, 返回更新后的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;
复制代码

写一个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 { HashRouter as Router, Route, Switch, Link } from 'react-router-dom';

import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
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="/couter">Counter</Link>
        </li>
      </ul>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/page1" component={Page1} />
        <Route path="/couter" component={Counter} />
      </Switch>
    </div>
  </Router>
);

export default getRouter;
复制代码

npm run dev 看看效果。 下一步,咱们让Counter组件和react-redux联合起来。使Counter能得到到Redux的state,而且能发射action。 先来安装react-redux

npm install --save react-redux
复制代码

src/pages/Counter/Counter.js

import React, { Component } from 'react';
import { increment, decrement, reset } from 'actions/couters';

import { connect } from 'react-redux';

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

const mapStateToProps = (state) => {
  return {
    counter: state.couter,
  };
};

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 './css/index';
import { Provider } from 'react-redux';
import getRouter from './router/router';
import store from './redux/store';

const router = getRouter();

/* 初始化 */
renderWithHotReload(router);

function renderWithHotReload(RootElement) {
  ReactDOM.render(
    <Provider store={store}>{RootElement}</Provider>,
    document.getElementById('root')
  );
}

// 还须要在主要的js文件里写入下面这段代码
if (module.hot) {
  // 实现热更新
  module.hot.accept();
}
复制代码

咱们在说清楚一下 1.Provider组件是让全部的组件能够访问到store。不用手动去传。也不用手动去监听。 2.connect函数做用是从 Redux state 树中读取部分数据,并经过 props 来把这些数据提供给要渲染的组件。也传递dispatch(action)函数到props。

引入异步action,集成redux-thunk

下面,咱们以向后台请求用户基本信息为例。 1.咱们先建立一个user.json,等会请求用,至关于后台的API接口。

在根目录
mkdir api
cd api
touch user.json
复制代码

user.json

{
  "name": "xiehaitao",
  "intro": "please give me a star"
}
复制代码

2.建立必须的action建立函数。

cd src/redux/actions
touch userInfo.js
复制代码

src/redux/actions/userInfo.js

export const GET_USER_INFO_REQUEST = "GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "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建立函数。 3.建立reducer

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;
  }
}
复制代码

组合reducer src/redux/reducers.js

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

export default function combineReducers(state = {}, action) {
    return {
        couter: couter(state.couter, action),
        userInfo: userInfo(state.userInfo, action)
    }
}
复制代码

4.如今有了action,有了reducer,咱们就须要调用把action里面的三个action函数和网络请求结合起来。 src/redux/actions/userInfo.js增长

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

    return fetch('/api/user.json')
      .then((response) => {
        return response.json();
      })
      .then((json) => {
        dispatch(getUserInfoSuccess(json));
      })
      .catch(() => {
        dispatch(getUserInfoFail());
      });
  };
}
复制代码

咱们这里发现,别的action建立函数都是返回action对象: 可是咱们如今的这个action建立函数 getUserInfo则是返回函数了。 为了让action建立函数除了返回action对象外,还能够返回函数,咱们须要引用redux-thunk。

npm install --save redux-thunk
复制代码

简单的说,中间件就是action在到达reducer,先通过中间件处理。咱们以前知道reducer能处理的action只有这样的{type:xxx},因此咱们使用中间件来处理 函数形式的action,把他们转为标准的action给reducer。这是redux-thunk的做用。 使用redux-thunk中间件 src/redux/store.js

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

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

export default store;

复制代码

到这里,redux-thunk已经配置完成了,写一个组件来验证一下

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

export default connect(
  state => ({ userInfo: state.userInfo }),
  { getUserInfo }
)(UserInfo);

复制代码

增长路由 src/router/router.js

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

import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
import Counter from 'pages/Counter/Counter';
import UserInfo from 'pages/UserInfo/UserInfo';

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

export default getRouter;

复制代码

image.png

combinReducers优化

redux提供了一个combineReducers函数来合并reducer,不用本身合并 src/redux/reducers.js

import { combineReducers } from 'redux';
import userInfo from 'reducers/userInfo';
import couter from './reducers/couters';

export default combineReducers({
  couter,
  userInfo,
});

复制代码

指定环境

在webpack4以前都是须要建三个文件来须要环境的,webpack.base.js,webpack.dev.js,webpack.prod.js,如今在webpack4以后就不须要了,由于用--mode 就能够区分环境了 先安装

npm install -D yargs-parser
复制代码
这个包能够拿到--mode 里面的参数,这样子就能够区别是本地环境仍是线上环境了
    "dev": "cross-env webpack-dev-server --mode development",
    "build": "npm run lint && cross-env npm run clean && webpack --mode production",
复制代码
//  webpack.config.js
const argv = require('yargs-parser')(process.argv.slice(2));
const pro = argv.mode == 'production' ? true : false;  //  区别是生产环境和开发环境

let plu = [];
if (pro) {
    //  线上环境
    plu.push(
        new HtmlWebpackPlugin({
            template: './src/index.html',
            hash: true, // 会在打包好的bundle.js后面加上hash串
            chunks: ['vendor', 'index', 'utils']  //  引入须要的chunk
        }),
        // 拆分后会把css文件放到dist目录下的css/style.css
        new ExtractTextWebpackPlugin('css/style.[chunkhash].css'),
        new ExtractTextWebpackPlugin('css/reset.[chunkhash].css'),
        new CleanWebpackPlugin('dist'),
    )
} else {
    //  开发环境
    plu.push(
        new HtmlWebpackPlugin({
            template: './src/index.html',
            chunks: ['vendor', 'index', 'utils']  //  引入须要的chunk
        }),
        // 拆分后会把css文件放到dist目录下的css/style.css
        new ExtractTextWebpackPlugin('css/style.css'),
        new ExtractTextWebpackPlugin('css/reset.css'),
        new webpack.HotModuleReplacementPlugin(),  // 热更新,热更新不是刷新
    )
}

devtool: pro ? '' : 'inline-source-map'  //  只有本地开发才须要调试

复制代码

集成eslint

eslint目标是以可扩展,每条规则独立,不内置编码风格为理念的lint工具,用户能够定制本身的规则作成公共包

eslint主要有如下特色:

1)默认规则包含全部的jslint,jshint中存在的规则易迁移

2)规则可配置性高,可设置警告,错误两个error等级,也能够直接禁用

3)包含代码风格检测的规则

4)支持插件扩展,自定义规则

针对react开发者,eslint已经能够很好的支持jsx语法了。 先安装插件

npm install -D eslint eslint-config-airbnb eslint-loader  eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
复制代码

配置.eslintrc文件

// 直接继承airbnb的配置规则,同时也能够写入本身的特定规则,后面的内容会覆盖默认的规则,
//  下面是我比较习惯的lint规则
{
  "extends": ["airbnb"],
  "env": {
    "browser": true,
    "node": true,
    "es6": true,
    "mocha": true,
    "jest": true,
    "jasmine": true
  },
  "rules": {
    "no-plusplus": [0],
    "eqeqeq": [0],
    "no-empty": [0],
    "no-param-reassign": [0],
    "generator-star-spacing": [0],
    "consistent-return": [0],
    "no-shadow": [0],
    "react/forbid-prop-types": [0],
    "react/jsx-filename-extension": [
      1,
      {
        "extensions": [".js"]
      }
    ],
    "react/button-has-type": [
      "<enabled>",
      {
        "button": false,
        "submit": false,
        "reset": false
      }
    ],
    "global-require": [1],
    "import/prefer-default-export": [0],
    "react/jsx-boolean-value": [0],
    "react/jsx-no-bind": [0],
    "react/prop-types": [0],
    "react/prefer-stateless-function": [0],
    "react/jsx-one-expression-per-line": [0],
    "react/jsx-wrap-multilines": [
      "error",
      {
        "no-empty": [0],
        "no-param-reassign": [0],
        "declaration": "parens-new-line",
        "assignment": "parens-new-line",
        "return": "parens-new-line",
        "arrow": "parens-new-line",
        "condition": "parens-new-line",
        "logical": "parens-new-line",
        "prop": "ignore"
      }
    ],
    "no-else-return": [0],
    "no-restricted-syntax": [0],
    "import/no-extraneous-dependencies": [0],
    "no-use-before-define": [0],
    "jsx-a11y/no-static-element-interactions": [0],
    "jsx-a11y/no-noninteractive-element-interactions": [0],
    "jsx-a11y/click-events-have-key-events": [0],
    "jsx-a11y/anchor-is-valid": [0],
    "no-nested-ternary": [0],
    "arrow-body-style": [0],
    "import/extensions": [0],
    "no-bitwise": [0],
    "no-cond-assign": [0],
    "import/no-unresolved": [0],
    "comma-dangle": [
      "error",
      {
        "arrays": "always-multiline",
        "objects": "always-multiline",
        "imports": "always-multiline",
        "exports": "always-multiline",
        "functions": "ignore"
      }
    ],
    "object-curly-newline": [0],
    "function-paren-newline": [0],
    "no-restricted-globals": [0],
    "require-yield": [1]
  },
  "globals": {
    "document": true,
    "localStorage": true,
    "window": true
  }
}

复制代码

除此以外还要在webpack.config.js文件增长module的loader

module: {
        rules: [
            {
                enforce: "pre",  //  表明在解析loader以前就先解析eslint-loader
                test: /\.js$/,
                exclude: /node_modules/,
                include:/src/,
                loader: "eslint-loader",
              },
          ]
}
复制代码

pagekage.json文件里面script增长

"lint": "npm run format && npm run fix && eslint --ext .js src",  //  检测你写的代码是否符合eslint的规则
    "fix": "npm run format && eslint --fix --ext .js src",  //  npm run fix 这个是能够修复你没有按照lint规则的写法
复制代码

自动格式化以及提交代码时的优化配置

###第一步 格式化全部代码 prettier

npm install -D prettier
复制代码

在package.json的script里面添加以下配置

{
  "scripts": {
    "format": "prettier --single-quote --trailing-comma es5 --write \"src/**/*.js\""
  }
}
复制代码

你能够经过 npm run format试一下是否能够自动格式化你的代码 第二步 配置Eslint 上面咱们已经配置好eslint了在package.json的scripts里添加以下

"fix": "npm run format && eslint --fix --ext .js src",
复制代码

第三步 添加Git钩子(Pre-commit Hook)

Git 钩子(hooks)是在Git 仓库中特定事件(certain points)触发后被调用的脚本。 详情可浏览 git-scm.com/book/zh/v2/… 每次提交代码,执行 git commit以后进行自动格式化,免去每次人为手动格式化,使远程仓库代码保持风格统一。

npm install -D lint-staged husky 
复制代码

在package.json的scripts里添加以下

"precommit": "npm run lint",
复制代码

如今让咱们来看看在package.json的scripts的全部配置吧

"scripts": {
    "dev": "cross-env webpack-dev-server --mode development",
    "build": "npm run lint && cross-env npm run clean && webpack --mode production",
    "precommit": "npm run lint",
    "clean": "cross-env rm -rf dist && mkdir dist",
    "test": "mocha --compilers js:babel-register --require ./test/test_helper.js --recursive",
    "test:watch": "npm run test --watch",
    "lint": "npm run format && npm run fix && eslint --ext .js src",
    "fix": "npm run format && eslint --fix --ext .js src",
    "format": "prettier --single-quote --trailing-comma es5 --write \"src/**/*.js\""
  },
复制代码

好了,接下来还会继续维护这个脚手架,把react的SSR服务端渲染的脚手架也搭建起来

相关文章
相关标签/搜索