12.React实战开发--团购webapp

源码:https://github.com/Ching-Lee/react_shopping

1.环境搭建

1.1 使用react脚本架搭建项目(详情参见第一节)
  • 创建项目:
create-react-app dianping
  • 安装react-router2.8版本


  • 使用的是react-router2.8版本,保证react和react-dom都是15的版本
    ,否则会报错。
   "react": "^15.6.2",
    "react-dom": "^15.6.2",
    "react-router": "^2.8.1",

创建路由routerMap.js

import React from 'react';
import {Router,hashHistory,Route,IndexRoute} from 'react-router';
import Home from './containers/home';
import App from './App';
export default class RouterMap extends React.Component{
    render(){
        return(
            
                
                    
                
            
        );
    }
}

父组件App.js

import React from 'react';

import './App.css';

class App extends React.Component {
    render() {
        return (
            
{this.props.children}
); } } export default App;
1.2 引入图标

进入官网https://icomoon.io/app/#/select,导入提供的图标图片,可以生成图标。

1.3 后端数据模拟
  • 使用koa
npm install koa koa-body koa-router --save-dev
  • 修改package.json


  • mock目录



    ad.js中是首页广告的json数据

module.exports = [
    {
        title: '暑假5折',
        img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016191639092-2000037796.png',
        link: 'http://www.imooc.com/wap/index'
    },
    {
        title: '特价出国',
        img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016191648124-298129318.png',
        link: 'http://www.imooc.com/wap/index'
    },
    {
        title: '亮亮车',
        img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016191653983-1962772127.png',
        link: 'http://www.imooc.com/wap/index'
    },
    {
        title: '学钢琴',
        img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016191700420-1584459466.png',
        link: 'http://www.imooc.com/wap/index'
    },
    {
        title: '电影',
        img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016191706733-367929553.png',
        link: 'http://www.imooc.com/wap/index'
    },
    {
        title: '旅游热线',
        img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016191713186-495002222.png',
        link: 'http://www.imooc.com/wap/index'
    }
]

list.js中是首页-推荐列表

module.exports = {
    hasMore: true,
    data: [
        {
            img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016201638030-473660627.png',
            title: '汉堡大大',
            subTitle: '叫我汉堡大大,还你多彩口味',
            price: '28',
            distance: '120m',
            mumber: '389'
        },
        {
            img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016201645858-1342445625.png',
            title: '北京开源饭店',
            subTitle: '[望京]自助晚餐',
            price: '98',
            distance: '140m',
            mumber: '689'
        },
        {
            img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016201652952-1050532278.png',
            title: '服装定制',
            subTitle: '原价xx元,现价xx元,可修改一次',
            price: '1980',
            distance: '160',
            mumber: '106'
        },
        {
            img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016201700186-1351787273.png',
            title: '婚纱摄影',
            subTitle: '免费试穿,拍照留念',
            price: '2899',
            distance: '160',
            mumber: '58'
        },
        {
            img: 'http://images2015.cnblogs.com/blog/138012/201610/138012-20161016201708124-1116595594.png',
            title: '麻辣串串烧',
            subTitle: '双人免费套餐等你抢购',
            price: '0',
            distance: '160',
            mumber: '1426'
        }
    ]
}

server.js模拟服务端

const Koa = require('koa');
const Router = require('koa-router');
const KoaBody=require('koa-body');
const app = new Koa();
const router = new Router();
const koaBody=new KoaBody();

// 首页 —— 广告(超值特惠)
const homeAdData = require('./home/ad.js');

// 首页 —— 推荐列表(猜你喜欢)
const homeListData = require('./home/list.js');


router.get('/api/homead', async (ctx) => {
    ctx.body = homeAdData
}).get('/api/homelist/:city/:page', async (ctx) => {
    // 参数
    const params = ctx.params;
    const paramsCity = params.city;
    const paramsPage = params.page;

    console.log('当前城市:' + paramsCity);
    console.log('当前页数:' + paramsPage);

    ctx.body = homeListData;
});


/*
router.post('/api/post',koaBody,async (ctx) => {
    ctx.body = JSON.stringify(this.request.body);
});

*/

// 开始服务并生成路由
app.use(router.routes(), router.allowedMethods());
app.listen(3000);
  • 服务开启



  • 服务器运行在3000端口,而网站前端运行在3001端口,我们发请求时是在3001端口,为了在3001端口也能获得同样的结果,需要在package.json中实现代理转发,将所有api/下的请求转发到3000端口。


2.Home页面

2.1 header头部

import React from 'react';
import '../../static/css/font.css';
import './homeheader.css'

export default class HomeHeader extends React.Component{
    render(){
        return(
            
深圳
 
); } }

使用了flex布局,两端分别靠左右,中间自适应。

.home_header{
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: rgb(233,32,61);
    padding: 10px;
    color:#fff;
    font-size: 16px;
    line-height: 1;
}
.left_header{
    width: 60px;
}
/*中间搜索框*/
.middle_header{
    width: 70%;
    border-radius: 15px;
    background-color: #fff;
    padding: 5px;
    overflow: hidden;
}

.middle_header i{
    color:#ccc
}

.middle_header input{
    font-size: 16px;
    font-weight: normal;
    padding: 0;
    border: 0;
}

.right_header{
    width:30px;
    text-align: right;
}
2.2 category分类
  • 需要轮播功能,引入react-swipe插件
    https://www.npmjs.com/package/react-swipe
npm install react swipe-js-iso react-swipe

  • 在ReactSwipe中的每一个div,表示一个轮播面板。
    每个div都是由ul和li构成,ul采用自适应布局flex。
    我们自己定义了一个组件CategoryItem,用于表示每一项li,包括图片和文字。
    圆点circle类,表示轮播图下方的点
    1)CategoryItem组件


import React from 'react'

export default class CategoryItem extends React.Component {
    constructor() {
        super();
    }

    render() {
        let psty={
            textAlign:'center',
            margin:0,
            fontSize:'14px'

        };

        let imgsty={
          width:'40px',
        };
        let listy={
            textAlign:'center',
            width:'60px',
            marginLeft:'5px',
            marginRight:'5px',
            listStyle:'none'
        };
        return (
            
  • {this.props.text}

  • ); } }

    2)Category组件:

    import React from 'react';
    import ReactSwipe from 'react-swipe';
    import './Category.css'
    import CategoryItem from './category_item';
    
    export default class Category extends React.Component{
        constructor(){
            super();
            this.state={index:0};
        }
        render(){
            //当我们滑动轮播图,会返回一个索引值index,表示当前页面
            //index值为0,1,2
            let opt={
                continuous: false,
                callback: index=> {
                    this.setState({index:index});
                }
    
            };
    
    
            return(
                
    ); } }

    './Category.css'

    .carousel ul{
        padding-left: 0px;
        display: flex;
        justify-content: space-around;
        flex-wrap: wrap;
    }
    
    .circle{
        text-align: center;
        padding-left: 0px;
    }
    
    .circle li{
        list-style: none;
        width: 8px;
        height: 8px;
        border-radius: 4px;
        background-color: #ccc;
        margin:0 4px;
        display: inline-block;
    }
    
    li.selected{
        background-color: red;
    }
    

    2.3 超值特惠模块

    • 需要数据交互,首先开启后台数据。

    • 安装fetch框架用于发送Ajax请求
    npm install fetch --save
    
    • recomment组件
      1)首先在componentWillMount()方法中向服务器发起请求,获取json,
      将获得的json值赋给this.state.ad。
      2)开始渲染时,将每一项使用map函数,包装成li,li的宽为120px。
      li中包括标题和图片,使其内容居中,使用text-align:center。
      3)将生成的adlist放入ul中,ul采用flex布局,设置justifyContent:'space-around'均匀分布,设置flexWrap:'wrap'自动换行。
    import React from 'react';
    
    export default class Recomment extends React.Component {
        constructor() {
            super();
            this.state = {ad: ''}
        }
    
        componentWillMount() {
            let myFetchOptions = {method: 'GET'};
            fetch('/api/homead', myFetchOptions)
                .then(response => response.json())
                .then(json => this.setState({ad: json}));
        };
    
        render() {
            const ad = this.state.ad;
            let colors=['green','blue','yellow','orange','red','pink'];
            const adList = ad.length ?
                ad.map((adItem, index) => (
                    
  • {adItem.title}

  • )) : '加载中'; return (

    超值特惠

      {adList}
    ); } }
    • 遇到问题:
      本地调试的时候,图片src引用了第三方网站的图片资源,导致控制台出现了如下的报错:

    解决方法:
    在html中添加

    
    

    2.4 猜你喜欢模块


    • 首先在 componentWillMount()中向后台发起请求,获取json,将json中hasmore赋值给state中hasmore,将json中的data赋值给state中的list。
    • 渲染时遍历list,返回likelist。
      div是由img和content(div)两个部分构成。
      img:向左浮动,content也向左浮动,同时其父组件要清除浮动。
      content中分为上中下三个部分:
      1)上面的div中包括两个span,一个向左浮动,一个向右浮动,父组件清除浮动。
      2)中间是一个


      3)下面也是两个span,一个向左浮动,一个向右浮动。

    import React from 'react';
    import './likelist.css'
    
    export default class LikeList extends React.Component {
        constructor() {
            super();
            this.state = {list: '',hasMore:false,page:0}
        }
    
        componentWillMount() {
            let myFetchOptions = {method: 'GET'};
            fetch('/api/homelist/'+this.props.city+'/'+this.state.page, myFetchOptions)
                .then(response => response.json())
                .then(json => this.setState({list: json.data,hasMore:json.hasMore}));
        };
    
        render(){
            const like=this.state.list;
            let likeList=like.length?
                like.map((likeItem,index)=>(
                    
    {likeItem.title} {likeItem.distance}

    {likeItem.subTitle}

    ¥{likeItem.price} 已售{likeItem.mumber}
    )) :'加载中'; return(

    猜你喜欢

    {likeList}
    ); } }
    .like_item{
        width:100%;
        padding: 10px;
        border-bottom: 1px #cccccc solid;
    }
    .clearfix:after{
        display: block;
        clear: both;
        content: '';
    }
    .left{
        float: left;
        width:125px;
    }
    
    .content{
        float:left;
        width:220px;
        padding:0 10px ;
    }
    
    .title{
        float:left;
        font-size: 16px;
        font-weight: bold;
    
    }
    
    .distance{
        float: right;
        font-size: 12px;
        line-height: 21px;
        color:grey;
    }
    
    .subTitle{
        margin-top:10px;
        margin-bottom: 1.5em;
        font-size: 14px;
    }
    
    .price{
        float:left;
        color:red;
        font-family: Arial;
        font-size: 20px;
        font-weight: bold;
    }
    
    .mumber{
        float: right;
        color:grey;
        font-size: 14px;
    }
    
    • 点击按钮加载更多功能。



      1)LoadMore组件
      传递了isLoadingMore属性,用来表示是加载中还是加载更多。
      传递了一个方法,this.props.loadMoreFn(),用来实现点击加载更多后的事件。

    import React from 'react';
    export default class LoadMore extends React.Component{
        constructor(props){
            super(props);
        }
    
        handleClick(){
            this.props.loadMoreFn();
        }
        render(){
            return(
                
    { this.props.isLoadingMore? 加载中... :加载更多 }
    ); } }

    2)在likeList中state添加属性isLoadingMore,方法loadMoreData(),LoadMore组件

     this.state = {
                list: [], //存储列表信息
                page: 0, //请求的页码
                hasMore:false,
                isLoadingMore:false,
            }
    
    //点击加载更多触发
        loadMoreData() {
            //记录状态
            this.setState({isLoadingMore: true});
            let page=this.state.page+1;
            //发送请求
            let myFetchOptions = {method: 'GET'};
            fetch('/api/homelist/' + this.props.city + '/' + page, myFetchOptions)
                .then(response => response.json())
                .then(json => this.setState({list: this.state.list.concat(json.data), hasMore: json.hasMore}));
            //设置page
            this.setState({page: page, isLoadingMore: false});
    
     return (
                

    猜你喜欢

    {likeList} { this.state.hasMore? : '' }
    );
    • 上拉就加载更多
      主要是监听滚轮事件,判断底部加载更多div和顶部的距离top,如果top比屏幕距离小,表示它被暴露出来,下滑加载数据。
      在LoadMore组件中添加方法
     componentDidMount(){
            const loadMoreFn=this.props.loadMoreFn;
            const wrapper=this.refs.wrapper;
            let timeoutId;
            function callback(){
                //得到加载更多div距离顶部的距离
               let top=wrapper.getBoundingClientRect().top;
               let windowHeight=window.screen.height;
               //如果top距离比屏幕距离小,说明加载更多被暴露
               if(top&&top

    3.选择城市页面

    • 因为城市信息是我们需要在各个组件中共享的信息,比如homeheader的city显示,猜你喜欢的部分也需要发送城市名称才能获取相应数据。所以我们首先需要搭建Redux数据流环境,参见第13节。
    • 在routerMap中添加路由
       
                    
                        
                        
                    
                
    
    • 跳转到城市选择页
      不会刷新页面,react和react-router监听hash的变化,然后js层重新渲染页面,
      里面并没有页面的请求。
      在之前写的HomeHeader组件中添加跳转链接
     
    {this.props.cityName}
    .left_header a{
        text-decoration:none;
        color:#fff;
    }
    
    .left_header a:hover{
        color:gray ;
    }
    
    • city页面


    import React from 'react';
    import {bindActionCreators} from 'redux';
    import {connect} from 'react-redux';
    import * as userInfoActions from '../action/userInfoActions'
    import PageHeader from '../component/common/pageHeader'
    class City extends React.Component{
        render(){
            return(
                
    ); } } // -------------------redux react 绑定-------------------- function mapStateToProps(state) { return { userinfo: state.userinfo } } //触发数据改变 function mapDispatchToProps(dispatch) { return { userInfoActions: bindActionCreators(userInfoActions, dispatch), } } export default connect( mapStateToProps, mapDispatchToProps )(City)
    • 头部组件PageHeader


    import React from 'react';
    import '../../static/css/font.css';
    import './pageheader.css'
    
    export default class PageHeader extends React.Component {
    
        constructor(props) {
            super(props);
        }
    
        clickHandle() {
            window.history.back();
        }
    
    
        render() {
            return (
                

    {this.props.title}

    ); } }

    首先p元素让他占一行,并且文字居中。之后i采用了绝对定位,采用绝对定位会脱离文档流。

    .page_header{
        position: relative;
    }
    .page_header h1{
        text-align: center;
        background-color: rgb(233,32,61);;
        font-size: 16px;
        color:white;
        padding: 15px 10px;
        margin: 0;
    }
    
    .page_header i{
        position: absolute;
        left: 10px;
        top: 15px;
        color: white;
    }
    
    • 显示当前城市



    currentCity组件

    import React from 'react';
    import './currentCity.css'
    
    export default class CurrentCity extends React.Component {
    
        constructor(props) {
            super(props);
        }
    
    
        render() {
            return (
                

    {this.props.currentCity}

    ); } }
    • 热门城市列表



      设计了一个cityList组件



      在cityList组件中涉及到当点击城市li时,会触发点击事件,这个事件需要调用父组件中的方法, changeCity(newCity),将新的城市更新到redux中,同时更新localstorage,最后跳转到首页。
      CityList组件
    import React from 'react'
    import './citylist.css'
    export default class CityList extends React.Component{
        clickHandle(newcity){
            const changeFn=this.props.changeFn;
            changeFn(newcity);
        }
        render(){
            return(
                

    热门城市

    • 北京
    • 上海
    • 杭州
    • 深圳
    • 广州
    • 西安
    • 成都
    • 长沙
    • 无锡
    ); } }
    .city_list{
        display:flex;
        flex-wrap:wrap;
       justify-content:space-around;
        padding-left: 0px;
    
    }
    
    .city_list li{
        width:25%;
        list-style:none;
        border:#ccc 1px solid;
        text-align: center;
        margin-bottom: 1em;
    }
    
    

    city组件中的调用,及传递的方法

    class City extends React.Component{
        changeCity(newCity){
            if(newCity==null)
                return;
          //修改redux
            const userinfo=this.props.userinfo;
            userinfo.cityName=newCity;
            this.props.userInfoActions.update(userinfo);
          //修改localstorage
            localStorage.cityName=newCity;
    
            //跳转到首页
            hashHistory.push('/');
        }
        render(){
            return(
                
    ); } }

    4.搜索功能

    4.1 种类跳转
    • 添加路由
    
    
    • 在Category页面为CategoryItem添加
     
    

    4.2 搜索跳转

    HomeHeader组件中:
    使用state来保存keyword搜索关键字,两个事件,onChange的时候更新state中的关键字,onKeyUp判断如果是enter,跳转页面,使用hashHistory.push

    import {Link,hashHistory} from 'react-router'
    
    
    export default class HomeHeader extends React.Component {
        constructor(){
            super();
            this.state={kwd:''}
        }
        ChangeHandle(e){
            let val=e.target.value;
            this.setState({
                kwd:val
            });
        }
    
        KeyUpHandle(e) {
            if (e.keyCode !== 13)
                return;
            hashHistory.push('/search/all/'+encodeURIComponent(this.state.kwd))
        }
    ......
    
      
    

    4.3 search页面

    • 抽离SearchInput组件


      将这部分抽离出来复用
    import React from 'react';
    import './searchinput.css'
    export default class HomeHeader extends React.Component {
        constructor() {
            super();
            this.state = {kwd: ''}
        }
    
        ChangeHandle(e) {
            let val = e.target.value;
            this.setState({
                kwd: val
            });
        }
       //在父组件中定义了一个页面跳转的方法。
        KeyUpHandle(e) {
            if (e.keyCode !== 13)
                return;
            this.props.enterHandle(this.state.kwd);
        }
    
        render(){
            return(
                
     
    ); } }
    /*中间搜索框*/
    .middle_header{
        width: 70%;
        border-radius: 15px;
        background-color: #fff;
        padding: 5px;
        overflow: hidden;
    }
    
    .middle_header i{
        color:#ccc
    }
    
    .middle_header input{
        font-size: 16px;
        font-weight: normal;
        padding: 0;
        border: 0;
        width: 80%;
    }
    

    在HomeHeader中修改,调用searchInput的时候传递了一个方法,用于实现页面跳转。

    export default class HomeHeader extends React.Component {
    
        EnterHandle(value) {
            hashHistory.push('/search/all/'+encodeURIComponent(value));
        }
    
        render() {
            return (
                
    {this.props.cityName}
    ); } }
    • Search组件
      我们希望将主页搜索的内容显示到搜索页面的搜索框内。
      所以首先创建Search组件,通过this.props.params能够获取到传递的参数。
      将参数传递给SearchHeader


    import React from 'react';
    import SearchHeader from '../component/search/searchHeader';
    export default class Search extends React.Component{
        render(){
            const params=this.props.params;
            return(
                
    ); } }
    • 实现SearchHeader组件




      //在SearchHeader组件中向SearchInput组件传递了一个参数,keyword,
      需要在SearchInput组件中将keyword的值赋值给this.state>kwd

    import React from 'react'
    import SearchInput from '../common/searchInput'
    import './searchHeader.css'
    import '../../static/css/font.css'
    import {Link,hashHistory} from 'react-router'
    export default class SearchHeader extends React.Component{
        enterHandle(value) {
            hashHistory.push('/search/all/'+encodeURIComponent(value));
        }
    
        clickHandle() {
            window.history.back();
        }
    
        render(){
            return(
                
    ); } }

    SearchInput组件中做相应的修改

        componentDidMount(){
           this.setState({kwd:this.props.keyword});
        }
    
    • 实现SearchList组件,需要向后台获取数据。
      首先在mock数据中添加搜索相关j的son



      在server.js中添加服务

    const searchListData=require('./search/list');
    //搜索结果页 - 搜索结果 - 两个参数
    router.get('/api/search/:page/:city/:category', async (ctx) => {
        // 参数
        const params = ctx.params;
        const paramsPage = params.page;
        const paramsCity = params.city;
        const paramsCategory = params.category;
    
        console.log('当前页数:' + paramsPage);
        console.log('当前城市:' + paramsCity);
        console.log('当前类别:' + paramsCategory);
    
        ctx.body = searchListData;
    });
    

    重启服务



    1)将首页中的猜你喜欢每一条抽取出来一个likeItem组件以供复用


    抽离出来以供复用
    import React from 'react';
    import './likeItem.css'
    export default class LikeItem extends React.Component{
        render(){
            return(
                
    {this.props.item.title} {this.props.item.distance}

    {this.props.item.subTitle}

    ¥{this.props.item.price} 已售{this.props.item.mumber}
    ); } }
    1. searchList组件
      首先实现向后台获取数据并展示的功能:


    import React from 'react';
    import LikeItem from '../home/likelist/likeItem';
    import { connect } from 'react-redux';
    class SearchList extends React.Component{
        constructor(props){
         super(props);
            this.state = {
                list: [], //存储列表信息
                page: 0, //请求的页码
                hasMore:false,
                isLoadingMore:false,
            }
        }
    
        componentDidMount(){
            const myfetchOption={method:'GET'};
            const cityName=this.props.userinfo.cityName;
            const keyword=this.props.keyword;
            const category=this.props.category;
            fetch('/api/search/'+this.state.page+'/'+cityName+'/'+category,myfetchOption).then(response => response.json())
                .then(json => this.setState({list: json.data, hasMore: json.hasMore}));
        }
        render(){
            const search=this.state.list;
            const searchList=search.length?
                search.map((searchItem,index)=>(
                    
                ))
    
                :'加载中...';
            return(
                 
    {searchList}
    ); } } // -------------------redux react 绑定-------------------- function mapStateToProps(state) { return { userinfo: state.userinfo } } function mapDispatchToProps(dispatch) { return { } } export default connect( mapStateToProps, mapDispatchToProps )(SearchList)

    然后实现加载更多的功能,直接使用主页写好的loadmore组件

     return(
                 
    {searchList} { this.state.hasMore? : '' }
    );
    
        //点击加载更多触发
        loadMoreData() {
            //记录状态
            this.setState({isLoadingMore: true});
            let page=this.state.page+1;
            //发送请求
            let myFetchOptions = {method: 'GET'};
            const cityName=this.props.userinfo.cityName;
            const category=this.props.category;
            fetch('/api/search/'+this.state.page+'/'+cityName+'/'+category, myFetchOptions)
                .then(response => response.json())
                .then(json => this.setState({list: this.state.list.concat(json.data), hasMore: json.hasMore}));
            //设置page
            this.setState({page: page, isLoadingMore: false});
    
        }
    

    处理重新搜索

        // 处理重新搜索
        componentDidUpdate(prevProps, prevState) {
            const keyword = this.props.keyword;
            const category = this.props.category;
    
            // 搜索条件完全相等时,忽略。重要!!!
            if (keyword === prevProps.keyword && category === prevProps.category) {
                return
            }
    
            // 重置 state
            this.setState(initialState);
    
            // 重新加载数据
            this.loadFirstPageData()
        }
    
    
      //第一次加载数据
        loadFirstPageData(){
            const myfetchOption={method:'GET'};
            const cityName=this.props.userinfo.cityName;
            const category=this.props.category;
            fetch('/api/search/'+this.state.page+'/'+cityName+'/'+category,myfetchOption).then(response => response.json())
                .then(json => this.setState({list: json.data, hasMore: json.hasMore}));
        }
    
    const initialState={
        list: [], //存储列表信息
        page: 0, //请求的页码
        hasMore:false,
        isLoadingMore:false,
    };
    

    5.商户详情页

    • 在routerMap中添加路由
    
    
    • 创建detail组件


    import React from 'react';
    import PageHeader from '../component/common/pageHeader';
    
    export default class Detail extends React.Component{
        render(){
            const params=this.props.params;
            return(
                
    ); } }
    • 为每一个item添加Link
      在likeIte组件中
    ....
    • detail页面需要向后台发送数据获取商家信息和评论信息


      mock中添加相关内容

      修改server.js

    //详情页
    
    //获取商家信息
    const infoData=require('./detail/info');
    router.get('/api/detail/info/:id', async (ctx) => {
        // 参数
        const params = ctx.params;
        const paramsId = params.id;
    
        console.log('当前商家id:' + paramsId);
    
    
        ctx.body = infoData;
    });
    //获取评论信息
    const comment=require('./detail/comment');
    router.get('/api/detail/comment/:page/:id', async (ctx) => {
        // 参数
        const params = ctx.params;
        const paramsId = params.id;
        console.log('当前商家id:' + paramsId);
        ctx.body = comment;
    });
    

    重启服务



    • 商户信息组件



      1)实现Info组件
      Info组件向后台获取信息

    import React from 'react';
    import DetailInfo from './detailInfo';
    export default class Info extends React.Component{
        constructor(props){
            super(props);
            this.state={info:false};
        }
    
        componentDidMount(){
            const myfetchOption={method:'GET'};
            fetch('/api/detail/info/'+this.props.id,myfetchOption).then(response => response.json())
                .then(json => this.setState({info: json}));
        }
    
        //只有一个元素不能用array,json是一个Object,array的length还是0
        render(){
            const info=this.state.info;
            const dataList=info?
                 
                :'加载中...';
    
            return(
                
    {dataList}
    ); } }

    2)首先实现star组件
    star从其父组件获取star个数,利用数组的map来实现,比较item的值和star的值。star值>=item值,则设置class light,将具有light类的i标签设置为红色。

    import React from 'react';
    import './star.css'
    export default class DetailInfo extends React.Component{
        constructor(props){
            super(props);
        }
        render(){
            let star=this.props.star||0;
            if(star>5){
                star=star%5;
            }
            return(
                
                    {
                        [1,2,3,4,5].map((item,index)=>{
                            const lightClass=star>=item?' light':'';
                            return 
                        })
    
                    }
                
            );
        }
    }
    
    i.light {
       color:red;
    }
    

    3)实现DetailInfo组件

    import React from 'react';
    import './detailinfo.css';
    import Star from './star';
    export default class DetailInfo extends React.Component{
        render(){
            const info=this.props.info;
            return(
                
    {info.title}/

    {info.title}

    ¥{info.price}

    {info.subTitle}

    ); } }
    • 评论信息



      Comment组件:

    import React from 'react';
    import Star from '../info/star';
    import LoadMore from '../../home/likelist/LoadMore';
    import './comment.css'
    const initialState = {
        list: [], //存储列表信息
        page: 0, //请求的页码
        hasMore: false,
        isLoadingMore: false,
    };
    export default class Comment extends React.Component {
        constructor(props) {
            super(props);
            this.state = initialState;
        }
    
        componentDidMount() {
            this.loadFirstPageData();
        }
    
        //第一次加载数据
        loadFirstPageData() {
            const myfetchOption = {method: 'GET'};
            fetch('/api/detail/comment/' + this.state.page + '/' + this.props.id, myfetchOption).then(response => response.json())
                .then(json => this.setState({list: json.data, hasMore: json.hasMore}));
        }
    
        //点击加载更多触发
        loadMoreData() {
            //记录状态
            this.setState({isLoadingMore: true});
            let page = this.state.page + 1;
            //发送请求
            let myFetchOptions = {method: 'GET'};
            fetch('/api/detail/comment/' + this.state.page + '/' + this.props.id, myFetchOptions)
                .then(response => response.json())
                .then(json => this.setState({list: this.state.list.concat(json.data), hasMore: json.hasMore}));
            //设置page
            this.setState({page: page, isLoadingMore: false});
    
        }
    
        render() {
            let comment = this.state.list;
            const commentList = comment.length ?
                comment.map((item, index) => (
                    

    {item.username}

    {item.comment}

    )) : '加载中'; return (

    用户点评

    {commentList} { this.state.hasMore? : '' }
    ); } }
    .comment{
        padding:10px;
        border-top: solid 1em #f1f1f1 ;
    }
    .comment h4{
        margin-top: 0px;
    }
    .comment_info p{
        margin: 0;
    }
    
    .comment_info{
        margin-bottom: 2em;
    }
    
    .comment_content{
        color: grey;
    }
    

    6.登录页面

    • 配置路由
     
    

    主页面HomeHeader组件右边的icon添加跳转链接

     
    • 登陆页面逻辑
      首先要检测用户是否登录(通过连接redux,判断是否有用户信息),如果没有登录显示登录组件,如果登录,直接跳转到用户中心界面。


    import React from 'react';
    import {bindActionCreators} from 'redux';
    import {connect} from 'react-redux';
    import * as userInfoActions from '../action/userInfoActions';
    class Login extends React.Component {
        constructor(props) {
            super(props);
            this.state={checking:true};
        }
    
        componentDidMount(){
            this.doCheck();
        }
        //检测是否登陆
        doCheck(){
            const userinfo=this.props.userinfo;
            if(userinfo.username){
                //已经登录,直接转到用户中心
                this.goUserpage();
            }else{
                //尚未登录
                this.setState({
                   checking:false
                });
            }
        }
     
    goUserpage(){
            hashHistory.push('/User');
        }
    
       render() {
            const params = this.props.params;
            return (
                
    {this.state.checking?
    /*正在检查是否登陆*/
    :
    登录组件
    }
    ); } } // -------------------redux react 绑定-------------------- function mapStateToProps(state) { return { userinfo: state.userinfo } } //触发数据改变 function mapDispatchToProps(dispatch) { return { userInfoActions: bindActionCreators(userInfoActions, dispatch), } } export default connect( mapStateToProps, mapDispatchToProps )(Login)
    • 登录组件



    Login.js在调用LoginComponent组件时,传递了一个登录按钮点击后的方法。

     //登录成功之后的业务处理
        loginHandle(username){
            //保存用户名
            const actions=this.props.userInfoActions;
            let userinfo=this.props.userinfo;
            userinfo.username=username;
            if(username=='')
                return;
    
            actions.update(userinfo);
    
            //跳转连接
            const params=this.props.params;
            const router=params.router;
            if(router)
                hashHistory.push('/'+router);
            else
                this.goUserpage();
        }
    
     
    

    LoginComponent

    import React from 'react';
    import './loginComponent.css';
    export default class LoginComponent extends React.Component {
        constructor(props) {
            super(props);
            this.state = {phone: '',message:'该用户名为空'};
        }
    
        clickHandle() {
            const username = this.state.phone;
            //传过来了一个方法,是点击登录按钮后的处理
            const loginHandle = this.props.loginHandle;
            loginHandle(username);
        }
    
        changeHandle(e) {
          //做了两个验证,验证是否是空,验证是否是手机号
           let value=e.target.value;
           let telReg=/[1][34578]\d{9}$/;
            //验证用户名是否为空
            if(value=='')
                this.setState({message:'该用户名为空'});
            //验证用户名不是手机号
            else if(!telReg.test(value)){
                this.setState({message:'请输入正确的手机号码'});
            }
            else
                this.setState({message:''});
            this.setState({phone: e.target.value});
        }
    
        render() {
            const params = this.props.params;
            return (
                
    {this.state.message}
    ); } }
    .login-container{
        width:300px;
        margin: 100px auto 0 auto;
    }
    
    .input-container{
        border:1px solid rgb(233,32,61);
        padding: 5px 10px;
        border-radius: 5px;
        overflow: hidden;
    }
    
    .input-container input{
        font-size:16px;
        line-height: 1.5em;
        border: none;
        margin-left: 1em;
        width: 85%;
    }
    
    .input-container i{
        color: rgb(233,32,61);
        width:16px;
    }
    
    .password-container{
        margin-top: 0.5em;
    }
    .password-container input{
        width: 50%;
    }
    .password-container button{
        float: right;
        margin-top: 1px;
        border-radius: 5px;
        background-color: #f1f1f1;
        font-size: 14px;
    
    }
    
    .login-button{
        width:100%;
        background-color: rgb(233,32,61);
        color: white;
        border: none;
        border-radius: 5px;
        padding: 0.5em;
        font-size: 16px;
        margin-top: 0.5em;
    }
    

    7.收藏功能


    收藏功能的实现需要redux,redux-thunk插件,具体使用参考第13节。

    • action



      创建三个action,负责收藏信息的更新,添加收藏,删除收藏。
      收藏信息的更新在app.js中初始化从后台获得当前用户所有收藏列表的时候会使用。(初始化功能的实现具体讲解参见13节)
      同时在登录界面,用户一旦登录就应该触发redux中收藏列表的更新。

    //更新收藏列表
    export function update(data) {
        return {
            type: 'STORE_UPDATE',
            data
        }
    }
    
    export function add(item) {
        console.log('触发了add');
        return {
            type: 'STORE_ADD',
            data: item
        }
    }
    
    export function remove(item) {
        return {
            type: 'STORE_REMOVE',
            data: item
        }
    }
    
    //在App.js中完成页初始化,从后台获取该用户的所有收藏商品id存储到redux中
    export function getInitStore(username) {
        return function (dispatch) {
            console.log('getInitStore执行了');
            let option = {method: 'GET'};
            fetch(`/api/store/getStore/${username}`, option)
                .then(res => res.json())
                .then(json => dispatch(update(json)));
        };
    }
    
    //用户添加收藏时,先向后台发送请求
    //item就是调用方法时传入的{id:''}
    export function addStore(item) {
        return function (dispatch) {
            let option = {method: 'GET'};
            fetch(`/api/store/addStore/${JSON.stringify(item)}`, option)
                .then(res => res.json())
                .then(json => {
                        if (json)
                            dispatch(add(item));
                        else
                            alert('网络不畅');
                    }
                );
        };
    
    }
    
    //用户删除收藏时,先向后台发送请求
    export function removeStore(item) {
        return function (dispatch) {
            let option = {method: 'GET'};
            fetch(`/api/store/removeStore/${JSON.stringify(item)}`, option)
                .then(res => res.json())
                .then(json => {
                    if (json)
                        dispatch(remove(item));
                    else
                        alert('删除失败');
                });
        };
    
    }
    
    • reducer


    const initialState =[];
    //收藏创建的规则
    export default function store(state=initialState,action) {
        switch (action.type){
            //修改城市名字
            case 'STORE_UPDATE':
                return action.data;
            case 'STORE_ADD':
                state.unshift(action.data);
                return state;
            case 'STORE_REMOVE':
                return state.filter(item=>{
                    if(item.id!==action.data.id)
                        return item;
                });
            default:
                return state;
        }
    }
    
    • 收藏组件


    • 通过isStore属性来保存是否被收藏的状态。
    • 当渲染结束后,执行componentDidMount()方法,里面包括了 checkStoreState() 方法用来检验用户是否收藏该id的商品。通过获取redux中的store属性的列表,判断其中是否有当前的商品id,如果有,就说明被收藏了,isStore=true。
    • 点击收藏按钮时,绑定了storeHandle()事件。事件中首先判断该用户是否登录。如果没登录,跳转到登录界面。
      如果isStore=true,就触发removeStore的action(向后台发送删除请求,返回true,执行redux中store的删除方法)。
      如果isStore=false,就触发addStore的action(向后台发送添加请求,返回true,执行redux中store的添加方法)
    /*收藏组件*/
    
    import React from 'react';
    import {hashHistory} from 'react-router'
    import {bindActionCreators} from 'redux'
    import {connect} from 'react-redux';
    import * as storeActions from '../../../action/storeActions';
    
    
    class Store extends React.Component {
        constructor(props) {
            super(props);
            this.state = {isStore: false}
        }
    
    
    
        //验证是否登录
        loginCheck() {
            const id = this.props.id;
            const userinfo = this.props.userinfo;
            //把当前详情页的router传递过去,登录完了之后会跳转到原来的页面
            //如果没有用户名,跳转到登录页面
            if (!userinfo.username) {
                hashHistory.push('/login/' + encodeURIComponent('detail/' + id));
                return false;
            }
            return true;
        }
    
        //收藏事件
        storeHandle() {
    
            //验证登录
            const loginFlag = this.loginCheck();
            if (!loginFlag)
                return;
            //收藏的流程
            const id = this.props.id;
            //判断当前页面是否收藏,如果收藏,就取消
            if (this.state.isStore) {
               this.props.storeActions.removeStore({id: id});
               this.setState({isStore: false});
            } else {
                this.props.storeActions.addStore({id: id});
                this.setState({isStore: true});
            }
    
            //跳转到用户主页
            // hashHistory.push('/User');
    
        }
    
    
        render() {
    
            return (
    
                
    
            );
        }
    
        componentDidMount() {
    
    
            this.checkStoreState();
    
        }
    
        //检验当前商户是否被收藏
        checkStoreState() {
            //从父组件传递过来的
            const id = this.props.id;
    
            //some函数只要有一个满足即可
            this.props.store.some(item => {
                    if (item.id === id) {
                        this.setState({isStore: true});
                        return true;
                    }
    
                }
            );
        }
    
    
    }
    
    
    // -------------------redux react 绑定--------------------
    
    //用于收藏和购买部分的功能
    
    function mapStateToProps(state) {
        return {
            userinfo: state.userinfo,
            store: state.store
        }
    }
    
    //触发数据变化
    function mapDispatchToProps(dispatch) {
        return {
            storeActions: bindActionCreators(storeActions, dispatch)
        }
    }
    
    export default connect(
        mapStateToProps,
        mapDispatchToProps
    )(Store)
    

    8.用户中心

    • 配置路由
    
    
    • 如果用户没有登录,就跳转到登录页面,用户已经登录,就显示User组件
    import React from 'react';
    import PageHeader from '../component/common/pageHeader';
    import {bindActionCreators} from 'redux';
    import {connect} from 'react-redux';
    import {hashHistory} from 'react-router';
    import * as userInfoActions from '../action/userInfoActions';
     class User extends React.Component {
        constructor(props) {
            super(props);
            this.state={ischecking:true};
        }
    
        componentDidMount(){
            //判断有没有登陆,没有登陆直接跳转
            const userinfo=this.props.userinfo;
            //如果尚未登录
            if(!userinfo.username){
                hashHistory.push('/login');
            }
            //如果已经登录
            else{
                this.setState({ischecking:false});
            }
    
    
    
        }
        render(){
            return(
                this.state.ischecking ? 
    :
    ); } } // -------------------redux react 绑定-------------------- function mapStateToProps(state) { return { userinfo: state.userinfo } } //触发数据改变 function mapDispatchToProps(dispatch) { return { userInfoActions: bindActionCreators(userInfoActions, dispatch), } } export default connect( mapStateToProps, mapDispatchToProps )(User)
    • 这里的返回会有问题,退不出去,所以我们修改一下pageHeader
      使用pageHeader组件的时候传递一个backRouter。
     clickHandle() {
            const backRouter=this.props.backRouter;
            if(backRouter)
                hashHistory.push(backRouter);
            else {
                window.history.back();
            }
    
        }
    
    • 创建该用户的详细信息组件
      UserInfo


    import React from 'react';
    export default class User extends React.Component {
        constructor(props) {
            super(props);
        }
    
        render(){
            return(
                

    {this.props.username}

    {this.props.cityName}

    ); } }
    • 创建您的订单组件OrderList


    您的订单部分需要连接后台服务器获取数据,通过传递用户名作为参数获取其相应订单。
    在mock中添加相应json数据


    import React from 'react';
    import OrderListComponent from './orderListComponent';
    
    export default class OrderList extends React.Component {
        constructor(props) {
            super(props);
            this.state = {order: []}
    
        }
    
        componentDidMount() {
            const fetchOption = {method: 'GET'};
            fetch('/api/orderlist/' + this.props.username, fetchOption).then(response => response.json()).then(json => this.setState({order: json}));
    
        }
    
        render() {
            const order = this.state.order;
            const orderList = order.length ?
                order.map((item, index) => (
                    
                   ))
                : '加载中...';
            return (
                

    您的订单

    { orderList }
    ); } }
    • orderListComponent组件
    import React from 'react';
    import  './orderListComponent.css';
    export default class OrderListComponent extends React.Component{
        render(){
            const item=this.props.item;
            return(
                

    {`商户:${item.title}` }

    {`数量:${item.count}`}

    {`价格:${item.price}`}

    ); } }

    采用flex布局:

    .order{
        display:flex;
        justify-content: space-between;
        padding: 10px;
        align-items: center;
        border-bottom: solid 1px #ccc;
    }
    
    .order img{
        width:30%;
    }
    
    .order p{
        margin: 0.5em ;
    }
    
    .order button{
        width: 20%;
        background-color: rgb(233,32,61);
        color: white;
        font-size: 16px;
        padding-top: 1px;
        padding-bottom: 1px;
        border-radius: 10px;
    }
    

    9.评论功能

    • 从后台传递过来的每一项内容中是有一个commentState的,表示评论的状态(未评价0 已评价2),
      commentState===0显示评价按钮,commentState===2显示已评价按钮,commentState===1表示评价中不显示按钮
    • 当点击评价按钮时,设置commentState为1,显示评论框。
    • 点击取消,设置commentState为0,隐藏评论框。
    export default class OrderListComponent extends React.Component {
    
    
        constructor() {
            super();
            this.state = {commentState: '',commentValue:''};
        }
    
        componentDidMount() {
            //0未评价,1评价中,2已评价
            this.setState({commentState: this.props.item.commentState});
        }
    
    
        //显示评价框
        showComment() {
            //当未评价的时候,点击按钮的响应事件
            this.setState({commentState: 1});
        }
    
        //隐藏评价框
        hideComment(){
            this.setState({commentState:0});
        }
    
        //双向绑定评论内容
        commentText(e){
            this.setState({commentValue:e.target.value});
        }
    
     render() {
            const item = this.props.item;
            return (
                

    {`商户:${item.title}`}

    {`数量:${item.count}`}

    {`价格:${item.price}`}

    { this.state.commentState === 0 ? //未评价 : this.state.commentState === 1 ? //评价中 '' : //已评价 }
    { this.state.commentState === 1 ?