经典项目Vue饿了么外卖复盘

由于最近离职,趁着一段空白期,对过去做个一个比较详情的总结,也开始尝试写一写不一样的行业的项目,于是就开始在网上找了一些项目进行练手,做完之后其实收获还是挺多的,做个简单的心得分享和对项目的复盘,顺便复盘的时候还对项目整体做了一些优化,毕竟自己屎一样的代码真的难看,只能尽力做到让他看起来不那么像屎....


image.png

Vue饿了么项目网上流传的资料是1.0的版本,所以是完全照着设计稿写的,单纯做分享

先简单的来看下设计稿~


image.png

image.png

image.png

总览

商品首页,商家展示页,购物车详情,商品详情,评论页面,商家介绍页面~

整体来说有难度的点主要在于:
1,购物车功能
2.不同尺寸图标的组件业务逻辑需要进行适配
3.better-scroll的理解及运用

首先是通过Vue-cli搭建Vue项目环境,以及配置商品,评价,商家三个动态路由~

然后是一个比较重要的本地服务器搭建,因为数据是通过node服务器读取的,所以需要自己手动搭建一个node服务器,也比较简单

var express = require('express');
var app = express();
var cors = require('cors');
 
app.use(cors());
var appData = require('./data.json');

var seller = appData.seller;
var goods = appData.goods;
var ratings = appData.ratings;

app.get('/seller',function(req,res){
    res.json({
        code:202,
        data:seller
    })
});

app.get('/goods',function(req,res){
    res.json({
        code:202,
        data:goods
    })
});

app.get('/ratings',function(req,res){
    res.json({
        code:202,
        data:ratings
    })
});

app.listen(3000,function () {
    console.log('runing');
});

数据搞定~

1.按照顺序那就先来看看首页~

image.png

首页主要有的组件:
1.头部
2.底部购物车
3.商品路由下对应的商品组件

外加一个动态路由,头部不变,主要通过动态路由控制路由视图切换

<template>
    <div id="app">
        <headers>我是头部</headers>
        <div class="topnav">
            <router-link to="/shopping" tag="div">商品</router-link>
            <router-link to="/evaluate" tag="div">评价</router-link>
            <router-link to="/merchant" tag="div">商家</router-link>
        </div>
        <router-view></router-view>
    </div>
</template>

头部组件

image.png

1.头部组件的布局,读取数据后进行相应的渲染
2.头部蒙层详情

-主要难点是在于如何根据后台返回的数据,渲染对应的蒙层icon图标
-如何根据评分渲染对应的星星评分

返回的json数据


image.png

后台根据不同的icon图标给了一个type字段,评分的话给了一个Number字段,表示当前的评分

icon图标怎么解决:

1.提前写好样式,把对应的icon类名提前声明在一个数组里,class类根据类名显示对应样式
2.supports[index]拿到的是当前index对应的type字段,通过点语法拿type字段对应的值
3.数组[下标] 拿到类名,最终就是需要展示的类名

 <span class="icon-map"  :class="classMap[datas.supports[index].type]"></span>

//提前写好的类名放在数组
  data() {
    return {
     classMap:['subtract', 'discount', 'reduced' , 'ticket' ,'safeguard']
    };
//提前写好样式(展示部分)
// 減
  .subtract{
    background-image: url('../merchant/decrease_2@2x.png');
  }
  //折扣
  .discount{
    background-image: url('../merchant/discount_2@2x.png');
  }
  //特惠
  .reduced{
    background-image: url('../merchant/special_2@2x.png');
  }

根据评分显示对应的评分样式

需求:根据设置的尺寸大小,给定的星星分值渲染对应的星星
思路:
1.设置一个类为满星星
2.设置一个类为空星星
3.设置一个类为半星

根据类名进行星星的拼凑,星星需要接受父组件传来的两个参数,一个是星星的尺寸大小,一个是星星对应的Number字段

 <div class="star" :class="sizes">
        <span v-for="(item,index) in arrs" :key="index" :class="item" class="item-star"></span>
    </div>

    const num = 5;
    //滿星
    const half = "half";
    //半星
    const on = "on";
    //沒星
    const off = "off";

    export default {
        data() {
            return {
                num: 5
            };
        },
        // 接收外部组件传过来的参数
        props: ["size", "score"],
        
        //计算属性
        computed: {
            //返回一个传进来的字符串与star进行拼接 star-24 star-36
            sizes() {
                return "star-" + this.size;
            },
            //返回一个数组,用来控制星星的生成,最终显示(item-star on) (item-star off) (item-star half),分别对应满星,无星,半星,数组排列的顺序,先是满星,然后是半星,最后是无星
            arrs() {
                let arr = [];
                //全部的星星等于:整数星星加上半整星星加上空星星
                // 把发过来的分数做处理,取0.5的倍数
                let score = Math.floor(this.score * 2) / 2;
                // 如果这个分数除以1取余不等于0,说明不是整数
                let has = score % 1 !== 0;
                //对发过来的数值做向下取整
                let inte = Math.floor(score);
                //给所有整数星星加上on这个类
                for (var i = 0; i < inte; i++) {
                    arr.push(on);
                }
                //给所有半数星星加上half
                if (has) {
                    arr.push(half);
                }
                //如果星星还没加满5个就给后面的全部补上 half
                for (var i = 0; i < this.num; i++) {
                    if (arr.length < this.num) {
                        arr.push(off);
                    }
                }
                return arr;
            }
        }
    };
</script>

类名尺寸提前写好

image.png

头部差不多完成,接下来商品组件

商品组件的难点是在于左右联动,因为有了srcoll的加入,所以第一次做的时候会觉得有些难度,我是用的是better-scroll,scroll这里文档说的比较详情,我就不多说啦~

需求:
1.当左边侧栏滚动,右边商品对应到当前的商品类
2.当左边侧栏点击,右边商品对应到当前的商品类
3.当右边商品滚动,左边对应相应的商品类目,并高亮显示

思路(左边,右边index值对应):
1.声明高亮类名,判断当前栏目index是否等于计算属性传过来的值,为true,高亮的类名就会被添加上

<li
        v-for="(item,index) in arr"
        :key="index"
        :class="{'current': index == indexsA}"
        @click="getindex(index)">
         <p>
               <span v-if="item.type > 0" :class="classMap[item.type]" class="icons"></span>
               {{item.name}}
         </p>
 </li>

2.把右边的商品栏目的高度存入一个数组中,当左边的商品滚动的时候,通过循环去比较高度最后通过计算属性最终返回一个当前的index值(高度的初始化需要在Dom渲染完之后updated钩子函数)

//在Dom元素渲染后才能拿到高度(updated里面,当然mounted也可以)
 indexHeight() {
                //定义出一个li高度的数组
                if (this.off) {
                    let hei = 0;
                    this.listheight.push(hei);
                    for (var i = 0; i < this.$refs.liheight.length; i++) {
                        let itme = this.$refs.liheight[i];
                        hei += itme.offsetHeight;
                        this.listheight.push(hei);
                    }
                    this.off = false;
                }
            },
//updated钩子函数中获取实时的y轴位置
 this.rightScroll.on("scroll", (pop) => {
                //1.取绝对值
                //2.取整
                this.scrolly = Math.abs(Math.round(pop.y));
            });

          //左右联动,右边的数组高度判断,修改当前的index值,这个是在计算属性computed中
            indexsA() {
                // 循环遍历有高度的那个数组
                for (let i = 0; i < this.listheight.length; i++) {
                    //第一个高度
                    let hei1 = this.listheight[i];
                    //第二个高度
                    let hei2 = this.listheight[i + 1];
                    //如果是在第一个和第二个之间,说明正触发这个索引
                    //如果遍历到最后一个下标,那i+1就会报错,所以要加上!h2
                    if (!hei2 || this.scrolly >= hei1 && this.scrolly < hei2) {
                        return i;
                    }
                }
                return 0;
            },

3.当用户点击左边的侧栏,通过better-scroll的Api实现

// 左右联动,左边index值
            getindex(index) {
                //获取右边滚动栏的元素
                let foodList = this.$refs.liheight;
                //通过index获取当前要滚动要的目标元素
                let el = foodList[index];
                this.rightScroll.scrollToElement(el, 300);
            },

也可以稍微提一下关于watch和computed计算属性的一个区别

watch监听

watch是用来监听data里面的数据变化,也可以监听router路由,当监听的数据发生了变化,就会执行相应的监听函数

computed计算属性

computed是计算属性,每当computed里面依赖的data属性发生了变化,就会执行,最后返回一个值

总结:左右联动的话,主要的代码就是这一些了~主要要注意的点:
1.在什么生命钩子函数中执行相应的方法
2.如何让左右两边共享一个index值,最后渲染高亮元素

下面主要就聊一下小球动画还有购物车功能把,其他的话主要是一些样式和一些前面使用过的组件复用了

小球动画

image.png
  <div class="ball-centent">
            //列表需要用group包裹起来
            <transition-group
                    @befor-enter="beforeEnters"
                    @enter="enters"
                    @after-enter="afterEnters"
                    name="ball"
                    tag="div">
                <div v-for="(item,index) in dropball" :key="index" v-if="item.shows">
                    <div class="ball" v-if="balloff"></div>
                </div>
            </transition-group>
        </div>

        data() {
            return {
                //小球默认都是隐藏的,当用户点击一个,将数组中的小球显示
                ball: [
                    {shows: false},
                    {shows: false},
                    {shows: false},
                    {shows: false},
                    {shows: false},
                ],
                //存储显示的球,渲染的是这个数组
                dropball:[],
            }
        },
    
         //小球的逻辑
                for (let i = 0; i < this.ball.length -1; i++) {
                    //如果小球的状态是false
                    if (!this.ball[i].shows) {
                        //让小球的状态变成true
                        this.ball[i].shows = true;
                        //这是为turn的球
                        this.dropball.push(this.ball[i]);
                        return;
                    }
                }
        
//小球动画开始前如果元素刚开始是没有创建的话,那这个beforeEnters是不会执行的
            beforeEnters(el){
                el.style.transform = 'translate(0,0)'
            },
            enters(el,done){
                let rf = el.offsetWidth;
                    //当前点击元素的x,
                    let balls = this.$refs.right.getBoundingClientRect();
                    //让当前小球的位置跟点击的元素位置一致
                    el.style.left = balls.left+'px';
                    el.style.top = balls.top+'px';
                    // //终点位置
                    let end= document.getElementById('icons').getBoundingClientRect();
                    // //0所在位置
                    let x =  end.left - balls.left;
                    let y =  end.top - balls.top+50;
                    el.style.transform =`translate(${x}px,${y}px)`;
                    el.style.transition = 'all 0.7s linear';

                    //done();
            },
//触发done的回调后让小球隐藏即可,但是这个有个BUG,done触发不了后续的动画(很蒙~),于是改用定时器,
afterEnters(el){
                var _this = this;
                let timid = setTimeout(function () {
                    let ball = _this.dropball.shift();
                  //两个数组间的小球进行切换,把小球当前状态添加到false状态的小球中
                    _this.ball.push({
                        shows:false
                    });
                    if (ball) {
                        ball.show = false;
                        el.style.display = 'none';
                    }
                    clearInterval(timid);
                },700)

            },

购物车

image.png

购物车功能主要是通过Vuex来实现,Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,通俗点来理解他就是一个所有组件都可以使用的一个数据中转枢纽,他里面常用的包含
1.state,相当于全局组件都可以使用这个里面的数据
2.Mutation,相当于全局组件的methods,所有组件都可以使用他的方法
3.Getter,相当于全局组件的计算属性,每个组件都可以共享
4,Action,主要处理异步代码

安装完Vuex之后创建一个Vuex全局实例对象后就可以进行使用了,那就下来就是梳理需求
1.需要一个全局都可以共享的当前购物数据
2.通过当前的购物数据渲染不同的样式
3.不同事件对Vuex属性中的state数据进行增删

1.如何添加一个共享数据
设计购物车所需要的数据

this.obj是通过父组件传过来的当前点击的商品信息
let obj = {
              //名称
             name:this.obj.name,
              //id
              id: this.obj.__ob__.dep.id,
              //数量
              num: 1,
              //价格
              price: this.obj.price,
              //是否被选中
              isok: true
            };
//添加一个数据
this.$store.commit('addcar', obj);

vuex

 state: {
        car: [ ] ////将购物车中的数组,用一个数组存储起来
    },
mutations: {
 addcar(state, obj) {
            //如果没有被选中
            var off = false;
            //假设这个商品已经被选中过
            for (let i = 0; i < state.car.length; i++) {
                //在数组中通过id匹配到这个数组
                if (state.car[i].id == obj.id) {
                    //把当前数组的num值加上穿过来的新num值
                    state.car[i].num ++;
                    off = true;
                }
            }
            //如果这个商品没有被选中过,将这个商品加入数组中
            if (off == false) {
                state.car.push(obj)
            }
        },
}

2.如何减少一个共享数据

//减少的点击事件,把id传过去
 mins(id) {
                this.$store.commit("minNum", {
                    id: id,
                    num: this.$refs.numbox.value
                })
            },

Vuex

minNum(state, obj) {
            for (let i = 0; i < state.car.length; i++) {
                //在数组中通过id匹配到这个数组
                if (state.car[i].id == obj.id) {
                    //把当前数组的num值减1
                    if (state.car[i].num == 0){
                        state.car.splice(i,1);
                        return;
                    }else {
                        state.car[i].num --;
                    }
                }
            }
        },

3.如何调用Vuex的计算属性返回当前购物数据,进行渲染

image.png

这是一些需要用到的全局计算属性,不能一一展示,但是大致都是一样的,只要思路是正确的,都没什么问题
Vuex

 getters: {
        //根据传过来的id返回当前id对应数组的num值
        getnum(state){
            var obj = {};
            for (let i = 0; i < state.car.length; i++) {
                obj[state.car[i].id] = state.car[i].num;
            }
            return obj;
        },
        //计算所有num的和,还有总价
        getmax(state){
          var obj ={}
          var a =0;
          var str ='';
          var price = 0
            for (let i = 0; i < state.car.length; i++) {
                price += state.car[i].num * state.car[i].price;
                a += state.car[i].num;
            }
            obj.num = a;
            obj.price=price;
            if(obj.price<20){
                str = '还差¥'+(20-obj.price)+'元起送'
            }else {
                str='去结算'
            }
            obj.str =str;
            return obj;
        },
        // 样式控制的开关
        off(state){
            var obj = {};
            for (let i = 0; i < state.car.length; i++) {
                obj[state.car[i].id] = state.car[i].num;
            }
            return obj;
        },
        // 获取当前购物车中的数据
        cardata(state){
            return state.car;
        }
    }

大致购物车的主要代码就是这样了,只要渲染出一个样式,其实后面还是很好做的,代码还有很大的优化空间~ 优化中......

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容