(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
總結:
(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,查看效果d. 修改webpack的output屬性
output: {
path: path.join(__dirname, 'dist'),
filename: 'app.js',
chunkFilename: '[name].js'
}
此時的文件名由:
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'
},
打包后的文件:
但是在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>
此時我們會發現
找到
我們發現之前在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
生成了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()
]
}
此時,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')
}
})
]
}
此時, 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
})
]
}