【饿了么】—— Vue2.0高仿饿了么核心模块&移动端Web App项目爬坑(三)

时间:2022-03-04 15:33:01

前言:接着上一篇项目总结,这一篇是学习过程记录的最后一篇,这里会梳理:评论组件、商家组件、优化、打包、相关资料链接。项目github地址:https://github.com/66Web/ljq_eleme,欢迎Star。


【饿了么】—— Vue2.0高仿饿了么核心模块&移动端Web App项目爬坑(三) 【饿了么】—— Vue2.0高仿饿了么核心模块&移动端Web App项目爬坑(三)
ratings seller
一、评论组件-ratings

评论组件主要分为三块

  • 评分信息-overview
  • 评论选择-ratingselect
  • 评论详细信息

评分信息部分

  • 左侧评分
  1. 布局Dom
    <div class="ratings-content">
    <div class="overview">
    <div class="overview-left">
    <h1 class="score">{{seller.score}}</h1>
    <div class="title">综合评分</div>
    <div class="rank">高于周边商家{{seller.rankRate}}%</div>
    </div>
    <div class="overview-right">
    .....
    </div>
    </div>
    <split></split>
  2. CSS样式

    .overview
    display flex
    padding 18px 0 18px 18px
    .overview-left
    padding-bottom 6px 0
    flex 0 0 137px
    width 137px // 防止出现兼容性问题
    border-right 1px solid rgba(7,17,27,0.1)
    text-align center
    @media only screen and (max-width 320px)
    flex 0 0 110px
    width 110px
    .score
    margin-bottom 6px
    line-height 28px
    font-size 24px
    color rgb(255, 153, 0)
    .title
    margin-bottom 8px
    line-height 12px
    font-size 12px
    color rgb(7, 17, 27)
    .rank
    line-height 10px
    font-size 10px
    color rgb(147, 153, 159)
    .overview-right
    flex 1
    padding 6px 0 6px 24px
  3. seller数据:App.vue中的routerview进行传递,在rating组件中使用props进行接收,这样才可以在模板中直接使用seller.XXX数据
     props: {
    seller: {
    type: Object
    }
    }
  • 右侧star组件+商品评分+送达时间
  1. 布局Dom
    <div class="overview">
    <div class="overview-left">
    ...
    </div>
    <div class="overview-right">
    <div class="score-wrapper">
    <span class="title">服务态度</span>
    <star :size="36" :score="seller.serviceScore"></star>
    <span class="score">{{seller.serviceScore}}</span>
    </div>
    <div class="score-wrapper">
    <span class="title">商品评分</span>
    <star :size="36" :score="seller.foodScore"></star>
    <span class="score">{{seller.foodScore}}</span>
    </div>
    <div class="delivery-wrapper">
    <span class="title">送达时间</span>
    <span class="delivery">{{seller.deliveryTime}}分钟</span>
    </div>
    </div>
    </div>
  2. CSS样式:

            .overview-right
    flex 1
    padding 6px 0 6px 24px
    @media only screen and (max-width 320px)
    padding-left 6px
    .score-wrapper
    line-height 18px
    margin-top 8px
    font-size 0
    .title
    display inline-block
    vertical-align top
    line-height 18px
    font-size 12px
    color rgb(7, 17, 27)
    .star
    display inline-block
    vertical-align top
    margin 0 12px
    .score
    display inline-block
    vertical-align top
    line-height 18px
    font-size 12px
    color rgb(255, 153, 0)
    .delivery-wrapper
    font-size 0
    .title //span文字和文字之间默认是垂直居中的,可以不用加display vertical-align
    display inline-block
    vertical-align top
    line-height 18px
    font-size 12px
    color rgb(7, 17, 27)
    .delivery
    display inline-block
    margin-left 12px
    vertical-align top
    line-height 18px
    font-size 12px
    color rgb(147, 153, 159)
  3. 坑:视口宽度不够宽时,右侧部分过长会出现折行。解决:添加一个mediea Query媒体查询

    .overview-left
    padding-bottom: 6px 0
    flex: 0 0 137px
    width: 137px // 防止出现兼容性问题
    border-right: 1px solid rgba(7,17,27,0.1)
    text-align: center
    @media only screen and (max-width 320px)
    flex: 0 0 110px
    width: 110px
    .overview-right
    flex 1
    padding: 6px 0 6px 24px
    @media only screen and (max-width 320px)
    padding-left: 6px
  • 页面很长,需要引用better-scroll
  1. 同时,已经做好的分割区split组件、星星star组件、评论选择ratingselect组件、时间戳转换等也都需要引用
    import star from '@/components/star/star'
    import BScroll from 'better-scroll';
    import split from '@/components/split/split'
    import ratingselect from '@/components/ratingselect/ratingselect'
    import {formatDate} from '@/common/js/date'
    <template>
    <div class="ratings" ref="ratings"> <!-- ratings-content大于ratings的时候出现滚动 -->
    <div class="ratings-content">
  2. 要实现滚动,像good组件一样,需要固定视口的高度,将其定位绝对定位,top为header组件的高度

    .ratings
    position: absolute
    top: 174px
    bottom: 0
    left: 0
    width: 100%
    overflow: hidden

       评论选择部分 

  • 使用引用并注册好的split组件和ratingselect组件
    <split></split>
    <ratingselect @increment="incrementTotal"
    :select-type="selectType"
    :only-content="onlyContent"
    :ratings="ratings">
    </ratingselect>

评论详细信息

  • 同商品组件,在created()函数中拿到ratings的API数据,将得到的ratings传到ratings的组件中
    const ERR_OK = 0;
    created () {
    this.$http.get('/api/ratings')
    .then((res) => {
    res = res.body;
    if (res.errno === ERR_OK) {
    this.ratings = res.data;
    // console.log(this.ratings)
    this.$nextTick(() => {
    this.scroll = new BScroll(this.$refs.ratings, {
    click: true
    })
    });
    }
    }
    )
  • 拿到数据之后在raring组件中填充html中的DOM数据

    <div class="rating-wrapper">
    <ul>
    <li v-for="rating in ratings" :key="rating.id" class="rating-item" v-show="needShow(rating.rateType, rating.text)">
    <div class="avatar">
    <img :src="rating.avatar" width="28px" height="28px">
    </div>
    <div class="content">
    <h1 class="name">{{rating.username}}</h1>
    <div class="star-wrapper">
    <star :size="24" :score="rating.score"></star>
    <span class="delivery" v-show="rating.deliveryTime">
    {{rating.deliveryTime}}
    </span>
    </div>
    <p class="text">{{rating.text}}</p>
    <div class="recommend" v-show="rating.recommend && rating.recommend.length"> <!-- 赞或踩和相关推荐 -->
    <i class="icon-thumb_up"></i>
    <span class="item" v-for="item in rating.recommend" :key="item.id">{{item}}</span>
    </div>
    <div class="time">
    {{rating.rateTime | formatDate}}
    </div>
    </div>
    </li>
    </ul>
    </div>
  • CSS样式

          .rating-wrapper
    padding 0 18px
    .rating-item
    display flex
    padding 18px 0
    border-1px(rgba(1, 17, 27, 0.1))
    .avatar
    flex 0 0 28px
    width 28px
    margin-right 12px
    img
    border-radius 50%
    .content
    position relative
    flex 1
    .name
    margin-bottom 4px
    line-height 12px
    font-weight 700
    font-size 10px
    color rgb(7, 17, 27)
    .star-wrapper
    margin-bottom 6px
    font-size 0
    .star
    display inline-block
    margin-right 16px
    vertical-align top
    .delivery
    display inline-block
    vertical-align top
    font-size 10px
    line-height 12px
    color rgb(147, 153, 159)
    .text
    line-height 18px
    color rgb(7, 17, 27)
    font-size 12px
    margin-bottom 8px
    .recommend
    line-height 16px
    font-size 0
    .icon-thumb_up, .item
    display inline-block
    margin 0 8px 4px 0
    font-size 9px
    .icon-thumb_up
    color rgb(0, 160, 220)
    .item
    padding 0 6px
    border 1px solid rgba(7, 17, 27, 0.1)
    border-radius 1px
    color rgb(147, 153, 159)
    background #fffff
    .time
    position absolute
    top 0
    right 0
    line-height 12px
    font-size 10px
    color rgb(147, 153, 159)
  • 绑定better-scroll,使评论列表部分可以滚动

  1. 拿到DOM数据,ref="ratings",将better-scroll初始化时机写在created函数拿到api数据之后
二、商家组件-seller

基础操作

  • 接收传递进来的seller数据
     props: {
    //APP.vue的routerview中已经将seller传进来了,这里只需要接收就好
    seller: {
    type: Object
    }
    }
  • 布局DOM

    <div class="overview">
    <h1 class="title">{{seller.name}}</h1>
    <div class="desc border-1px">
    <star :size="36" :score="seller.score"></star>
    <span class="text">({{seller.ratingCount}})</span>
    <span class="text">月售{{seller.sellCount}}单</span>
    </div>
    <ul class="remark">
    <li class="block">
    <h2>起送价</h2>
    <div class="content">
    <span class="stress">{{seller.minPrice}}</span>元
    </div>
    </li>
    <li class="block">
    <h2>商家配送</h2>
    <div class="content">
    <span class="stress">{{seller.deliveryPrice}}</span>元
    </div>
    </li>
    <li class="block">
    <h2>平均配送时间</h2>
    <div class="content">
    <span class="stress">{{seller.deliveryTime}}</span>元
    </div>
    </li>
    </ul>
    <div class="favorite" @click="toggleFavorite($event)">
    <i class="icon-favorite"
    :class="{'active':favorite}"></i> <!-- 对应是否收藏两种样式-->
    <span>{{favoriteText}}</span> <!-- 有没有选中对应不同的文本,所以这里要绑定一个变量,放到data中 -->
    </div>
    </div>
  • CSS样式

    .seller
    position: absolute
    top: 174px
    bottom: 0
    left: 0
    width: 100%
    overflow: hidden
    .overview
    padding: 18px
    position: relative
    .title
    margin-bottom: 8px
    line-height: 14px
    color: rgb(7, 17, 27)
    font-size: 14px
    .desc
    padding-bottom: 18px
    font-size: 0
    border-1px(rgba(7, 17, 27, 0.1))
    &:before
    display: none
    .star
    display: inline-block
    vertical-align: top
    margin-right: 8px
    .text
    display: inline-block
    vertical-align: top
    margin-right: 12px
    line-height: 18px // 不能为父元素设置line-heigth,否则组件会被撑高
    font-size: 10px
    color: rgb(77, 85, 93)
    .remark
    display: flex
    padding-top: 18px
    .block
    flex: 1
    text-align: center
    border-right: 1px solid rgba(7, 17, 27, 0.1)
    &:last-child
    border: none
    h2
    margin-bottom: 4px
    line-height: 10px
    font-size: 10px
    color: rgb(147, 153, 149)
    .content
    line-height: 24px
    font-size: 10px
    color: rgb(7, 17, 27)
    .stress
    font-size: 24px

       公告与活动部分

  • 先添加一个split组件,再添加内容,同时不要忘记把图片拷贝过来
  1. 布局DOM
    <div class="bulletin">
    <h1 class="title">公告与活动</h1>
    <div class="content-wrapper border-1px">
    <p class="content">{{seller.bulletin}}</p>
    </div>
    <ul v-if="seller.supports" class="supports">
    <li class="support-item border-1px"
    v-for="(item,index) in seller.supports"
    :key="(item.id,index.id)">
    <span class="icon" :class="classMap[seller.supports[index].type]"></span>
    <span class="text">{{seller.supports[index].description}}</span>
    </li>
    </ul>
    </div>
    <split></split>

    其中:图标icon  动态绑定class时,使用classMap,在created()中定义,通过获取索引值一一对应,同header.vue组件中一样

     created() {
    this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee'];
    }
  2. CSS样式
    .bulletin
    padding: 18px 18px 0 18px
    .title
    margin-bottom: 8px
    line-height: 14px
    color: rgb(7, 17, 27)
    font-size: 14px
    .content-wrapper
    padding: 0 12px 16px 1px
    border-1px(rgba(7, 17, 27, 0.1))
    .content
    line-height: 24px
    font-size: 12px
    color: rgb(240, 20, 20)
    .supports
    .support-item
    padding: 16px 12px
    border-1px(rgba(7, 17, 27, 0.1))
    font-size 0
    &:last-child
    border-none()
    .icon
    display inline-block
    width 16px
    height 16px
    vertical-align top
    margin-right 6px
    background-size 16px 16px
    background-repeat no-repeat
    &.decrease
    bg-image('decrease_4')
    &.discount
    bg-image('discount_4')
    &.guarantee
    bg-image('guarantee_4')
    &.invoice
    bg-image('invoice_4')
    &.special
    bg-image('special_4')
    .text
    display inline-block
    font-size 12px
    line-height 16px
    color rgb(7, 17, 27)

使用BScroll

  • 页面很长,需要引用BScroll
  1. 坑:初始化BScroll语句放在created()中,但是不起作用。
  2. 原因:seller是异步获取的,但是我们的内容都是靠seller里的数据撑开的,所以一开始内容肯定是小于我我们定义的wrapper的,所以没有被撑开
  3. 解决:将其放入watch:{} 中可以监测到seller的变化,将初始化语句写成一个方法,在watch中进行调用
     methods: {
    _initScroll() {
    this.$nextTick(() => {
    if (!this.scroll) {
    this.scroll = new BScroll(this.$refs.seller, {click: true});
    }else{
    this.scroll.refresh();
    }
    })
    }
     watch: {
    'seller'() { //观测seller数据的更新,并且执行更新后的操作
    this._initScroll();
    this._initPics();
    }
    },
    created() {
    this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; this._initScroll();
    this._initPics();
    }
  4. 坑:之前的情况是切换之后不能滚动,现在的新问题是一开始(没切换界面之前)就不能滚动了,切换之后就可以滚动了;
  5. 原因:created()的执行时机要先于watch中的seller,然后我们在执行seller中的initScroll的时候就会发现BScroll已经被初始化了,所以initScroll失效,即使在watch中观察到变化也只能什么都不做

  6. 解决:一定要为初始化函数_initScroll()和this._initPics()中的nextTick()下的添加if-else语句,对BScroll进行刷新,完成

  • 商家实景区块 -- 横向滚动
  1. 添加图片,设置样式,横向排列
    <div class="pics">
    <h1 class="title">商家实景</h1>
    <div class="pic-wrapper" ref="picWrapper">
    <ul class="pic-list" ref="picList">
    <li class="pic-item" v-for="pic in seller.pics" :key="pic.id">
    <img :src="pic" width="120" height="90">
    </li>
    </ul>
    </div>
    </div>
    <split></split>

    CSS样式:

     .pics
    padding: 18px
    .title
    margin-bottom: 12px
    line-height: 14px
    color: rgb(7, 17, 27)
    font-size: 14px
    .pic-wrapper
    width: 100%
    overflow: hidden
    white-space: nowrap /*不产生折行*/
    .pic-list
    font-size: 0
    .pic-item
    display: inline-block
    margin-right: 6px
    width: 120px
    height: 90px
    &:last-child
    margin: 0
  2. 原理: pic-wrapper是固定宽度的视口的大小,当里面的ul超过视口宽度的时候就会出现滚动
  3. 注意:ul是外层的宽度,并不是真实的li撑开的宽度
  4. 实现:使用BScroll实现滚动,添加_initPic()方法,并把它添加到watch和create()中
    _initPics() {
    if(this.seller.pics) {
    let picWidth = 120;
    let margin = 6;
    let width = (picWidth + margin)*this.seller.pics.length - margin;//计算ul的宽度
    this.$nextTick(() => {
    this.$refs.picList.style.width = width + 'px';//设置ul宽度,不要忘记单位
    if (!this.picScroll) {
    this.picScroll = new BScroll(this.$refs.picWrapper, {
    scrollX: true,//表示横向滚动
    eventPassthrough:'vertical'//横向滚动图片的时候忽略纵向的滚动
    });
    }else{
    this.scroll.refresh();
    }
    }) }
    }

       收藏商家

  • 收藏按钮:设置:active样式(红,白)和字体的变化(收藏和未收藏)
    <div class="favorite"  @click="toggleFavorite($event)">
    <i class="icon-favorite" :class="{'active':favorite}"></i> <!-- 对应是否收藏两种样式-->
    <span>{{favoriteText}}</span> <!-- 有没有选中对应不同的文本,所以这里要绑定一个变量,放到data中 -->
    </div>
  • favorite是一个变量,在data里观测,使用computed定义favoriteText()改变并返回变量
    data() {
    return {
    // favorite: false, //默认没有被收藏,从localStorge中取读取,不是一个默认值了
    favorite: (() => {
    return loadFromlLocal(this.seller.id, 'favorite', false);
    })()
    };
    },
    computed: {
    favoriteText() {
    return this.favorite ? '已收藏' : '收藏';
    }
    }
  • CSS样式

    .favorite
    position: absolute
    right: 11px
    top: 18px
    width: 50px
    text-align: center
    .icon-favorite
    display: block
    margin-bottom: 4px
    line-height: 24px
    font-size: 24px
    width: 50px
    color: #d4d6d9
    &.active
    color: rgb(240,20,20)
    .text
    line-height: 10px
    font-size: 10px
    color: rgb(77,85,93)
  • 添加点击事件,methods中定义toggleFavorite()方法

     toggleFavorite(event) {
    if (!event._constructed) {
    return;
    }
    this.favorite = !this.favorite;
    //这样写取法区分商家id,不同商家的状态一样
    //localStorage.favorite = this.favorite;
    saveToLocal(this.seller.id, 'favorite', this.favorite);
    },
  • 保存收藏状态

  1. 解析url中商家id数据为Object对象:每一个商家都有一个唯一的id,这个id存在url中,所以创建util.js,封装一个函数,将url解析成对象的模式
    /**
    * 解析url参数
    * Created by yi on 2016-12-28.
    * @return Object {id:12334}
    */
    export function urlParse() {
    let url = window.location.search;
    let obj = {};
    let reg = /[?&][^?&]+=[^?&]]+/g;
    let arr = url.match(reg);
    // ['?id=123454','&a=b'] if (arr) {
    arr.forEach((item) => {
    let tempArr = item.substring(1).split('=');// 先分割取到id=123454,之后用=号分开
    let key = tempArr[0];
    let val = tempArr[1];
    obj[key] = val;
    });
    }
    // return obj;
    return {id: 123123};
    };
  2. 在App.vue组件中引入urlParse,并在data中获取data,通过扩展对象在data.json文件中存入data
    import {urlParse} from './common/js/util.js'
    
    data() {
    return {
    seller:{
    id: (() => {
    let queryParam = urlParse();
    // console.log(queryParam)
    return queryParam.id;
    })()
    }
    }
    },
    created: function() {
    this.$http.get('/api/seller?id=' + this.seller.id)
    .then((res) => {
    res = res.body;
    if (res.errno === ERR_OK) {
    this.seller = res.data;
    // console.log(this.seller)
    this.seller = Object.assign({}, this.seller, res.data);//扩展对象 添加其它属性--id
    }
    }, (err) => { })
    }
  3. 刷新之后,收藏样式就会消失:创建store.js实现数据的存取,专门存取不同商家的id,通过唯一id,将收藏的信息添加到localStorge中

    //savaToLocal(this.seller.id, 'favorite', this.favorite);存取
    export function saveToLocal(id, key, value) { //存储到localStorge
    let seller = window.localStorage.__seller__;
    if (!seller) { //没有seller的时候,初始化,定义一个seller对象,并给他设定一个id
    seller = {};
    seller[id] = {}; // 每个id下都是一个单独的obj
    } else {
    seller = JSON.parse(seller); // JSON 字符串转换为对象
    if (!seller[id]) { //判断是否有当前这个商家
    seller[id] = {};
    }
    }
    seller[id][key] = value; // 将key和value存到id这个对象的下边
    //将一个JavaScript值(对象或者数组)转换为一个 JSON字符串
    window.localStorage.__seller__ = JSON.stringify(seller);
    }
    //loadFromlLocal(this.seller.id, 'favorite', false);读取
    export function loadFromlLocal(id, key, def) { //读取,读不到的时候传入一个default变量
    let seller = window.localStorage.__seller__;
    if (!seller) {
    return def;
    }
    seller = JSON.parse(seller)[id]; // 取到这个商家下所有的对象
    if (!seller) {
    return def;
    }
    let ret = seller[key];
    return ret || def;
    }
  4. seller.vue中引入,并在data和toggleFavorite()中使用这两个方法:

    import {saveToLocal, loadFromlLocal} from 'common/js/store.js';
    data() {
    return {
    // favorite: false, //默认没有被收藏,从localStorge中取读取,不是一个默认值了
    favorite: (() => {
    return loadFromlLocal(this.seller.id, 'favorite', false);
    })()
    };
    }
    toggleFavorite(event) {
    if (!event._constructed) {
    return;
    }
    this.favorite = !this.favorite;
    //这样写取法区分商家id,不同商家的状态一样
    //localStorage.favorite = this.favorite;
    saveToLocal(this.seller.id, 'favorite', this.favorite);
    },
三、优化&打包

       优化

  • 问题:切换界面时会闪现
  • 原因:界面被重新渲染了,生命周期函数被重新执行了一遍
  • 优化:切换组件的时候,组件之前的状态也能被保留
  • 解决:vue中提供 vue-router切换组件保留的功能内置组件<keepalive>,在App.vue中更改为
    <keep-alive>
    <router-view :seller="seller"></router-view>
    </keep-alive>

       打包

  • vue-cli 项目打包构建的结果就是根目录下会多出一个dist文件夹:存储编译后的文件
    npm run build
四、相关资料链接

Vue.js官网https://vuejs.org.cn/

Vue-cli:https://github.com/vuejs/vue-cli

Vue-resource: https://github.com/vuejs/vue-resource

Vue-router: https://github.com/vuejs/vue-router

better-scrollhttp://npm.taobao.org/package/better-scroll

webpack官网https://www.webpackjs.com/

Stylus中文文档https://www.zhangxinxu.com/jq/stylus/

 es6入门学习http://es6.ruanyifeng.com/

eslint规则http://eslint.org/docs/rules/

设备像素比https://www.zhangxinxu.com/wordpress/2012/08/window-devicepixelratio/

Flex布局http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html

贝塞尔曲线测试http://cubic-bezier.com/#.17,.67,.83,.67


注:项目来自慕课网