手把手教你全家桶之React(二)

前言

上一篇已經講了一些react的基本配置,本遍接着講熱更新以及react+redux的配置與使用。html

熱更新

咱們在實際開發時,都有用到熱更新,在修改代碼後,不用每次都重啓服務,而是自動更新。並而不是讓瀏覽器刷新,只是刷新了咱們所改代碼影響到的模塊。
關於熱更新的配置,可看介紹戳這裏node

由於咱們用了webpack-dev-server,咱們能夠不須要向上圖同樣配置,只須要修改啓動配置以修改默認值,--hot項。react

"start": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"

而後要作的是當模塊更新後,通知入口文件index.js。咱們看官網的教程配置
webpack

打開src/index.js,如上圖配置git

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組件,保存後是否是自動更新啦!
github

到這裏,你覺得結束了嗎,NO!NO!NO!在此咱們成功爲本身挖下了坑(說多了都是淚)。獻上一段demo
src/pages/Home/Home.jsweb

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就明瞭了,由於能夠保存狀態。試試:
安裝依賴shell

npm install react-hot-loader --save-dev

官網介紹來配置npm

  • 首先是.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!json

路徑的優化

上面的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的操做
  1. 應用中state的保持
  2. getState()獲取state
  3. dispatch(action)觸發reducers,改變state
  4. 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方法,接收兩個參數。

  1. mapStateToProps:把redux的state,轉爲組件的Props;
  2. 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,由於要先後端聯合起來處理數據。
正常咱們去發起一個請求時,給用戶呈現的大概步驟以下:

  1. 頁面加載,請求發起,出現loading效果
  2. 請求成功,中止loading效果,data渲染
  3. 請求失敗,中止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

相關文章
相關標籤/搜索