12

VUE高仿饿了么app

VUE 搭建简介

刚学习了VUE高仿饿了么app课,记录课的要点,巩固知识。
图片描述图片描述图片描述

图片描述图片描述图片描述

VUE 优势

Vue.js 是一个用于创建 web 交互界面的。其特点是

简洁 HTML 模板 + JSON 数据,再创建一个 Vue 实例,就这么简单。
数据驱动 自动追踪依赖的模板表达式和计算属性。
组件化 用解耦、可复用的组件来构造界面。
轻量 ~24kb min+gzip,无依赖。
快速 精确有效的异步批量 DOM 更新。
模块友好 通过 NPM 或 Bower 安装,无缝融入你的工作流。

VUE 搭建工具

借用express + data 构建拟后台

vue 1.0
express
vue.router
vue.rescrouse
better-scroll
less

CSS 使用的要点

1像素边框制作

设备上像素 = 样式像素 * 设备缩放比例

屏幕宽度 320px 480px 640px
缩放比例   1    1.5    2

当样式像素一定时,因手机有320px,640px等.各自的缩放比差异,所以设备显示像素就会有1Npx,2Npx.为保设计稿还原度,解决就是用media + scale.

.border(@borderColor){
    position: relative;

    &::after{
        content: "";
        position: absolute;
        bottom: 0;
        left: 0;
        width: 100%;
        border-top: 1px solid @borderColor;
    }
}

@media (min-device-pixel-ratio: 1.5) {
    .border{

        &::after{
            transform: scaleY(0.7);
        }
    }
}

@media (min-device-pixel-ratio: 2) {
    .border{

        &::after{
            transform: scaleY(0.5);
        }
    }
}

通过查询它的缩放比,在媒体宽为1.5倍时, round(1px 1.5 / 0.7) = 1px 在媒体宽为2倍时, round(1px 2 / 0.5) = 1px.

自适应宽

图片描述

在商品路由中,导航宽度固定80px的,因为手机分辨率大小不一,所以食物详情自适应.解决就是flex布局.


css

<style type="text/less">
    .food{
        display: flex;
        width: 100%;

        .nav{
            flex: 0 0 80px;
            width: 80px;
        }

        .foodList{
            flex: 1;
        }
    }
</style>

html

<div class="food">
    <section class="nav"></section>
    <section class="foodList"></section>
</div>

在父元素设弹性布局,导航里设弹性为0,定宽为80px.商品食物详情弹性为1.就适应宽度变化.

Sticky footer

图片描述

做商家弹出页时,信息高度是无法预定的,有可能溢出window高度,也可能少于window高度,但底部按钮,当信息高度少于window高度,要固定在底部40px.解决就是用sticky footer布局


css

<style type="text/less">
    .showDetil{
        position: absolute;
        width: 100%;
        height: 100%;

        .sellerDetil{
            width: 100%;
            min-height: 100%;
            padding-bottom: 40px;
        }

        .btn{
            position: relative;
            top: -40px;
            height: 40px;
        }
    }
</style>

html

<div class="showDetil">
    <section class="sellerDetil"></section>
    <section class="btn"></section>
</div>

父元素高相同window高,信息最小高就相同window高,按钮这时就溢出了.
再设置底的填充,底内边距高就是按钮的高. 按钮在用相对定位,定在信息的底填充里.
因信息最少高度是100%,所按钮要不钉在底部了.要不溢出.

自适相等宽高

图片描述

在食物弹出页.设计图食物图的宽高是相等,每张图的宽高比例有可能有区别,但也要做自适应.解决就是用padding边距.


css

<style type="text/less">
    .imgs{
        width: 100%;
        height: 0;
        position: relative;
        padding-top: 100%;

        .image{
            position: absolute;
            top: 0;
            width: 100%;
            height: 100%;
            left: 0;
        }
    }
</style>

html

<div class="imgs">
    <img src="..." class="image">
</div>

在父元素,边距的长是取决去宽的,所其宽度与边距的长是相等的.
在把高设为0,宽为100%,上边距100%,上边据就盒子的高.盒子是为正形.
子元素设宽与高为100%,那也是正形.

VUE要点

小图标的编选

图片描述

根据后台输出的数据,判定显示那个的图标.这vue典型的数据.驱动.解决是使用:class困绑数据


html

<template>
    <ul>
        <li v-for="date in goods">
            <span :class="classmap[date.type]"></span>
        </li>
    </ul>
</template>

js

<script type="text/javascript">
    export default{
        data() {
            return {
                classmap: ['decrease', 'discount', 'guarantee', 'invoice', 'special']
            };
        }
    }
</script>

css

<style type="text/less">
.bgimg(@imgs) {
    background-image: url('@imgs+".png"') 0 0 no-repeat ~'/' 100% 100%;
}
    .decrease{
        display: inline-block;
        height: 12px;
        width: 12px;
    
       .bgimg('decrease_3');
    }

     .discount{
        display: inline-block;
        height: 12px;
        width: 12px;
    
        .bgimg('discount_3');
    }

    .guarantee{
        display: inline-block;
        height: 12px;
        width: 12px;
    
        .bgimg('guarantee_3');
    }

    .invoice{
        display: inline-block;
        height: 12px;
        width: 12px;
    
        .bgimg('invoice_3');
    }

    .special{
        display: inline-block;
        height: 12px;
        width: 12px;
    
        .bgimg('special_3');
    }
</style>

通过v-for,遍历数据,所以date.type得到数据并判断类型.然后通classmap数组判定绑那个class.来加图标.

小球动画

图片描述

点击加食物时,触动小球弹出的动画,小球的落点是在车的中央.但起点是根各个节点位子而又差别的.解决使用transitions + events + dispatch事件冒泡

cartcontrol子组件

图片描述


html

<template>
    <div class="cartcontrol">
        <section class="cart-decrease" @click.stop.prevent="decreaseCart" v-show="food.count > 0" transition="move"></section>  
        <section class="cart-count" v-show="food.count > 0">{{food.count}}</section>
        <section class="cart-add" @click.stop.prevent="addCart"> </section>
    </div>
</template>

js

<script type="text/javascript">
export default {
    props: {
        food: {
            type: Object
        }
    },
    methods: {
        addCart(event) {
            if (!this.food.count) {
                Vue.set(this.food, 'count', 1);
                this.food.count = 1;
            } else {
                this.food.count++;
            };
            this.$dispatch('cart.add', event.target);
        },
        decreaseCart() {
            if (this.food.count) {
                this.food.count--;
            };
        }
    }
};
</script>

在加食物,触发了addCart事件,设用set方法给数据加属性,并使cart.add事件冒泡出去,event.target作为事件参数,即节点冒泡出去.

goods父组件


html

<template>
    <shop v-ref:shop :delivery-price="seller.deliveryPrice" :min-price="seller.minPrice" :select-foods="selectFoods"></shop>
</template>

js

<script>
    export default {
        methods: {
            _drop(target) {
                this.$refs.shop.drop(target);
            }
        },
        events: {
            'cart.add'(target) {
                this._drop(target);
            }
        },
        components: {
            shop,
            cartcontrol,
            food
        }
    };
</script>

在冒泡被events钩子监听,与触动_drop方法,通过接口获得购物车组建的事件,就把control组建event.target传入购物车组建的事件,及把control节点传入了shop组建.

shop组建


html

<template>
    <div class="shopcart">
        <section class="ball-container">
            <div transition="drop" v-for="ball in balls" v-show="ball.show" class="ball">
                <div class="inner inner-hook"></div>
            </div>
        </section>
    </div>
</template>

js

<script type="text/javascript">
    export default {
        data() {
            return {
                balls: [
                {
                    show: false
                },
                {
                    show: false
                },
                {
                    show: false
                },
                {
                    show: false
                },
                {
                    show: false
                }],
                dropBalls: [],
                fold: true
            };
        },
        methods: {
            drop(el) {
                for (var i = 0; i < this.balls.length; i++) {
                    let ball = this.balls[i];
                    if (!ball.show) {
                        ball.show = true;
                        ball.el = el;
                        this.dropBalls.push(ball);
                        return;
                    };
                };
            }
        },
        transitions: {
            drop: {
                beforeEnter(el) {
                    let count = this.balls.length;
                    while (count--) {
                        let ball = this.balls[count];
                        if (ball.show) {
                            let rect = ball.el.getBoundingClientRect();
                            let x = rect.left - 32;
                            let y = -(window.innerHeight - rect.top - 22);
                            el.style.display = '';
                            el.style.transform = `translate3d(0,${y}px,0)`;
                            let inner = el.getElementsByClassName('inner-hook')[0];
                            inner.style.transform = `translate3d(${x}px,0,0)`;
                        }
                    }
                },
                enter(el) {
                    let rf = el.offsetHeight;
                    this.$nextTick(() => {
                        el.style.transform = 'translate3d(0,0,0)';
                        let inner = el.getElementsByClassName('inner-hook')[0];
                        inner.style.transform = 'translate3d(0,0,0)';
                    });
                },
                afterEnter(el) {
                    let ball = this.dropBalls.shift();
                    if (ball) {
                        ball.show = false;
                        el.style.display = 'none';
                    };
                }
            }
        },
        components: {
            cartcontrol
        }
    };
</script>

传入节点数据,过渡执行前可插入一个beforeEnter事件,通getBoundingClientRect定位.动画执行后可插入一个afterEnter,还原小球

接后台数据

与后台的配合,通过插vue.resource + express 连接得到数据

dev-server

<script type="text/javascript">
    import express from 'express';
    var app = express();

    var appData = require('../data.json');
    var seller = appData.seller;
    var goods = appData.goods;
    var ratings = appData.ratings;
    
    var apiRoutes = express.Router();
    
    apiRoutes.get('/seller', function (req, res) {
        res.json({
            errno: 0,
            data: seller
        });
    });
    
    apiRoutes.get('/goods', function (req, res) {
        res.json({
            errno: 0,
            data: goods
        });
    });
    
    apiRoutes.get('/ratings', function (req, res) {
        res.json({
            errno: 0,
            data: ratings
        });
    });
    app.use('/api', apiRoutes);
</script>

通过与配和框架express,连到数据。并放在api里.

main.js

import VueResource from 'vue-resource';
Vue.use('VueResource');

引进插件和使用,在全局也可以使用.

header组建

<script type="text/javascript">
    export default{
        created() {
            this.$http.get('/api/ratings').then((response) => {
                var response = response.body;
                if (response.errno === 0) {
                    this.ratings = response.data;
                };
            });
        }
    }
</script>

在框架的钩子,及创建就通过http.get连到express发的数据,通参数response得到.body表示数据以json格式响应.注意接收数据是异步实现,如果出报错undefined,可用v-if判断,当获取数据后在渲染.

评分类换

图片描述

用户的满意度有,推荐与吐槽再加上全部,就三个分页,分页通过按钮切换.如何制作呢?解决是使用v-show进判断.

ratingselect子组件

图片描述


html

<template>
    <div class="ratingselect">
        <div class="rating-type">
            <span @click="select(2, $event)" class="block positive" :class="{'active':selectType === 2}">{{desc.all}}<span class="count">{{ratings.length}}</span></span>
            <span  @click="select(0, $event)" class="block positive" :class="{'active':selectType === 0}">{{desc.positive}}<span class="count">{{positives.length}}</span></span>
            <span @click="select(1, $event)" class="block negative" :class="{'active':selectType === 1}">{{desc.negative}}<span class="count">{{negatives.length}}</span></span>
        </div>
        <div @click="toggleContent" class="switch" :class="{'on':onlyContent}">
            <span class="iconfont arre">&#xe905;</span>
            <span class="text">只看有内容的评价</span>
        </div>
    </div>
</template>

js

<script type="text/javascript">
export default{
    props: {
        ratings: {
            type: Array,
            default() {
                return [];
            }
        },
        selectType: {
        type: Number,
        default: 2
        },
        onlyContent: {
            type: Boolean,
            default: true
        },
        desc: {
            type: Object,
            default() {
                return {
                    all: '全部',
                    positive: '满意',
                    negative: '不满意'
                };
            }
        }
    },
    methods: {
        select(type) {
            this.selectType = type;
            this.$dispatch('ratingtype.select', type);
        },
        toggleContent() {
            this.onlyContent = !this.onlyContent;
            this.$dispatch('ratingtype.toggleContent', this.onlyContent);
        }
    }
};
</script>

满意是为:0,不满意是为:1,全部是为:2.

因在点击切换按钮,触发方法,通过传入参数来替换数据,数据selectType赋值等于参数.参数是自义定,然而可以在参数下功夫,然用冒泡将数据分出.

food父组件
html

<template>
    <transiton>
        <div class="rating">
            <h4 class="title">商品评价</h4>
            <ratingselect :select-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect>
            <div class="rating-wrapper">
              <ul v-show="food.ratings && food.ratings.length">
                <li v-for="rating in food.ratings" v-show="needShow(rating.rateType,rating.text)" class="rating-item">
                  <div class="user">
                    <span class="name">{{rating.username}}</span>
                    <img class="avatar" width="12" height="12" :src="rating.avatar">
                  </div>
                  <div class="time">{{rating.rateTime | formatDate}}</div>
                  <p class="text">
                    <span v-if="rating.rateType === 0" class="arrowUp iconfont">&#xe901;</span>
                    <span v-if="rating.rateType === 1" class="arrowDown iconfont">&#xe902;</span>{{rating.text}}
                  </p>
                </li>
              </ul>
              <div class="no-rating" v-show="!food.ratings || !food.ratings.length">暂无评价</div>
            </div>
        </div>
    </transiton>
</template>

js

<script type="text/javascript">
    import Vue from 'vue';

    import ratingselect from 'components/ratings/ratingselect';

    const POSITIVE = 0;
    const NEGATIVE = 1;
    const ALL = 2;
  
   export default {
        data() {
            return {
                showFlage: false,
                selectType: ALL,
                onlyContent: true,
                desc: {
                    all: '全部',
                    positive: '推荐',
                    negative: '吐槽'
                }
            };
        },
        methods: {
            needShow(type, text) {
                if (this.onlyContent && !text) {
                return false;
                }
                if (this.selectType === ALL) {
                    return true;
                } else {
                    return type === this.selectType;
                }
            }
        },
        events: {
            'ratingtype.select'(type) {
                this.selectType = type;
                this.$nextTick(() => {
                    this.scroll.refresh();
                });
            },
            'ratingtype.toggleContent'(onlyContent) {
                this.onlyContent = onlyContent;
                this.$nextTick(() => {
                    this.scroll.refresh();
                });
            }
        },
        components: {
            ratingselect
        }
    };
</script>

在事件钩子上,实行监听,把冒泡触发并赋值,数据就得到.在遍历数据,用v-show进行判断.

VUE杂项

过渡流程

只在v-if,v-show,v-for触动节点的变动效果

当 show 属性改变时,Vue.js 将相应地插入或删除元素,按照如下规则改变过渡的 CSS 类名:

如果 show 变为 false,Vue.js 将:

调用 beforeLeave 钩子;
添加 v-leave 类名到元素上以触发过渡;
调用 leave 钩子;
等待过渡结束(监听 transitionend 事件);
从 DOM 中删除元素并删除 v-leave 类名;
调用 afterLeave 钩子。
如果 show 变为 true,Vue.js 将:

调用 beforeEnter 钩子;
添加 v-enter 类名到元素上;
把它插入 DOM;
调用 enter 钩子;
强制一次 CSS 布局,让 v-enter 确实生效。然后删除 v-enter 类名,以触发过渡,回到元素的原始状态;
等待过渡结束;
调用 afterEnter 钩子。

better-scroll

节点溢满时,是设计稿没有滚动条的,要上下移动.解决使用better-scroll插件.


html

<div class="sellerx" v-el:seller style="overflow: hidden;">
    <div class="seller-content"></div>
</div>

js

<script type="text/javascript">
    ready() {
         this.$nextTick(() => {
             if (!this.scroll) {
                 this.scroll = new Bscroll(this.$els.seller, {
                    click: true
                });
            } else {
                this.scroll.refresh();
            }
        });
    },
</script>

但父元素设置溢出隐藏,可用插件的移动显出子节点超的内容.要在节点放个接口,使用框架钩子,创建betterScroll事例,那藏的内容通立体相上下移.better-scroll是调用样式的translate是子节点上下引动.

less样式处理

通过引入样式,有是会错误.解决使用设置标签

<style type="text/less"></style>

处理器会识别到标签的样式类别,编译样式.

esLint

在使用eslint语法校验时,经常报错,但可以在eslintrc设置进行忽略.

no mixed spaces and tabs

是把标签缩进与空格捆和使用,解决是可用tab代替空格.

Expected indentation of 2 space characters but found 3 indent

'indent': 0,
'space-before-function-paren': 0
设置缩进空行.

defined but never use

可在前加注销
/ eslint-disable no-unused-vars /

后序

要灵活的用vue,先要处理好数据的逻辑.
然而要懂得基本的数据传递属性.

子组件传给父组件-
可以用接口ref;也可以子组件的冒泡把数据传去,父组件用钩子events监听并接到数据.
父组件传给子组件-
可以在子组件props钩子,接收父组件的传递.也可以父组件用ref接口调用子组件的方法,并把数据传入方法去.

实战是最重要.


lea_
253 声望19 粉丝

在有限时间,做最好自己.