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


    前言

    上一篇已经讲了一些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的操作
    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

  • 相关阅读:
    4.28综合练习
    团队项目第一阶段冲刺第六天
    4.27防盗链和代理
    梦断代码阅读笔记3
    团队项目第一阶段冲刺第五天
    4.26抓取猪⼋戒数据
    团队项目第一阶段冲刺第四天
    4.25xpath解析
    4.24aiohttp模块学习
    如何将类数组转化为数组?
  • 原文地址:https://www.cnblogs.com/zhoulin-circle/p/9012729.html
Copyright © 2020-2023  润新知