前言
上一篇已經講了一些react的基本配置,本遍接着講熱更新以及react+redux的配置與使用。
熱更新
我們在實際開發時,都有用到熱更新,在修改代碼后,不用每次都重啟服務,而是自動更新。並而不是讓瀏覽器刷新,只是刷新了我們所改代碼影響到的模塊。
關於熱更新的配置,可看介紹戳這里
因為我們用了webpack-dev-server,我們可以不需要向上圖一樣配置,只需要修改啟動配置以修改默認值,--hot項。
"start": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"
然后要做的是當模塊更新后,通知入口文件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');
)
下面來試試重啟后,修改Home或About組件,保存后是不是自動更新啦!
到這里,你以為結束了嗎,NO!NO!NO!在此我們成功為自己挖下了坑(說多了都是淚)。獻上一段demo
src/pages/Home/Home.js
import React,{Component} from 'react';
export default class Home extends Component{
constructor(props){
super(props);
this.state={
count:0
}
}
_test(){
this.setState({
count:++this.state.count
});
}
render(){
return(
<div>
<h1>當前共點擊次數為:{this.state.count}</h1>
<button onClick={()=> this._test()}>點擊我!</button>
</div>
)
}
}
此時,按鈕每點擊一次,狀態會自增,但是如果我們用熱更新改一下文件,會發現,狀態被清零了!!!顯然這不是我們要的效果,那么我們平時在項目里為什么會用到react-hot-loader就明了了,因為可以保存狀態。試試:
安裝依賴
npm install react-hot-loader --save-dev
按官網介紹來配置
- 首先是.babelrc文件
{
"plugins":["react-hot-loader/babel"]
}
- 修改 webpack.dev.config.js
entry:[
'react-hot-loader/patch',
path.join(__dirname,'src/index.js')
]
- 修改src/index.js
import React from 'react';
import ReactDom from 'react-dom';
import getRouter from './router/router';
import {AppContainer} from 'react-hot-loader';
const hotLoader = RootElement => {
ReactDom.render(
<AppContainer>
{RootElement}
</AppContainer>,
document.getElementById('app')
);
}
/*初始化*/
hotLoader(getRouter());
if(module.hot){
module.hot.accept('./router/router',()=>{
const getRouter=require('./router/router').default;
hotLoader(getRouter());
});
}
哇哦哇哦,成功保存狀態啦,666!
路徑的優化
上面的demo我們已經寫過好幾個組件了,發現在引用的時候都要用上相對路徑,這樣非常不方便。我們可以優化一下。
我們以前做數學題總會尋找一些共同點提出來,這里也一樣。我們的公共組件都放在了src/components文件目錄下,業務組件都放在src/pages目錄下。在webpack中,提供一個別名配置,讓我們無論在哪個位置下,都通過別名從對應位置去讀取文件。
修改webpack.dev.config.js
resolve:{
alias:{
pages:path.join(__dirname,'src/pages'),
components:path.join(__dirname,'src/components'),
router:path.join(__dirname,'src/router')
}
}
然后按下面的形式改掉之前的路徑
/*之前*/
import Home from '../pages/Home/Home';
/*之后*/
import Home from 'pages/Home/Home';
看下改了路徑后,是不是依然可以正常運行呢!
Redux
如果用react做過項目的,基本對redux就不陌生了吧。此文主講全家桶的搭建,在此我就不詳細解說。簡單說下引用,做個小型計數器。
- 安裝
npm install --save redux
- 相關目錄搭建
cd src
mkdir redux && cd redux
mkdir actions
mkdir reducers
touch reducer.js
touch store.js
touch actions/counter.js
touch reducers/counter.js
- 增加文件的別名
打開webpack.dev.config.js
alias:{
...
actions:path.join(__dirname,'src/redux/actions'),
reducers:path.join(__dirname,'src/redux/reducers'),
//redux:path.join(__dirname,'src/redux') 與模塊重名
}
- 創建action,action是來描述不同的場景,通過觸發action進入對應reducer
打開文件src/redux/actions/counter.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}
}
- 接下來寫reducers,用來接收action和舊的state,生成新的state
src/redux/reducers/counter.js
import {INCREMENT,DECREMENT,RESET} from '../actions/counter';
const initState = {
count : 0
};
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 './rdeducers/counter';
export default function combineReducers(state={},action){
return {
counter:counter(state.counter,action)
}
}
- 創建store倉庫,進行存取與監聽state的操作
- 應用中state的保持
- getState()獲取state
- dispatch(action)觸發reducers,改變state
- subscribe(listener)注冊監聽器
打開src/redux/store.js
import {createStore} from 'redux';
import combineReducers from './reducers.js';
let store = createStore(combineReducers);
export default store;
- 測試
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());
//監聽每次更新值
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
);
//發起action
store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(reset());
//停止監聽
unsubscribe();
在當前目錄下運行
webpack testRedux.js build.js
node build.js
我這里報如下錯誤了
經排查,發現是node版本的問題,我用nvm來作node版本管理工具,從原本的4.7切換到9.0的版本,運行正確。
我們試用了一下redux,對於在項目熟用的童鞋來說,簡直是沒難度吧。那么回歸正題,我們用redux搭配着react一起用。將上述counter改成一個組件。
- 文件初始化搭建
cd src/pages
mkdir Counter
touch Counter/Counter.js
打開文件
import React,{Component} from 'react';
export default class Counter extends Component{
render(){
return(
<div>
<h2>當前計數為:</h2>
<button onClick={
()=>{
console.log('自增');
}
}>自增
</button>
<button onClick={()=>{
console.log('自減');
}}>自減
</button>
<button onClick={()=>{
console.log('重置')
}}>重置
</button>
</div>
)
}
}
- 路由增加
router/router.js
import Home from 'pages/Home/Home';
import About from 'pages/About/About';
import Counter from 'pages/Counter/Counter';
const getRouter=()=>(
<Router>
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="counter">Counter</Link></li>
</ul>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/counter" component={Counter}/>
</Switch>
</div>
</Router>
);
export default getRouter;
我們可以先跑一下,檢查路由跳轉是否正常。下面將redux應用到Counter組件上。
react-redux
- 安裝 react-redux
npm install --save react-redux
- 組件的state綁定
因為react-redux提供了connect方法,接收兩個參數。
- mapStateToProps:把redux的state,轉為組件的Props;
- mapDispatchToprops:觸發actions的方法轉為Props屬性函數。
connect()的作用有兩個:一是從Redux的state中讀取部分的數據,並通過props把這些數據返回渲染到組件中;二是傳遞dispatch(action)到props。
打開 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>
<h2>當前計數為:{this.props.counter.count}</h2>
<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);
- 調用的用的時候到src/index.js中,我們傳入store
注:我們引用react-redux中的Provider模塊,它可以讓所有的組件能訪問到store,不用手動去傳,也不用手動去監聽。
...
import {Provider} from 'react-redux';
import store from './redux/store';
const hotLoader = RootElement => {
ReactDom.render(
<AppContainer>
<Provider store={store}>
{RootElement}
</Provider>
</AppContainer>,
document.getElementById('app')
);
}
...
然后我們運行下,效果如圖
異步action
在實際開發中,我們更多的是用異步action,因為要前后端聯合起來處理數據。
正常我們去發起一個請求時,給用戶呈現的大概步驟如下:
- 頁面加載,請求發起,出現loading效果
- 請求成功,停止loading效果,data渲染
- 請求失敗,停止loading效果,返回錯誤提示。
下面我們模擬一個用戶信息的get請求接口:
- 創建文件
cd dist
mkdir api && cd api
touch userInfo.json
- 打開文件模擬數據
{
"name":"circle",
"age":24,
"like":"piano",
"female":"girl"
}
- 創建action
cd src/redux/actions
touch userInfo.js
在action中,我要需要創建三種狀態:請求中,請求成功,請求失敗。打開redux/actions/userInfo.js
export const GET_USERINFO_REQUEST="userInfo/GET_USERINFO_REQUEST";
export const GET_USERINFO_SUCCESS="userInfo/GET_USERINFO_SUCCESS";
export const GET_USERINFO_FAIL="userInfo/GET_USERINFO_FAIL";
export function getUserInfoRequest(){
return {
type:GET_USERINFO_REQUEST
}
}
export function getUserInfoSuccess(userInfo){
return{
type:GET_USERINFO_SUCCESS,
userInfo:userInfo
}
}
export function getUserInfoFail(){
return{
type:GET_USERINFO_FAIL
}
}
- 創建reducer
cd src/redux/reducers
touch userInfo.js
打開文件
import {GET_USERINFO_REQUEST,GET_USERINFO_SUCCESS,GET_USERINFO_FAIL} from 'actions/userInfo';
const initState = {
isLoading:false,
userInfo:{},
errMsg:''
}
export default function reducer(state=initState,action){
switch(action.type){
case GET_USERINFO_REQUEST:
return{
...state,
isLoading:true,
userInfo:{},
errMsg:''
}
case GET_USERINFO_SUCCESS:
return{
...state,
isLoading:false,
userInfo:action.userInfo,
errMsg:''
}
case GET_USERINFO_FAIL:
return{
...state,
isLoading:false,
userInfo:{},
errMsg:'請求出錯'
}
default:
return state;
}
}
以上...state的意思是合並新舊的所有state可枚舉項。
- 與之前做計數器一樣,接下來到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)
}
}
redux中提供了一個combineReducers函數來合並reducer,不需要我們自己寫合並函數,在此我們對上面的reducers.js作下優化。
import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';
import {combineReducers} from 'redux';
export default combineReducers({
counter,
userInfo
});
- 接下來發起請求
打開文件 src/redux/actions/userInfo.js,加入
...
export function getUserInfo(){
return function(dispatch){
dispatch(getUserInfoRequest());
return fetch('http://localhost:8000/api/userInfo.json')
.then((response=>{
return response.json()
}))
.then((json)=>{
dispatch(getUserInfoSuccess(json))
}
).catch(()=>{
dispatch(getUserInfoFail());
}
)
}
}
之前我們做計數器時,與之對比現發action都是返回的對象,這里我們返回的是函數。
為了讓action可以返回函數,我們需要裝新的依賴redux-tuhnk。它的作用是在action到reducer時作中間攔截,讓action從函數的形式轉為標准的對象形式,給reducer作正確處理。
npm install --save redux-thunk
- 引入redux-thunk,打開src/redux/store.js
我們可以使用Redux提供的applyMiddleware方法來使用一個或者是多個中間件,將它作為createStore的第二個參數傳入即可。
import {createStore,applyMiddleware} from 'redux';
import combineReducers from './reducers.js';
import thunkMiddleware from 'redux-thunk';
let store = createStore(combineReducers,applyMiddleware(thunkMiddleware));
export default store;
到這里我們基本的redux就搞定啦,下面寫個組件來驗證。
cd src/pages
mkdir UserInfo && cd UserInfo
touch 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,errMsg} = this.props.userInfo;
return(
<div>
{
isLoading ? '請求中...' :
(
errMsg ? errMsg :
<div>
<h2>個人資料</h2>
<ul>
<li>姓名:{userInfo.name}</li>
<li>年齡:{userInfo.age}</li>
<li>愛好:{userInfo.like}</li>
<li>性別:{userInfo.female}</li>
</ul>
</div>
)
}
<button onClick={
()=> this.props.getUserInfo()
}>查看個人資料</button>
</div>
)
}
}
export default connect((state)=>({userInfo:state.userInfo}),{getUserInfo})(UserInfo);
- 配置路由,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 About from 'pages/About/About';
import Counter from 'pages/Counter/Counter';
import UserInfo from 'pages/UserInfo/UserInfo';
const getRouter=()=>(
<Router>
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</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="/about" component={About}/>
<Route path="/counter" component={Counter}/>
<Route path="/userinfo" component={UserInfo}/>
</Switch>
</div>
</Router>
);
export default getRouter;
- 運行效果如下
未完待續 _ (偷偷告訴你,第三篇會講一些優化哦!)
我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan