react+webpack+react-router+redux项目搭建(四)

(12)react-redux

与上述手动编译,引入store不同,react-redux提供了一个方法connect

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

a.安装react-redux

npm install --save react-redux

b.创建Counter组件

src/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 Counter from 'pages/Counter/Counter';
+ <li><Link to="/counter">Counter</Link></li>
+ <Route path="/counter" component={Counter}/>

npm start查看效果

c .将Counter组件与Redux联合起来

使Counter能获得Redux的state,并且能发射action。与(11).f测试方法不同,这里使用react-redux提供的connect方法。
connect接收两个参数,一个mapStateToProps,就是把redux的state,转为组件的Props,还有一个参数是mapDispatchToprops,把发射actions的方法,转为Props属性函数。
优化路径:

alias {
     + actions: path.join(__dirname, 'src/redux/actions'),
     + reducers: path.join(__dirname, 'src/redux/reducers')
    }

注意:为了避免后面使用import {createStore} from ‘react-redux’冲突,因此我们不将redux写别名。
在src/index.js导入store,作为Counter组件的属性,如下:

import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';
+ import {Provider} from 'react-redux';

import getRouter from 'router/router';
+ import store from 'redux/store';

/*初始化*/
// 如果没该步骤,页面会出现空白
renderWithHotReload(getRouter());

/*热更新*/
if (module.hot) {
   module.hot.accept('./router/router.js', () => {
       const getRouter = require('./router/router.js').default;
       renderWithHotReload(getRouter());
   });
}
function renderWithHotReload(RootElement) {
   ReactDom.render(
       <AppContainer>
           <Provider store={store}>
               {RootElement}
           </Provider>
       </AppContainer>,
       document.getElementById('app')
   )
}

修改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);

npm start


2.png

总结:
(a)在store.js初始化 store,然后将 state 上的属性作为 props 在最外成组件中层层传递下去。
(b)在最外层容器中,把所有内容包裹在 Provider 组件中,将之前创建的 store 作为 prop 传给 Provider。
(c)Provider 内的任何一个组件,如果需要使用 state 中的数据,就必须是「被 connect 过的」组件——使用 connect 方法对「你编写的组件」进行包装后的产物。
(d)connet到底做了什么呢?
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
connect() 接收四个参数,它们分别是 mapStateToProps,mapDispatchToProps,mergeProps和options。
**mapStateToProps(state, ownProps) : stateProps **这个函数允许我们将 store 中的数据作为 props 绑定到组件上。

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

获取我们需要的state,并转为props,所以<Counter>会有一个名为counter的属性。也可以将state.counter进行筛选后再动态输出。
函数的第二个参数 ownProps,是 组件自己的props。
** mapDispatchToProps(dispatch, ownProps): dispatchProps** 将action作为props绑定到组件上。

const mapDispatchToProps = (dispatch) => {
    return{
        increment: () => {
            dispatch(increment())
        },
        decrement: () => {
            dispatch(decrement())
        },
        reset: ()=> {
            dispatch(reset())
        }
    }
};

每当我们在 store 上 dispatch 一个 action,store 内的数据就会相应地发生变化。
同时,Redux 本身提供了 bindActionCreators 函数,来将 action 包装成直接可被调用的函数

import {bindActionCreators} from 'redux';

const mapDispatchToProps = (dispatch, ownProps) => {
  return bindActionCreators({
    increase: action.increase,
    decrease: action.decrease
  });
}

不管是 stateProps 还是 dispatchProps,都需要和 ownProps merge 之后才会被赋给 MyComp。connect 的第三个参数就是用来做这件事。通常情况下,你可以不传这个参数,connect 就会使用 Object.assign 替代该方法。

(13)action异步

当调用异步 API 时,有两个非常关键的时刻:发起请求的时刻,和接收到响应的时刻(也可能是超时)。

这两个时刻都可能会更改应用的 state;为此,你需要 dispatch 普通的同步 action。一般情况下,每个 API 请求都需要dispatch 至少三种 action:

①一种通知reducer请求开始的action:

对于这种 action,reducer 可能会切换一下state中的isFetching 标记。以此来告诉 UI 来显示加载界面。

②一种通知reducer请求成功的action。

对于这种 action,reducer 可能会把接收到的新数据合并到state 中,并重置 isFetching。UI 则会隐藏加载界面,并显示接收到的数据。

③一种通知 reducer 请求失败的action。

对于这种 action,reducer 可能会重置isFetching。另外,有些 reducer 会保存这些失败信息,并在 UI 里显示出来。

为了区分这三种 action,可能在 action 里添加一个专门的 status 字段作为标记位,又或者为它们定义不同的 type。


{ type: 'FETCH_POSTS_REQUEST' }

{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }

{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }

该实例的逻辑思路如下:

i. 请求开始的时候,界面转圈提示正在加载。isLoading置为true。

ii. 请求成功,显示数据。isLoading置为false,data填充数据。

iii. 请求失败,显示失败。isLoading置为false,显示错误信息。

a.创建后台API

创建一个user.json,等会请求用,相当于后台的API接口。在dist/api/user.json文件下:


{

 "name": "LALA",

 "intro": "enjoy hotpot"

}

b.创建action
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";

// 创建请求中,请求成功,请求失败 三个action创建函数

function getUserInfoRequest() {

 return {

 type: GET_USER_INFO_REQUEST

 }

}

function getUserInfoSuccess() {

 return {

 type: GET_USER_INFO_SUCCESS,

 userInfo: userInfo

 // userInfo: userInfo???有什么作用呢?

 }

}

function getUserInfoFail() {

 return {

 type: GET_USER_INFO_FAIL

 }

}

// 将三个action与网络请求联系到一起

// 为什么要将网络请求放到action,而不是rreducer???

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返回的是个函数,而不是对象,所以需要使用redux-thunk。

c.创建reducer

在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.tyoe) {

 case GET_USER_INFO_REQUEST:

 return {

 // ...state 保证其他state更新;是和别人的Object.assign()起同一个作用

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

 }

}

d. 合并reducer

redux/reducer.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)

 }

}

e. redux-thunk

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

f. 安装redux-thunk

npm install --save redux-thunk

g. 在src/redux/store.js中引入middleware

+ import {createStore, applyMidddleware} from 'redux';

import combineReducers from './reducers.js';

+ import thunkMiddleware from 'redux-thunk';

+ let store = createStore(combineReducers,applyMidddleware(thunkMiddleware));

export default store;

h. 创建UserInfo组件验证

src/pages/ UserInfo /UserInfo.js


import React, {Component} from 'react';

import {connect} from 'react-redux';

import {getUserInfo} from 'action/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);

i. 添加路由/userInfo

在 src/router/router.js 文件下


+ import UserInfo from 'pages/UserInfo/UserInfo';

+ <li><Link to="/userInfo">UserInfo</Link></li>

+ <Route path="/userInfo" component={UserInfo}/>

npm start,查看效果
3.png
d. 修改webpack的output属性
    output: {
    path: path.join(__dirname, 'dist'),
    filename: 'app.js',
     chunkFilename: '[name].js'
  }
4.png

此时的文件名由:

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

中的name值决定

(20)webpack缓存

为了避免多次访问同一页面后不再次下载资源,需要进行缓存。可以通过命中缓存,以降低网络流量,使网站加载速度更快,然而,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。由于缓存的存在,当你需要获取新的代码时,就会显得很棘手。因此,可以使用webpack配置,通过必要的配置,以确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件。

  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
    chunkFilename: '[name].[chunkhash].js'
  },

打包后的文件:


6.png

但是在dist/index.html中引用的还是之前的filename: app.js ,访问时会出错。因此,可以用插件HtmlWebpackPlugin

(21)HtmlWebpackPlugin

HtmlWebpackPlugin插件会自动生成一个HTML文件,并引用相关的文件。

a. 安装

npm install --save-dev html-webpack-plugin

b. webpack配置
+let HtmlWebpackPlugin = require('html-webpack-plugin'); //通过 npm 安装
+  plugins: [new HtmlWebpackPlugin({
        filename: 'index.html',
        template: path.join(__dirname, 'src/index.html')
    })],
c.创建index.html

删除 dist/index.html,创建src/html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Data</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

此时我们会发现


image.png

找到



我们发现之前在dist/index.html中的<script>仍然存在,这是为什么呢???
(22)提取公共代码

对于基本不会发生变化的代码提取出来,比如react,redux,react-router。但是他们合并到了bundle.js中了,每次项目发布需要重新提取。
CommonsChunkPlugin 可以用于将模块分离到单独的文件中。能够在每次修改后的构建结果中,将 webpack 的样板(boilerplate)和 manifest 提取出来。通过指定 entry 配置中未用到的名称,此插件会自动将我们需要的内容提取到单独的包中:

a.配置webpack.dev.config.js
+ const webpack = require('webpack'); //访问内置的插件
+  entry: {app: [
    'react-hot-loader/patch',
     path.join(__dirname, 'src/index.js'),
  ],
  vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
  }
+ plugins: [
…
new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor'
        })
]

把react等库生成打包到vendor.hash.js里面去。

但是你现在可能发现编译生成的文件main.[hash].js和vendor.[hash].js生成的hash一样的,这里是个问题,因为呀,你每次修改代码,都会导致vendor.[hash].js名字改变,那我们提取出来的意义也就没了。其实文档上写的很清楚,

output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[hash].js', //这里应该用chunkhash替换hash
        chunkFilename: '[name].[chunkhash].js'
    }
b.存在问题

但是无奈,如果用chunkhash,会报错。和webpack-dev-server --hot不兼容,具体看这里。
现在我们在配置开发版配置文件,就向webpack-dev-server妥协,因为我们要用他。问题先放这里,等会再配置正式版webpack.dev.config.js的时候要解决这个问题。

(23)构建生产环境

开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。

a. 编写webpack.config.js

在webpack.dev.config.js基础上做一下修改:
①先删除webpack-dev-server相关的东西
②devtool的值改成cheap-module-source-map
③将filename: '[name].[hash].js',改成[chunkhash],这是因为npm run build后,发现app.xxx.js和vendor.xxx.js不一样了哦。

c. package.json增加打包脚本
"build":"webpack --config webpack.config.js"

运行 npm run build
注意,不是npm build

image.png

生成了vundor.[chunkhash].html

(24)压缩文件
a. 安装

npm i --save-dev uglifyjs-webpack-plugin

b. 配置

在webpack.config.js下

const UglifyJSPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
  plugins: [
    new UglifyJSPlugin()
  ]
}
image.png

此时,vendor.[hash].js小了很多

(25)指定环境

许多 library 将通过与 process.env.NODE_ENV 环境变量关联,以决定 library 中应该引用哪些内容。例如,当不处于生产环境中时,某些 library 为了使调试变得容易,可能会添加额外的日志记录(log)和测试(test)。其实,当使用 process.env.NODE_ENV === 'production' 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。我们可以使用 webpack 内置的 DefinePlugin 为所有的依赖定义这个变量:

在webpack.config.js下

module.exports = {
  plugins: [
       new webpack.DefinePlugin({
          'process.env': {
              'NODE_ENV': JSON.stringify('production')
           }
       })
  ]
}
image.png

此时, vendor.[hash].js更小了

(26)优化缓存

我们现在来解决(21).b存在的问题,如何让在修改任意一个.js文件后,vendor.[].js名字保持不变。Webpack官网推荐了HashedModuleIdsPlugin插件,见https://doc.webpack-china.org/plugins/hashed-module-ids-plugin

该插件会根据模块的相对路径生成一个四位数的hash作为模块id, 建议用于生产环境。


plugins: [

…

 new webpack.HashedModuleIdsPlugin()

 ]

然而,再次大包后vendor的名字还会发生变化。这时还需要添加一个runtime代码抽取


new webpack.optimize.CommonsChunkPlugin({

 name: 'runtime'

})

这时因为:

runtime,以及伴随的 manifest 数据,主要是指:在浏览器运行时,webpack 用来连接模块化的应用程序的所有代码。runtime 包含:在模块交互时,连接模块所需的加载和解析逻辑。包括浏览器中的已加载模块的连接,以及懒加载模块的执行逻辑。

引入顺序在这里很重要。CommonsChunkPlugin 的 'vendor' 实例,必须在 'runtime' 实例之前引入

(27)打包优化

每次打包后的文件都存在了/dist文件夹下,我们希望每次打包前自动清理下dist文件。可以使用clean-webpack-plugin进行清理。

a. 安装

npm install clean-webpack-plugin --save-dev

b.配置

webpack.config.js下


+ const CleanWebpackPlugin = require('clean-webpack-plugin');

+plugins: [

…

 new CleanWebpackPlugin(['dist'])

]

运行nom run build会发现,之前的./dist文件先被删除后才生成新的文件。

(28)抽取CSS

目前的CSS打包进了js里面,webpack提供了一个插件,可以将css提取出来。Webpack提供了extract-text-webpack-plugin插件,可以提取单独的css文件。见:https://github.com/webpack-contrib/extract-text-webpack-plugin

a. 安装

npm install --save-dev extract-text-webpack-plugin

b. 配置

webpack.config.js


const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {

 module: {

 rules: [

 {

 test: /\.scss$/,

 use: ExtractTextPlugin.extract({

 fallback: 'style-loader',

 use: ['css-loader', 'sass-loader']

 })

 }

 ]

 },

 plugins: [

…

new ExtractTextPlugin({

 filename: '[name].[contenthash:5].css',

 allChunks: true

 })

 ]

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335