【Vue项目实战】vue.js2.5饿了么APP(6)主要组件实现 - 评价页+商家页部分【速看】面试教你有话可说

528 阅读8分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

个人主页juejin.cn/user/180285…

之前章节传送:

【Vue项目实战】vue.js2.5 饿了么APP(1)概述+项目准备 juejin.cn/post/708900…

【Vue项目实战】vue.js2.5饿了么APP(2)主要组件实现 - 头部相关组件 - 【速看】面试教你有话可说 juejin.cn/post/708932…

【Vue项目实战】vue.js2.5饿了么APP(3)主要组件实现 - 购物车相关组件(上)juejin.cn/post/708900…

【Vue项目实战】vue.js2.5饿了么APP(4)主要组件实现 - 购物车相关组件(下)juejin.cn/post/709075… 【Vue项目实战】vue.js2.5饿了么APP(5)主要组件实现 - 商品详情页部分 juejin.cn/post/709627…


速看

1. 商家评价页面 商家评价页面主要是显示商家评分以及评论,复用了rating-select组件。其中需要从seller接口获取评价数据。

2. 商家详情页面 商家详情页主要包括:商家具体信息(评分 销量)+ 公告活动 + 商家实景 + 商家额外信息 + 页面收藏。其中收藏页面本地缓存的设计思想:收藏的效果就是,点击按钮切换图标的显示状态,但是问题是,当再次刷新页面,状态会被重新初始化。由于项目只提供了一个商家的信息,并且不包含id。因此,希望通过url解析出包含id值的一个对象,在每次请求时加上id进行模拟收藏效果。为了实现本地存储,使用了封装了sessionstorage的storage库,提供了set get接口,和直接调用原生api 需要对对象和字符串进行转换相比,使用起来更简单,可以直接对对现象操作。导出两个接口saveLocal和loadLocal,来实现本地缓存。saveload中构造一个复杂对象,第一层是id,它还是一个对象,包含收藏和其他需要存储的项,然后返回该对象。最后的效果就是:点击收藏之后会在localstorage中添加一个__seller__,会显示id,以及favorite的true false。

小掘友的话:本文充斥着很口语化的字样,在于友友希望做完一个项目之后可以很好的表达出来,面试被提问到的时候也有话可说、能够表达清除。

一、商家评价页面(ratings)

1. 概述

商家评价页面主要是显示商家评分以及评论,复用了rating-select组件。其中需要从seller接口获取评价数据。

2. 布局

评价页主要分两个部分:整体介绍(评分)+ 评价部分(rating-select组件 + ratings列表)

        <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 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>
            <split></split>
            <rating-select
                :ratings="ratings"
                :onlyContent="onlyContent"
                :selectType="selectType"
                @select="onSelect"
                @toggle="onToggle"
                v-if="ratings.length">
            </rating-select>
            <div class="rating-wrapper">
                <ul>
                    <li
                        v-for="(rating, index) in computedRatings"
                        :key="index"
                        class="rating-item border-bottom-1px">
                        <div class="avatar">
                            <img width="28" height="28" :src="rating.avatar">
                        </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">
                                <span class="icon-thumb_up"></span>
                                <span
                                    class="item"
                                    v-for="(item, index) in rating.recommend"
                                    :key="index">{{item}}
                                </span>
                            </div>
                            <div class="time">{{format(rating.rateTime)}}</div>
                        </div>
                    </li>
                </ul>
            </div>
        </div>
    </cube-scroll>

3. 实现

(1)数据获取

对于整体介绍部分overview部分的数据都是通过seller取的。 为了拿到评价的数据,注意需要导出接口,而ratings有单独接口,在index.js中写入,并在ratings组件中导入import { getRatings } from 'api'

 
const getSeller = get('/api/seller')
const getGoods = get('/api/goods')
const getRatings = get('/api/ratings')
 
export {
  getSeller,
  getGoods,
  getRatings
}

getRatings何时使用:会在每次tab切换的时候执行fetch钩子方法(获取数据)

因此在ratings中添加方法fetch,避免多次获取数据(只获取一次就可以了)

            if (!this.fetched) {
                this.fetched = true
                getRatings().then((ratings) => {
                    this.ratings = ratings
                })
            }
        },

(2)滑动的实现

为了实现滑动,使用了cube-scroll组件,并作默认设置(防止横向纵向滚动有冲突)

        data: {
            type: Object
        }
    },
data() {
        return {
            ratings: [],
            scrollOptions: {
                click: false,
                directionLockThreshold: 0
            }
        }
    },

(3)评价部分

商家评论列表部分和商品评价部分设计思路相同,但是没有做成大的评论组件,在于DOM结构还是有区别的。

(4)使用rating-select组件

新建ratings的mixin抽离出评价的公共部分,组件的使用如布局所示

 
export default {
    data() {
        return {
            onlyContent: true,
            selectType: ALL
        }
    },
    computed: {
        computedRatings() {
            const ret = []
            this.ratings.forEach((rating) => {
                if (this.onlyContent && !rating.text) {
                    return
                }
                if (this.selectType === ALL || this.selectType === rating.rateType) {
                    ret.push(rating)
                }
            })
            return ret
        }
    },
    methods: {
        onSelect(type) {
            this.selectType = type
        },
        onToggle() {
            this.onlyContent = !this.onlyContent
        }
    }
}

(5)优化

当点击评价时先展开rating-select然后渲染出评价部分,但是这样时不友好的,当没有ratings时是没有rating-select的,因此在组件上添加v-if="ratings.length",这时,只有评论加载完成之后才会渲染rating-select组件。

二、商家详情页面(seller)

1.概述

商家详情页主要包括:商家具体信息(评分 销量)+ 公告活动 + 商家实景 + 商家额外信息 + 页面收藏。

收藏页面本地缓存的设计思想:收藏的效果就是,点击按钮切换图标的显示状态,但是问题是,当再次刷新页面,状态会被重新初始化。由于项目只提供了一个商家的信息,并且不包含id。因此,希望通过url解析出包含id值的一个对象,在每次请求时加上id进行模拟收藏效果。为了实现本地存储,使用了封装了sessionstorage的storage库,提供了set get接口,和直接调用原生api 需要对对象和字符串进行转换相比,使用起来更简单,可以直接对对现象操作。导出两个接口saveLocal和loadLocal,来实现本地缓存。saveload中构造一个复杂对象,第一层是id,它还是一个对象,包含收藏和其他需要存储的项,然后返回该对象。最后的效果就是:点击收藏之后会在localstorage中添加一个__seller__,会显示id,以及favorite的true false。

2.布局

商家具体信息(评分 销量)+ 公告活动 + 商家实景 + 商家额外信息

   其中使用了star组件 split组件等,主要是静态页面,没有着重要说的部分

        <div class="seller-content">
            <div class="overview">
                <h1 class="title">{{seller.name}}</h1>
                <div class="desc border-bottom-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.deliveryPrice}}</span>元
                        </div>
                    </li>
                </ul>
                <div class="favorite">
                    <span class="icon-favorite" :class="{'active':favorite}"></span>
                    <span class="text">{{favoriteText}}</span>
                </div>
            </div>
            <split></split>
            <div class="bulletin">
                <h1 class="title">公告与活动</h1>
                <div class="content-wrapper border-bottom-1px">
                    <p class="content">{{seller.bulletin}}</p>
                </div>
                <ul v-if="seller.supports" class="supports">
                    <li
                        class="support-item border-bottom-1px"
                        v-for="(item, index) in seller.supports"
                        :key="index">
                        <support-ico :size=4 :type="seller.supports[index].type"></support-ico>
                        <span class="text">{{seller.supports[index].description}}</span>
                    </li>
                </ul>
            </div>
            <split></split>
            <div class="pics">
                <h1 class="title">商家实景</h1>
                <cube-scroll class="pic-wrapper" :options="picScrollOptions">
                    <ul class="pic-list">
                        <li class="pic-item"
                            v-for="(pic, index) in seller.pics"
                            :key="index">
                        <img :src="pic" width="120" height="90">
                        </li>
                    </ul>
                </cube-scroll>
            </div>
            <split></split>
            <div class="info">
                <h1 class="title border-bottom-1px">商家信息</h1>
                <ul>
                    <li
                        class="info-item border-bottom-1px"
                        v-for="(info, index) in seller.infos"
                        :key="index">
                    {{info}}
                    </li>
                </ul>
            </div>
        </div>
    </cube-scroll>

3. 实现

(1)滚动的实现

使用cube-scroll组件实现滚动,其中商家实景部分也需要使用cube-scroll,使用纵向滚动,其中横向滚动时每一个li的布局是inline-block,并且配置scrollX: true, 并且阻止滚动的冒泡。横向滚动和纵向滚动不影响。其中默认配置:

        return {
            favorite: false,
            sellerScrollOptions: {
                directionLockThreshold: 0,
                click: false
            },
            picScrollOptions: {
                scrollX: true,
                stopPropagation: true,
                directionLockThreshold: 0
            }
        }
    },

(2)收藏页面设计思路

思路:在收藏商家的时候,不同的商家回有不同的标识如id,在url中有一个字段来区分(如?id=1或?id=2)对于请求不同的数据,商家信息也是不同的,因此需要解析出url中的id参数作为异步接口的参数告诉服务端服务端通过接口拿到id再根据不同的id取不同数据返回给前端。(但是由于本项目中模拟数据是相同的,因此返回的数据也是一样的)

(3)区分id

1)可以手写解析或者使用query-string库,npm安装之后在app.vue写入import qs from 'query-string'

    return {
      seller: {
        id: qs.parse(location.search).id
      }
    }
  },

2)这时就可以拿到id,为了将id作为接口的请求参数传递给服务端,因此修改方法 ,此时可以看到id作为参数已经传入

    _getSeller() {
      getSeller({
        id: this.seller.id
      }).then((seller) => {
        this.seller = seller
      })
    }
  },

3)同理,修改其他组件goods组件 ratings组件。之后可以看到效果seller和goods都可以传入参数,但是ratings没有,

            if (!this.fetched) {
                this.fetched = true
                getGoods({
                    id: this.seller.id
                }).then((goods) => {
                this.goods = goods
            })
            }
        },
            if (!this.fetched) {
                this.fetched = true
                getRatings({
                    id: this.seller.id
                }).then((ratings) => {
                    this.ratings = ratings
                })
            }
        },

4)修改vue.config.js文件中的devServer部分代码,从而实现了前后端联动

app.get('/api/seller', function (req, res) {
        res.json({
          errno: 0,
          data: seller
        })
      })
// 修改后
app.get('/api/seller', function (req, res) {
        const id = req.query.id
        res.json({
          errno: 0,
          data: Object.assign({}, seller, { id })
        })
      })

(4)添加收藏方法

文案“收藏”“已收藏”以及效果切换,但是效果是刷新页面就会丢失

            this.favorite = !this.favorite
        }

(5)本地缓存实现思路

在common中添加js文件,并创建storage.js

对于项目扩展可以使用本地缓存去存储其他东西,都可以复用通用存储的功能。 使用一个good-storage库,提供了set get接口,并且对sessionstorage做了封装,好处在于相对于直接使用原生API,存在两个问题:(1)首先需要测试storage可不可以用,在隐身模式操作loaclstorage和sessionstorageAPI会报错。(2)当存储对象的时候,需要先把对象转化为字符串存储,并且获取数据时需要利用json.pase转换成对象。

(6)导出存储通用接口

在商家页面定义一个SELLER_KEY

导出两个接口:saveToLocal(id, key, value)。逻辑:构造一个复杂对象,首先第一层是id,用于区分不同的商家,他是一个对象(因为对于一个商家可以存储不同的key,比如收藏favorite、评价等),因此先取到它,然后对其修改,然后把对象作为整体存储。

loadFromLocal(id, key, def)。逻辑:读取时如果读不到,直接取空对象。如果没有定义id就返回默认值,如果定义了id,如果没有key就取默认值,有key就返回key

 
const SELLER_KEY = '__seller__'
 
export function saveToLocal(id, key, val) {
    const seller = storage.get(SELLER_KEY, {})
    if (!seller[id]) {
        seller[id] = {}
    }
    seller[id][key] = val
    storage.set(SELLER_KEY, seller)
}
 
export function loadFromLocal(id, key, def) {
    const seller = storage.get(SELLER_KEY, {})
    if (!seller[id]) {
        return def
    }
    return seller[id][key] || def
}

(7)存储方法使用

在seller中可以使用两个方法,添加created

        this.favorite = loadFromLocal(this.seller.id, KEY, false)
    },
    methods: {
        toggleFavorate() {
            this.favorite = !this.favorite
            saveToLocal(this.seller.id, KEY, this.favorite)
        }
    },

效果:点击“收藏”后会显示存储,切换id=1在收藏local storage中会添加,并且保存状态。