React全家桶高仿「饿了么」APP-上

1,097 阅读20分钟

前言

团队合作临摹饿了么移动端APP,选择了现在比较热门的React框架,虽然项目功能还不完善,但是在开发的过程中涵盖了React大部分的主要知识点,适合新手入门,熟悉框架,快速上手。我主要负责其中的发现页面、订单页面、登录页面,下面我简单总结下各个页面承载的功能和知识点,同时针对使用过程中遇到的问题也做了梳理,是一次非常愉快的学习过程。

项目已上传github,欢迎大家下载交流。

前端项目地址:github.com/Hanxueqing/…

后台数据地址:github.com/Hanxueqing/…

在线项目手册:hanxueqing.github.io/React-Eleme…

项目访问地址:http://39.96.84.220/react-ele/#/home

项目技术栈

项目运行

# 克隆到本地
git clone git@github.com:Hanxueqing/React-Eleme.git

# 安装依赖
npm install

# 开启本地服务器localhost
yarn start

# 发布环境
yarn build

项目开发

1.创建项目

先全局安装create-react-app脚手架

cnpm install create-react-app -g

全局安装yarn工具

yarn npm install -g yarn

生成一个react开发模板在eleme目录

create-react-app eleme

当我们要进行二次配置的时候,需要找到node_modules文件夹里的react-scripts进行配置,当我们执行yarn eject就可以将配置文件抽出,方便开发配置。

yarn eject(抽离配置文件,方便后续开发)

安装redux、redux-thunk、react-redux、axios模块

yarn add  redux redux-thunk  react-redux  axios -S

安装node-sass

yarn add node-sass -D

有时候会引起node_modules包冲突,需要删除,再通过cnpm i来重新安装*

解决 npm i 及 yarn install 都无法进行安装的问题和node-sass安装太慢的问题,请参考这篇文章:blog.csdn.net/qq_14988399…

2.搭建项目

2-1 style相关配置

​ 在src/stylesheets/样式文件夹中依次创建:

​ _base.scss

​ _commons.scss

​ _reset.scss

​ _mixins.scss

​ 在main.scss文件中将创建的scss文件引入

@import "_base.scss";
@import "_reset.scss";
@import "_mixins.scss";
@import "_commons.scss";

2-2 store相关配置 (建议把所有的数据都放入redux里面进行管理)

src/store/index.js文件

import {createStore,applyMiddleware} from "redux"
import reducer from "./reducer"
import thunk from "redux-thunk" //这个中间件   action=>到达reducer之间的过程  内部函数可以实现异步的操作,所以说增强了diaptch功能
const store = createStore(reducer, applyMiddleware(thunk));//在项目actionCreators里面可以进行异步请求了
export default store;		

src/store/reducer.js文件

//这是一个合并的reducer 
import {combineReducers} from "redux"
import commons from "./commons/reducer"
const reducer = combineReducers({
    commons
})
export default reducer;

src/store/commons/reducer.js (分支的reducer.js文件)

//分支的reducer必须是一个纯函数
//固定的输入必须要有固定的输出  不能更改之前的状态   不能有返回值不确定的数据(Math.random  new Date())
//内部只能进行同步操作!   新状态的地址与之前状态的地址如果不一样的话,才是认为返回新的状态(深拷贝才可以)
import state from "./state"
const reducer = (prevState = state,action)=>{
    let new_state = {...prevState}
    switch (action.type) {
        default:
            break;
    }
    return new_state;
}

export default reducer;

2-3 components相关配置

import React,{Component} from "react"
import "./index.scss"
class Template extends Component{
    render(){
        return (
            <div>
                Template
            </div>
        )
    }
}
export default Template

2-4 axios相关数据请求实现

为了解决跨域问题,我们首先需要在config文件夹中的webpackDevServer.config.js中配置反向代理

proxy: {
      "/ele": {
        target: "http://39.106.171.220:8989",
        changeOrigin: true
      }
    }

axios-utils/Post.js 封装Post方法

import axios from "axios"
import qs from "querystring"

export default ({url,data})=>{
    return axios.post(url,qs.stringify(data))
}

axios-utils/Get.js 封装Get方法

import axios from "axios"

export default ({ url, data }) => {
    return axios.get(url, {
        params:data
    })
}

封装了axios-utils/index.js文件

import Post from "./Post"
import Get from "./Get"
import {Component} from "react"

Component.prototype.$get = Get;
Component.prototype.$post = Post;

export { //方便后续actionCreators要用   组件用的话直接通过 this.$post或者this.$get
    Get,Post
}

App.js文件测试组件里面通过 this.$post

或者this.$get实现请求

 componentDidMount(){
    // this.$http.get()   Component.prototype.$post = axios;
     this.$get({
       url:"/ele/order/order",
       data:{
         limit:3
       }
     }).then(res=>{
        console.log(res)
    })
  }

2-5 rem的配置

在modules文件夹中封装一个rem.js文件,实现移动端响应式布局。

document.documentElement.style.fontSize = 
    document.documentElement.clientWidth / 3.75 +"px";

window.onresize = function(){
    document.documentElement.style.fontSize =
        document.documentElement.clientWidth / 3.75 + "px";
}

3.react-router-dom实现一级路由

3-1 安装react-router-dom

cnpm i react-router-dom -S

3-2 用Router包裹App组件

在index.js中,将用Router包裹起来并且引入哈希路由HashRouter,之后网址后面会带一个"#"号

import {HashRouter as Router} from "react-router-dom"
ReactDOM.render(
    <Router>
        <App />
    </Router>, document.getElementById('root'));

3-3 创建页面

在src/components/pages中创建各个页面,最后在index.js中统一导出。

import Home from './Home'
import Find from './Find'
import Order from './Order'
import Mine from './Mine'
export {
    Home,Find,Order,Mine
}

3-4 App.js文件创建一级路由

在需要切换路由的时候,引入Route

import {Route} from "react-router-dom"

path指定路径,component指定要渲染的组件

render(){
        return(
            <div>
                <Route path="/" component={Home} />
                <Route path="/find" component={Find} />
                <Route path="/order" component={Order} />
                <Route path="/mine" component={Mine} />
            </div>
        )  
    }

但是这时候在浏览器中输入/list,Home和List同时会匹配到,需要在“/”中设置exact属性,设置之后,只有完全匹配之后才能使用。

<Route path = "/" component = {Home} exact/>

render可以传入一个函数,在这里逻辑判断之后再去返回一个组件。

<Route path = "/find/a" render = {()=>(<div>hello 我是/find/a</div>)}/>

Switch 里面只运行渲染一个路由,可以有效的防止同级路由多次渲染(总是渲染第一个匹配到的组件,按照从上到下的顺序依次渲染)。

引入Switch

import {Route,Switch} from "react-router-dom"

第一个"/"后面必须带exact,因为Switch只渲染一个路由,如果不加exact,每一个地址都会先跟"/"匹配,所以不论输入什么地址都只会出来Home主页。

render(){
        return(
            <Switch>
                <Route path = "/" component = {Home} exact/>
                {/* <Route path = "/find/a" render = {()=>(<div>hello 我是/find/a</div>)}/> */}
                <Route path = "/find" component = {Find} />
                <Route path = "/order" component = {Order} />
                <Route path = "/mine" component = {Mine} />
            </Switch>
        )  
    }

在App.defaultProps中挂载到默认属性

App.defaultProps = {
    navs:[
        { id: 1, path: "/", component: Home, exact: true },
        { id: 2, path: "/Order", component: Order, exact: false },
        { id: 3, path: "/Find", component: Find, exact: false},
        { id: 4, path: "/mine", component: Mine, exact: false }
    ]
}

将Switch中的内容修改

import React,{Component} from 'react';
import {
  Home, Order, Find, Mine
} from "./components/pages"
import {Route,Switch} from "react-router-dom"//引入路由对象
class App extends Component{
  render(){
    return (
      <div>
        <Switch>
          <Route path="/" component={Home} exact={true}/>
          <Route path = "/find" component = {Find} />
          <Route path = "/order" component = {Order} />
          <Route path = "/mine" component = {Mine} />
        </Switch>
      </div>
    )
  }
}
export default App;

3-5 循环渲染默认属性

可以把一级路由放入到App.defaultProps上面去,进行循环渲染

编写一个renderNavs的方法,返回Switch中的内容

renderNavs(){
        return(
            <Switch>
                <Route path="/" component={Home} exact={true} />
                {/* <Route path = "/find/a" render = {()=>(<div>hello 我是/find/a</div>)}/> */}
                <Route path = "/find" component = {Find} />
          <Route path = "/order" component = {Order} />
          <Route path = "/mine" component = {Mine} />
            </Switch>
        )
    }

render中渲染this.renderNavs返回的结果,效果跟之前一样。

render(){
        return(
            <div>
                {this.renderNavs()}
            </div>
        )  
    }

将navs从this.props中解构,在Switch中循环遍历

import React,{Component} from 'react';
import {
  Home, Order, Find, Mine
} from "./components/pages"
import {Route,Switch} from "react-router-dom"//引入路由对象
class App extends Component{
  renderNavs(){
    let {navs} = this.props;
    return (
      <Switch>
        {
          navs.map(item=>{
            return (
              <Route key={item.id} path={item.path} component={item.component} exact={item.exact} />
            )
          })
        }
        {/* <Route path="/" component={Home} exact={true}/>
          <Route path = "/find" component = {Find} />
          <Route path = "/order" component = {Order} />
          <Route path = "/mine" component = {Mine} /> */}
      </Switch>
    )
  }
  render(){
    return (
      <div>
        {this.renderNavs()}
      </div>
    )
  }
}

App.defaultProps = {
    navs:[
        { id: 1, path: "/", component: Home, exact: true },
        { id: 2, path: "/Order", component: Order, exact: false },
        { id: 3, path: "/Find", component: Find, exact: false},
        { id: 4, path: "/mine", component: Mine, exact: false }
    ]
}
export default App;

发现了如果直接访问/的时候,页面不会出现任何的内容

原因是因为当时路径为/的时候,home组件才会出现,render函数才会执行。所以App.js文件中componentWillReceiveProps钩子函数中将/路径replace到/home:

componentWillReceiveProps(props) {
    let { pathname } = props.location;
    let { replace } = props.history;
    if (pathname === "/find") {
      replace("/find/coin")
    }
    if (pathname === "/") {
      replace("/home")
    }
  }

同时将/home的exact​改为false:

{ id: 1, path: "/home", component: Home, exact: false }

4.创建AppFooter组件

先在index.html中引入font-awesome字体图标库,开始编写AppFooter组件的样式。

<!-- 引入font-awesome字体图标 -->
    <link rel="stylesheet" href="%PUBLIC_URL%/font-awesome/css/font-awesome.min.css" />

在AppFooter中的index.js编写四个选项

import React,{Component} from "react"
import "./index.scss"
class AppFooter extends Component{
    render(){
        return(
            <div className = "app-footer">
                <a href = "">
                    <i className = "fa fa-home"></i>
                    <span>首页</span>
                </a>
                <a href="">
                    <i className="fa fa-podcast"></i>
                    <span>发现</span>
                </a>
                <a href="">
                    <i className="fa fa-book"></i>
                    <span>订单</span>
                </a>
                <a href="">
                    <i className="fa fa-user"></i>
                    <span>我的</span>
                </a>
            </div>
        )
    }
}
export default AppFooter

react-router中提供了Link用来做路由跳转,先把Link引入

import{Link} from "react-router-dom"

再将a标签替换成Link标签,需要添加to属性,就可以实现一级路由跳转

import{Link} from "react-router-dom"
class Template extends Component{
    render(){
        return(
            <div className = "app-footer">
                <Link to = "/">
                    <i className = "fa fa-home"></i>
                    <span>首页</span>
                </Link>
                <Link to = "/find">
                    <i className="fa fa-podcast"></i>
                    <span>发现</span>
                </Link>
                <Link to = "/order">
                    <i className="fa fa-book"></i>
                    <span>订单</span>
                </Link>
                <Link to = "/mine">
                    <i className="fa fa-user"></i>
                    <span>我的</span>
                </Link>
            </div>
        )
    }
}
export default Template

NavLink也可以实现路由跳转,同时还可以帮助我们添加ClassName:active(默认)在标签上面,也可以通过activeClassName给标签指定被激活时的class名,我们可以设置按钮被选中时的样式,需要在首页"/"后面添加exact完全匹配。

import React,{Component} from "react"
import "./index.scss"
// import{Link} from "react-router-dom"
import{NavLink} from "react-router-dom"
class Template extends Component{
    render(){
        return(
            <div className = "app-footer">
                <NavLink to = "/" exact>
                    <i className = "fa fa-home"></i>
                    <span>首页</span>
                </NavLink>
                <NavLink to = "/find">
                    <i className="fa fa-podcast"></i>
                    <span>发现</span>
                </NavLink>
                <NavLink to = "/order">
                    <i className="fa fa-book"></i>
                    <span>订单</span>
                </NavLink>
                <NavLink to = "/mine">
                    <i className="fa fa-user"></i>
                    <span>我的</span>
                </NavLink>
            </div>
        )
    }
}
export default Template

在index.scss中编写active的样式

&.active{
            color:#ae8232;
        }

将选项挂载到AppFooter.defaultProps,通过循环加载渲染到页面上。

import React,{Component} from "react"
import "./index.scss"
import {NavLink} from "react-router-dom"
class AppFooter extends Component{
    renderFooter(){
        let {navs} = this.props;
        return (
            navs.map(item=>{
                return (
                    <NavLink key={item.id} to={item.path} exact={item.exact}>
                        <i className={"fa fa-"+item.icon}></i>
                        <span>{item.title}</span>
                    </NavLink>
                )
            })
        )
    }
    render(){
        return (
            <div className="app-footer">
                {this.renderFooter()}
            </div>
        )
    }
}

AppFooter.defaultProps = {
    navs:[
        {id:1,path:"/",icon:"home",exact:true,title:"首页"},
        {id:2,path:"/find",icon:"podcast",exact:false,title:"发现"},
        {id:3,path:"/order",icon:"book",exact:false,title:"订单"},
        {id:4,path:"/mine",icon:"user",exact:false,title:"我的"}
    ]
}
export default AppFooter

5.实现AppFooter的显示与隐藏(两种方式)

5-1 在需要用到AppFooter组件的页面引入

分别在首页Home.js /发现Find.js / 订单Order.js 里面进行引入AppFooter组件。

import AppFooter from "../../commons/AppFooter"
class Find extends Component{
    render(){
        return(
            <div>Find
                <AppFooter></AppFooter>
            </div>
        )
    }
}
export default List

5-2 放入全局App.js文件中

在全局引入AppFooter

import AppFooter from "./components/commons/AppFooter"

在render中渲染,现在所有的组件都拥有AppFooter

render(){
        return(
            <div>
                {this.renderNavs()}
                <AppFooter></AppFooter>
            </div>
        )  
    }

比较Mine组件和App组件中的this:

在app.js组件的render里面进行打印

render(){
        console.log("App.js",this)
        return(
            <div>
                {this.renderNavs()}
                <AppFooter></AppFooter>
            </div>
        )  
    }

发现app组件props上面只会有navs,navs是我们给他挂载的默认状态,并且render只会执行一次。

image

在mine.js组件的render里面进行打印

class Mine extends Component{
    render(){
        console.log("mine.js", this)
        return(
            <div>Mine</div>
        )
    }
}

发现mine组件的props上面有history/location/match相关的东西。

image

(mine组件外面被Route包裹,上面的属性是由Route给他传递的属性)

image

当路由发生变化的时候,mine组件的属性上就会发生改变了,从而触发其componentWillReceiveProps这个钩子函数。

APP组件不是路由组件,监听不到路由的变化。所以说,我们想让APP组件监听路由的变化,应该如何操作?

我们可以让APP组件也变成路由组件,那么它就可以了监听到路由的变化了,一旦路由发生改变了,APP组件的componentWillReceiveProps这个钩子函数也就会被执行了。

withRouter是一个高阶组件,withRouter包裹之后,APP组件就会变成了一个伪路由组件,可以监听到路由的变化,但是不能够实现跳转。(mine组件是可以跳转/监听路由变化的)

首先引入withRouter组件

import {Route,Switch,withRouter} from "react-router-dom"

将App使用withRouter包裹

export default withRouter(App);

本来App.js组件是一个普通组件,上面只有navs属性,但是通过高阶组件withRouter(App)一包裹之后,发现App组件上面就会有location/match/history相关的路由属性,那这样的话,当路由发生变化的时候,上面的属性就会改变,一旦改变,App组件的componentWillReceiveProps这个钩子就会被触发,一旦触发就可以在这个钩子函数内部进行相应的路由操作的业务逻辑了。

高阶组件本质上就是一个函数,参数接受一个组件,然后再返回一个新的组件。之前接受的这个组件上面就会有一些额外的属性供使用。

我们先让它打印pathname,可以拿到每个路由的路径名称。

componentWillReceiveProps(props){
        let pathname = props.location.pathname;
        console.log(pathname)
    }

image

我们设置一个默认状态hasFooter来控制Footer的显示与隐藏,默认赋值为true。

constructor(){
        super()
        this.state = {
            hasFooter:true
        }
    }

当pathname为"/mine"的时候给它赋值为false

componentWillReceiveProps(props){
        let pathname = props.location.pathname;
        // console.log(pathname)
        if(pathname === "/mine"){
            this.setState({
                hasFooter:false
            })
        }
    }

在render中通过hasFooter的值来控制AppFooter组件的显示与隐藏

render(){
        let{hasFooter} = this.state;
        // console.log("App.js",this)
        return(
            <div>
                {this.renderNavs()}
                {!hasFooter || <AppFooter />}
            </div>
        )  
    }

这个时候可以实现点击调转到mine页面时AppFooter组件隐藏,但是返回其他页面的时候仍处于隐藏状态,我们需要补充else语句给非mine页面的hasFooter重新赋值为true

componentWillReceiveProps(props){
        let pathname = props.location.pathname;
        // console.log(pathname)
        if(pathname === "/mine"){
            this.setState({
                hasFooter:false
            })
        }else{
            this.setState({
                hasFooter:true
            })
        }
    }

5-3 Appfooter放置

一开始我们是通过componentWillReceiveProps这个钩子函数改变状态来控制组件的显示与隐藏,它只会在状态改变的时候被触发,但是初始化的时候并不会执行。所以当我从其他页面跳转到mine页面的时候AppFooter组件会隐藏,但是我直接访问mine页面的时候,AppFooter组件还是会显示。 考虑到App.js文件的render函数不论初始化还是路由发生变化的时候都会执行,就不需要通过状态去控制AppFooter的显示与隐藏。

renderFooter(){
    let {pathname} = this.props.location;
    if(pathname==="/mine") return "";
    return <AppFooter/>
  }
  render(){
    return (
      <div>
        {this.renderNavs()}
        {this.renderFooter()}
      </div>
    )
  }

6.Mine二级路由

6-1 Mine下面创建 user和login 两个子模块

在main中的index.js中配置二级路由,从react-router-dom中引入Route,将Login和User两个子模块引入。

import React,{Component} from "react"
import "./index.scss"
import {Route} from "react-router-dom"
import Login from "./Login"
import User from "./User"
class Mine extends Component{
    render(){
        return (
            <div>
                <Route path="/mine/login" component={Login}/>
                <Route path="/mine/user" component={User}/>
                Mine
            </div>
        )
    }
}
export default Mine

6-2 需要一条数据控制显示login还是显示user组件

​ 1)在store/commons/state写一条userInfo数据,默认赋值为空

export default {
    userInfo:null
}

​ 2)考虑到组件需要使用redux里面的数据,所以需要封装一个group出来。 封装一个connect()

import {connect} from "react-redux"
import {bindActionCreators} from "redux"
import actionCreators from "../../store/commons/actionCreators"

export default connect(state=>state.commons,dispatch=>{
    return bindActionCreators(actionCreators,dispatch)
});

​ 3)mine组件想要获取userInfo这个被redux管理的状态,需要写成容器嵌套ui的形式。这样的话mine组件就可以通过props来获取userInfo了。

但是发现会报错!原因是因为最外层没有嵌套Provider组件!因为Provider才可以给它提供这个数据过去,提供数据,然后被connect()包裹之后才可以拿到这个数据,再引入store,Provider上添加store属性。

最外层的index.js文件:

import {Provider} from "react-redux"
import store from "./store"

ReactDOM.render(
    <Provider store={store}>
        <Router>
            <App />
        </Router>
    </Provider>, document.getElementById('root'));

Mine组件上面:

import {CommonsGroup} from "../../../modules/group"
//Mine组件是路由组件,可以监听路由的变化
//Mine组件是UI组件,可以获取store中的数据
class Mine extends Component{
    render(){
        console.log("mine",this.props)  //this.porps上面就会有userInfo这个状态了!
        return (
            <div>
                <Route path="/mine/login" component={Login}/>
                <Route path="/mine/user" component={User}/>
                Mine
            </div>
        )
    }
}
export default CommonsGroup(Mine)

总结:

mine组件一旦被commonsGroup(Mine) 包裹之后,Mine组件就变得丰富了。

mine组件充当了两个角色身份. 一个角色本身是路由组件,可以监听到路由的变化,一旦路由改变了,componentWillReceiveProps这个钩子函数就会被执行。

另外一个角色是容器嵌套ui组件的形式,mine组件就可以获取一些被redux管理的状态与更改状态的方法。一旦userInfo这个状态被改变了,容器组件就会监听到状态的改变,然后给UI组件(Mine组件)传递新的属性,属性改变了,mine组件的componentWillReceiveProps这个钩子函数也会被执行。

6-3 immutable 不可改变的对象

考虑到reducer是一个纯函数,prevState是不能去随便更改的,即便更改的时候,还必须进行深拷贝一个新的对象,这样操作的话,还要随时谨记。这样就比较麻烦,所以引入immutable库解决此类问题。

cnpm i immutable  redux-immutable -S

在store/commons/reducer.js文件中从immutable库引入fromJS,将state包裹

//facibook团队在  redux时候  历时3年  immutable库
import state from "./state"
import {fromJS} from "immutable"
const reducer = (prevState = fromJS(state),action)=>{ 
    switch (action.type) {
        default:
            return prevState; //immutable对象!
    }
}

export default reducer;

会发现出错了! connect(mapStateToProps) 需要更改一下

modules/group/commons-group.js文件

import {connect} from "react-redux"
import {bindActionCreators} from "redux"
import actionCreators from "../../store/commons/actionCreators"
export default connect(state=>{
    return {
         userInfo:state.commons.get("userInfo") //从immutable对象里面取数据通过get方法获取
    }
},dispatch=>{
    return bindActionCreators(actionCreators,dispatch)
});

这时候mine组件上就会拿到userInfo数据

image

后续规范统一数据格式,state目前还是js对象,state.commons就变成了immutable对象了,那我们也应该让state对象变成immutable对象。

我们要安装另一个工具:

cnpm i redux-immutable -S

在汇总的reducer.js文件中,将redux改成redux-immutable

//这是一个合并的reducer 
import {combineReducers} from "redux-immutable"
import commons from "./commons/reducer"
const reducer = combineReducers({
    commons  
})
export default reducer;

state目前已经变成immutable对象,可以调用immutable中的getIn方法来获取数据

import {connect} from "react-redux"
import {bindActionCreators} from "redux"
import actionCreators from "../../store/commons/actionCreators"
export default connect(state=>{
    return {
        userInfo:state.getIn(["commons","userInfo"]) //state目前已经变成immutable对象
    }
},dispatch=>{
    return bindActionCreators(actionCreators,dispatch)
});

6-4 mine组件获取userInfo,实现跳转

mine组件由于被Route包裹,所以上面会有history属性和location属性,history中的push和replace都会实现路由跳转,如果我们从order跳转到mine再进入user,返回应该返回到order,中间从mine到user的过程不用被记录,所以我们需要使用replace,而push则会记录这个过程,点击返回,返回到mine页面。

	componentDidMount(){
        this.checkUserInfo()
    }
    //order -- mine  (replace)  mine/user 
    checkUserInfo(){
        let {userInfo,history} = this.props;
        if(userInfo){ //说明用户已经登录了
            history.replace("/mine/user")
        }else{
            history.replace("/mine/login")
        }
    }

6-5 登录界面实现登录功能

mine/login组件也需要引入CommonGroup,包裹一下UI组件,包裹之后就可以拿到用户信息。

import React,{Component} from "react"
import "./index.scss"
import {CommonsGroup} from "../../../../modules/group"
class Login extends Component{
    constructor(){
        super()
        this.login = this.login.bind(this)
    }
    login(){
        //登录   改变userInfo这个状态
        this.props.login()
    }
    render(){
        return (
            <div>
                <button onClick={this.login}>Login</button>
            </div>
        )
    }
}
export default CommonsGroup(Login)

改变redux状态,需要走actionCreators

import {CHECK_USER_INFO} from "./const"
export default {
    login(){
        return dispatch=>{
            setTimeout(() => {
                let action = {
                    type: CHECK_USER_INFO,
                    userInfo:{username:"二狗"}
                }
                dispatch(action)
            }, 1000);
        }
    }
}

commons/reducer.js处理action

//prevState.set方法并没有对之前的状态做任何的改变
//immutbale对象的set方法,会结合之前的immutable对象和设置的值,返回一个全新的对象
//而不会去改变之前的prevState对象。

import {CHECK_USER_INFO} from "./const"
import state from "./state"
import {fromJS} from "immutable"
const reducer = (prevState = fromJS(state),action)=>{ //
    switch (action.type) {
        case CHECK_USER_INFO:  
            return prevState.set("userInfo",action.userInfo)
        default:
            return prevState; //immutable对象!
    }
}

export default reducer;

之后就可以实现点击按钮,1秒钟之后拿到用户数据

image

假设login组件传递用户信息给action

login(){
        this.props.login({
            username:"123",
            password:"456",
            success:data=>{
                alert(data)
                //跳转到个人中心
                this.props.history.replace("/mine/user")
            },
            fail:err=>{
                alert(err)
            }
        })
    }

然后action经过判别用户信息后进行登录

import {CHECK_USER_INFO} from "./const"
export default {
    login({username,password,success,fail}){
        return dispatch=>{
            setTimeout(() => {
                if(username==="123" && password==="456"){
                    let action = {
                        type: CHECK_USER_INFO,
                        userInfo: { username: "二狗" }
                    }
                    dispatch(action)
                    success("登录成功!")
                    return false;
                }
                fail("登录失败!")
            }, 1000);
        }
    }
}

当前端页面不传递fail值的时候如果登录失败系统会报错,说fail未定义,为了增强代码的健壮性,我们可以做一个优化。就是当success或fail为真的时候再去执行后续操作。

setTimeout(()=>{
                if(username === "123" && password === "456"){
                    let action = {
                        type: CHECK_USER_INFO,
                        userInfo: { username: "二狗" }
                    }
                    dispatch(action)
                    success && success("登录成功! ")
                    return false;
                }
                fail && fail("登录失败!")
            },1000)

登录成功后进入个人中心界面,首先也需要用CommonsGroup包裹一下,这样才可以拿到数据。

import React,{Component} from "react"
import "./index.scss"
import {CommonsGroup} from "../../../../modules/group"
class User extends Component{
    render(){
        return(
            <div>
                <p>用户名为:{this.props.userInfo.username}</p>
            </div>
        )
    }
}
export default CommonsGroup(User)

如果直接进入/mine/user页面会报错,说无法从null中取出数据,我们可以在前面加一个判断,当this.props.userInfo为真时再进行后续操作。

<p>用户名为:{this.props.userInfo && this.props.userInfo.username}</p>

实现点击按钮退出功能。

import React,{Component} from "react"
import "./index.scss"
import {CommonsGroup} from "../../../../modules/group"
class User extends Component{
    constructor(){
        super()
        this.exit = this.exit.bind(this)
    }
    exit(){
        this.props.exit(); //redux里面更改状态的方法
        this.props.history.replace("/mine/login") //跳转到登录界面
    }
    render(){
        return (
            <div>
                <p><button onClick={this.exit}>退出</button></p>
                <p>用户为:{this.props.userInfo&&this.props.userInfo.username}</p>
            </div>
        )
    }
}
export default CommonsGroup(User)

在store/commons/actionCreators.js中添加exit方法

exit(){
        let action = {
            type: CHECK_USER_INFO,
            userInfo:null
        }
        return action;
    }

考虑一下:

在登录成功的时候,login的success回调函数里面实现的跳转功能,然后在个人中心界面点击退出,在其中实现的退出功能。考虑到这登录与个人中心组件都在Mine组件里面,可否让mine组件实现这两个的跳转呢?

Mine组件充当两个角色,路由&UI组件。

userInfo改变了,相当于状态改变了,Mine组件的componentWillReceiveProps钩子就会执行。

路由发生变化的时候,/mine/login ==> /mine/user 路由变化了,Mine组件的componentWillReceiveProps钩子也会被执行。

所以在mine组件的componentWillReceiveProps钩子函数里面,根据状态来实现Login和User的跳转功能。

	componentWillReceiveProps(nextProps){
        if(nextProps.userInfo !== this.props.userInfo)			{//说明redux里面的userInfo状态改变了
            this.checkUserInfo(nextProps)
        }
    }

    checkUserInfo(props){ //跳转的方法
        let {userInfo,history} = props || this.props;
        if(userInfo){ //说明用户已经登录了
            history.replace("/mine/user")
        }else{
            history.replace("/mine/login")
        }
    }

当我们在/mine/user页面再点击我的时会跳转到/mine页面,这时候路由发生变化,我们要在if语句中再添加一个判断,当前面一个语句为假的时候执行后面的语句,让这两种情况都执行this.checkUserInfo(nextProps)方法。

if(nextProps.userInfo !== this.props.userInfo || nextProps.location.pathname==="/mine")

6-6 引入antd-mobile组件库

	cnpm install antd-mobile --save 

	cnpm install babel-plugin-import  -D

webpack.config.js文件 搜索babel-loader

plugins: [
        ......       
       ["import", { libraryName: "antd-mobile", style: "css" }] 
],

6-7 登录注册页面

先引入antd-mobile组件库中的NavBar组件

import { NavBar, Icon } from 'antd-mobile';

在页面中渲染NavBar

render(){
        return(
            <div>
                <NavBar
                    mode="light"
                    icon={<Icon type="left" />}
                    onLeftClick={() => this.props.history.replace("/")}
                    rightContent={[
                        <Icon key="1" type="ellipsis" />,
                    ]}
                >NavBar</NavBar>
            </div>
        )
    }

将手机号登录单独抽离出来一个组件LoginTextForm.js

import React,{Component} from "react"
class LoginTextForm extends Component{
    render(){
        return(
            <form>
                <div className = "form-group">
                    <input type = "text" placeholder = "手机号"/>
                </div>
                <div className="form-group">
                    <input type="text" placeholder="验证码" />
                </div>
                <button>登录</button>
            </form>
        )
    }
}
export default LoginTextForm;

在index.js中引入

import LoginTextForm from "./LoginTextForm"

为LoginTextForm.js组件编写样式,然后在首页添加一个p标签,实现点击切换表单的功能。

<p className = "change-type">账户密码登录</p>

将渲染的内容单独封装成一个函数changeLoginType()

changeLoginType(){
        return(
            <div className="content">
                <LoginTextForm />
                <p className="change-type">账户密码登录</p>
            </div>
        )
    }

给p标签添加一个点击事件changeType,我们想实现点击p标签表单切换,就要让页面重新渲染,这时候就要定义一个默认状态。

constructor(){
        super()
        this.login = this.login.bind(this)
        this.state = {
            loginType:"text";    
        }
    }

当点击p标签时,changeType方法被调用,状态改变,render函数重新执行。

let {loginType} = this.state;
const changeType = () =>{
            this.setState({
                loginType:"user"
            })
        }

这时候changeLoginType()又会被调用,执行if语句,进行判断,将Form重新赋值为LoginUserForm,然后在页面上重新渲染。

if(loginType !== "text"){
            Form = LoginUserForm
        }

给title单独赋值,当切换的时候title也改变。

let title = "账号密码登录"

if(loginType !== "text"){
            Form = LoginUserForm
            title = "短信快捷登录"
        }
        
<p onClick = {changeType} className="change-type">{title}</p>

NavBar的标题栏也随之改变

{this.state.loginType === "text" ? "短信快捷" : "账户密码"}登录

现在我们可以实现点击标签切换组件,但是没法再切换回去,所以我们单独定义一个type为user,点击p标签时,执行changeType方法,将type赋值给loginType改变状态,然后判断当loginType !== "text"的时候,再将type重新赋值回"text",从而实现了点击p标签两个表单来回切换的效果。

changeLoginType(){
        let Form = LoginTextForm;
        let {loginType} = this.state;
        let title = "账号密码登录"
        let type = "user"
        if(loginType !== "text"){
            Form = LoginUserForm
            title = "短信快捷登录"
            type = "text"
        }
        const changeType = () =>{
            this.setState({
                loginType:type
            })
        }
        return(
            <div className="content">
                <Form />
                <p onClick = {changeType} className="change-type">{title}</p>
            </div>
        )
    }

效果演示:

image

6-8 登录功能

在Login的index.js中给button标签添加type = "submit",同时form表单也要添加onSubmit

return(
            <form onSubmit = {this.handleSubmit}>
                <div className = "form-group">
                    <input type = "text" placeholder = "手机号"/>
                </div>
                <div className="form-group">
                    <input type="text" placeholder="验证码" />
                </div>
                <button type = "submit" className = "login">登录</button>
            </form>
        )

在actionCreators中添加loginByText和loginByUser两个方法

    loginByText({ phone, code, success, fail }) {
        return dispatch => {
            setTimeout(() => {
                if (phone === "110" && code === "456") {
                    let action = {
                        type: CHECK_USER_INFO,
                        userInfo: { username: "马云" }
                    }
                    dispatch(action)
                    success && success("手机登录成功! ")
                    return false;
                }
                fail && fail("手机登录失败!")
            }, 1000)
        }
    },
    loginByUser({ username, password, success, fail }) {
        return dispatch => {
            setTimeout(() => {
                if (username === "123" && password === "456") {
                    let action = {
                        type: CHECK_USER_INFO,
                        userInfo: { username: "马云" }
                    }
                    dispatch(action)
                    success && success("用户登录成功! ")
                    return false;
                }
                fail && fail("用户登录失败!")
            }, 1000)
        }
    }

在Login的LoginTextForm.js中引入CommonsGroup并包裹LoginTextForm,让LoginTextForm通过this.props.loginByText得到actionCreators中定义的方法。

我们采用非受控组件的方法获取数据

<div className = "form-group">
                    <input ref = {el => this.phone = el} type = "text" placeholder = "手机号"/>
                </div>
                <div className="form-group">
                    <input ref={el => this.code = el} type="text" placeholder="验证码" />
                </div>

在LoginTextForm中编写handleSubmit方法,将参数传递给actionCreators

handleSubmit = () => {
        this.props.loginByText({
            phone:this.phone.value,
            code:this.code.value,
            success:data=>{
                alert(data)
            },
            fail:err=>{
                alert(err)
            }
        })
    }

再复制一份改成LoginUserForm.js

import React, { Component } from "react"
import { CommonsGroup } from "../../../../modules/group"
class LoginUserForm extends Component {
    handleSubmit = () => {
        this.props.loginByUser({
            username: this.username.value,
            password: this.password.value,
            success: data => {
                alert(data)
            },
            fail: err => {
                alert(err)
            }
        })
    }
    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                <div className="form-group">
                    <input ref={el => this.username = el} type="text" placeholder="用户名" />
                </div>
                <div className="form-group">
                    <input ref={el => this.password = el} type="text" placeholder="密码" />
                </div>
                <button type="submit" className="login">登录</button>
            </form>
        )
    }
}
export default CommonsGroup(LoginUserForm);

引入组件库中的Toast

image

在用户登录成功的时候我们给出一个轻提示

success:data=>{
                Toast.success(data, 1);
            }

效果演示:

image

登录失败的时候再回调函数中将密码清空,同时获取焦点

fail:err=>{
                Toast.fail(err, 1, ()=>{
                    this.code.value=""
                    this.code.focus()
                })
            }

效果演示:

image

阻止掉登录按钮的默认行为

e.preventDefault();

由于掘金的字数限制,我只能将这篇总结分为上下两个章节发布,感兴趣的小伙伴请继续阅读《React全家桶高仿「饿了么」APP-下》。或者直接移步简书:视觉派Pie,获得更完整的阅读体验。