Vue商城——首页功能

Vue商城项目的前提工作

用脚手架3创建项目

  • vue create 项目名称

在GitHub上建一个仓库

将项目与github联系起来

  • git init
  • git add .
  • git commit -m ‘项目名称’
  • git remote add origin github地址
  • git push -u origin master

更新文件到github

  • git add 文件名称或者 git add .

  • git commit -m “这是注释内容”

  • 这一步从本地仓库或本地分支获取并集成(整合),输入指令:git pull origin master

  • 如果过程中出现‘please enter a commit message…’,首先按下esc退出键然后输入 :wq即可

  • 输入指令:git push -u origin master

划分目录结构

  • src
    • assets 放一些资源
      • css
      • img
    • components 放一些公共的组件
      • common 当前项目里面可以使用的组件 在下一个项目里面也可以使用的组件
      • content 与业务相关的组件 只针对于当前项目来说是公共的
    • views 对应视图的一些逻辑
    • router 路由相关的东西
    • store 公共状态的管理 vuex
    • network 网络相关的
    • common 公共的js文件
      • const.js 公共常量
      • utils.js 工具函数
      • mixin.js 一些混入

引入css文件

一般开发的是一个前端项目的话,会对项目里面很多css进行初始化

  • normalize.css 对浏览器上的很多标签进行统一 在github上搜索 文件链接
  • base.css 这是属于自己的css

配置别名

在vue.config.js中配置别名

首页功能的实现

项目模块划分

在components/common封装tabbar
在components/content封装mainTabbar

  1. npm install vue-router --save
  2. 在router的index.js中配置路由映射关系
    对组件进行懒加载
  3. 在main.js中挂载

请求首页的多个数据

  1. npm install axios --save 安装框架
  2. 在network文件夹下新建request.js
  3. import axios from 'axios' 导入
    注意:这里写的是export而不是export default,为什么?
    因为export default只能暴露一个对象,而export 可以暴露多个对象,以后需要用到多个的话就可以
  4. 在network文件夹下新建home.js获取首页所需数据的api
  5. 在Home组件中面向home.js进行开发,首页创建好之后就发送网络请求在create中
  6. 在data中保存数据

网络模块的封装

network->request.js
第一层是工具函数层,封装一个通用的axios,创建一个实例,包括请求的基本路径,请求拦截器,响应拦截器。

export function request(config) {
  // 创建axios实例
  const instance = axios.create({
    // 默认是get请求
    // 支持跨域 jsonp 在url后面写上callback
    // 这个接口不支持post
    baseURL: "http://123.207.32.32:8000",
    timeout: 5000,
  })

  // 请求的拦截器
  instance.interceptors.request.use(config => {
    // 比如config中的一些信息不符合服务器的请求
    // 比如每次发送网络请求时,都希望界面中显示一个请求的图标
    // 某些网络请求(比如登录),需要携带一些信息
    return config
  }, err => {
    console.log(err);
  })

  // 响应的拦截器
  instance.interceptors.response.use(res => {
    return res
  }, err => {
    console.log(err);
  })

  //发送真正的网络请求
  return instance(config)
}

首页面向request发送网络请求
network->home.js
第二层封装,接口层,对首页所有数据的请求都在home.js中,统一管理。

export function getHomeMultidata(){
  return request({
    url:'/home/multidata'
  })
}

第三层,调用层,Home.vue中,首页创建完就进行网络请求

getHomeMultidata(){
      getHomeMultidata().then(res=>{
      //console.log(res);
      // result在组件中,会一直存在,保存在data中
      //this.result=res;
      this.banners=res.data.data.banner.list;// 轮播图
      this.recommends=res.data.data.recommend.list; // 推荐信息
    })
    },

导航栏的封装和使用

在这里插入图片描述

  1. 在common中封装NavBar,使用具名插槽
  2. 在Home中引入封装好的组件
    NavBar的封装
<template>
  <div class="nav-bar">
    <div class="left"><slot name="left"></slot></div>
    <div class="center"><slot name="center"></slot></div>
    <div class="right"><slot name="right"></slot></div>
  </div>
</template>

在Home.vue中的使用
固定在顶部,不跟随滚动条滚动

<nav-bar class="home-nav">
  <div slot="center">购物街</div>
</nav-bar>

轮播图的封装和使用

在这里插入图片描述

  1. 在componets/common下新建swiper文件下,封装Swiper和SwiperItem
  2. 在home/childComps下封装HomeSwiper组件,
  3. 在Home中引入封装好的组件
  4. 展示轮播图数据banners

推荐信息的展示

在这里插入图片描述

  1. 在home/childComps下封装HomeRecommendView组件
  2. 在Home中引入封装好的组件
  3. 展示推荐信息的数据recommend
<div class="recommend">
    <div v-for="(item,index) in recommends" :key="index" class="recommend-item">
      <a :href="item.link">
        <img :src="item.image" alt="">
        <div>{{item.title}}</div>
      </a>
    </div>
 </div>

FeatureView的封装

在这里插入图片描述

  1. 独立组件封装。在home/childComps下封装FeatureView组件
  2. 放的是一张图片 div>a>img

TabControl选项卡的封装

在这里插入图片描述

  1. 在componets/content下封装TabControl,只是文字不一样的时候,就没必要弄插槽了
  2. props->titles div->根据titles v-for遍历 div->span{{title}}
  3. flex布局,均等分
  4. 选中谁,谁就变红色
    在data中弄一个变量currentIndex来记录当前谁被选中;
    动态绑定class属性,:class="{active:index == currentIndex}",默认第一个选中变红色;
  5. 点击谁的时候,谁就变,监听item的点击,绑定点击事件@click="itemClick(index)"
itemClick(index){
      this.currentIndex=index;
 }
  1. 吸顶效果,监听滚动,当滚动到一定位置的时候,将position设置成fixed,向下滚动的时候把fixed属性删除,就可以随着一起滚动了。position:sticky;必须设置top值,这个属性的作用是 在没有达到top值之前,position是sticky,达到之后改成fixed,但是很多浏览器不兼容这个属性。

商品列表数据的请求和展示

在这里插入图片描述

  1. 请求商品数据,既要展示流行数据,又要展示新款数据,还要展示精选数据,根据不同的点击,展示不同的数据
  2. 用变量保存数据,一次性把三个数据请求一下,在data中定义
    要先考虑设计什么数据结构,然后再去使用
    有自己对应的页码和数据,page用来记录当前加载到第几页了,list用来保存数据
goods:{
        'pop':{page:0,list:[]},
        'new':{page:0,list:[]},
        'sell':{page:0,list:[]},
      },
  1. 在network的home.js中获取商品所需数据的api,getHomeGoods
export function getHomeGoods(type,page) {
  return request({
    url: '/home/data',
    params:{
      type,
      page
    }
  })
}
  1. 在Home的create中请求商品数据
    默认情况下先加载第一页的数据,在以后的上拉加载更多的操作下再去请求更多的数据。
getHomeGoods(type){
      const page=this.goods[type].page + 1;
      getHomeGoods(type,page).then(res=>{
      //对象中的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中
      this.goods[type].list.push(...res.data.data.list);
      this.goods[type].page += 1;
    })
},
  1. 在components/content下新建goods文件夹,在里面封装GoodsList.vue和GoodsListItem.vue,从某一个类型中取出商品的list传给GoodsList,然后遍历出小的item传给GoodsListItem,让GoodsListItem展示商品
  2. 在GoodsList.vue中,定义props:goods,商品一行显示两个,用flex布局,给子item设置固定的宽度
.goods{
  display: flex;
  flex-wrap: wrap;
  justify-content: space-around;
}
  1. 在GoodsListItem.vue中,定义props:goodsItem,父组件向子组件传数据,用props

TabControl点击切换商品

  1. 首先内部监听点击,将事件传到Home中去,之前点击的时候只是内部样式的改变,现在需要传递事件,子组件发生点击事件,将事件传给父组件,用自定义事件$emit,
methods: {
    itemClick(index){
      this.currentIndex=index;
      this.$emit('tabClick',index)
    }
  }

在父组件Home中监听事件,@tabClick="tabClick",根据监听决定goodslist显示商品的类型。

  1. 定义一个当前显示的商品类型,默认是pop,currentType:'pop',根据点击切换显示类型
<goods-list :goods="goods[currentType].list"/>

tabClick(index){
      switch (index) {
        case 0:
          this.currentType='pop'
          break;
        case 1:
          this.currentType='new'
          break;
        case 2:
          this.currentType='sell'
          break;
      }
}
  1. :goods="goods[currentType].list"这一串太长了,弄一个计算属性computed,来展示商品
<goods-list :goods="showGoods"/>

computed: {
    showGoods(){
      return this.goods[this.currentType].list
    }
  },

吸顶效果

  1. 必须知道滚动到多少时,开始有吸顶效果,获取到tabcontroloffsetTop,在data中保存tabOffsetTop:0,为了能够拿到,绑定一个属性ref="tabControl",拿到组件,组件没有offsetTop属性,而是要拿到组件对应的元素,所有的组件都有一个属性$el,用来获取组件中的元素mounted中DOM加载完毕,但是图片不一定加载完了,所以要等图片加载完了之后计算出一个最终的offsetTop
  2. 监听轮播图是否加载完成,只需要发出一次事件,弄一个变量记录状态,和之前的防抖的区别

在HomeSwiper.vue中

<img :src="item.image" @load="imageLoad">

data(){
    return{
      isLoad:false; // 只需要发出一次事件
    }
  },

imageLoad(){
      if(!this.isLoad){
        this.$emit('swiperImageLoad');
        this.isLoad=true;
      }
    }

在Home.vue中

<home-swiper :banners="banners" @swiperImageLoad="swiperImageLoad"/>

swiperImageLoad(){
      this.tabOffsetTop=this.$refs.tabControl.$el.offsetTop;
    },
  1. 监听滚动,动态改变tabControl的样式,弄一个变量,isTabFixed:false,默认情况下不吸顶,当滚动到一定位置的时候,改变状态,动态绑定class
<tab-control :titles="['流行','新款','精选']" @tabClick="tabClick"
             ref="tabControl" :class="{fixed:isTabFixed}"/>
             
contentScroll(position){
      //1.判断回到顶部是否显示
      this.isShowBackTop = (-position.y) > 1000
      //2.判断tabControl是否吸顶
      this.isTabFixed = (-position.y) > this.tabOffsetTop
    },

.fixed{
  position: fixed;
  left: 0;
  right: 0;
  top: 44px;
}

问题:这种方法下面的商品内容会一下子往上移,虽然tabControl设置了fixed,但是也随着better-scroll一起滚出去了,因为tabControl会脱标。better-scroll滚动是根据translate属性移动的,设置了fixed还是能跟着一起滚动。
可以把tabControl再复制一份,来实现停留效果。
在这里插入图片描述
在这里插入图片描述
但是选项卡会被标题导航给盖住。
标题购物车可以不定位,这样就不会脱标,使用定位是在用浏览器原生滚动的时候。复制的tabControl放到navbar的下面,此时在轮播图的后面,再设置相对定位给一个层级关系,相对定位是因为还在原来的位置。默认它是不显示的,v-show="isTabFixed",当滚动到一定位置的时候再显示,当滚动没有达到一定位置时,隐藏。

swiperImageLoad(){
      this.tabOffsetTop=this.$refs.tabControl2.$el.offsetTop;
    },

问题:现在两个tabControl并没有保持一致。

tabClick(index){
      switch (index) {
        case 0:
          this.currentType='pop'
          break;
        case 1:
          this.currentType='new'
          break;
        case 2:
          this.currentType='sell'
          break;
      }
      this.$refs.tabControl1.currentIndex=index;
      this.$refs.tabControl2.currentIndex=index;
    },

我们并不能保证用户点击的是哪一个,所以两个都要赋值。

当把项目部署到服务器之后,用手机端去请求网页的时候,样式各方面没什么问题,但是滚动的时候,没有一个滚动的时候的滑动效果,滑动的时候也很卡顿,此时用的是原生的滚动,什么是原生的滚动呢?当里面的内容超过了当前的窗口的时候,自动就可以滚动了。但是用在移动端,会非常卡顿。以前用iScroll来适配移动端的滚动,但是这个框架现在作者不更新了。better-scroll在iscroll基础上,还加入了一些c3的属性。可以实现移动端的顺滑滚动,还在顶部和底部都增加了弹簧效果。

原生的滚动

.content{
	height:150px;
	overflow-y:scroll;
}

Better-Scroll的安装和使用

npm install better-scroll --save
在外层弄一个wrapper,wrapper需要固定的高度,在 wrapper里面放内容content,只能是一个标签组成的content,它是父元素的第一个子元素,滚动的部分是content元素。

  1. 使用
    import BScroll from 'better-scroll'
    new BScroll(el,options) el挂载一个要滚动的元素
    new BScroll(document.querySelector('.wrapper'))
    也可以直接传一个类,根据类型查找标签
    new BScroll('.wrapper',{})

  2. 实时监听当前滚动到什么位置了
    (默认情况下,BScroll不能实时监听滚动的位置)
    需要设置一个属性 probeType
    probeType值为0,1 不侦测实时的位置
    2 在手指滚动的过程中侦测,手指离开后的惯性滚动过程中不侦测
    3 只要是在滚动,都侦测

  3. 上拉加载更多 设置pullUpLoad为true,要调用finishPullUp方法才能实现下一次的上拉加载更多
    要监听什么时候滚动到最底部了

  4. 滚动区域包裹的元素需要监听点击 ,设置click属性是true,better-scroll会阻止浏览器的原生 click 事件(现在好像可以了)

BScroll的基本使用

on方法

scroll事件 滚动的实时坐标

bscroll.on("scroll",(position)=>{
	console.log(position);
})

pullingUp 上拉加载更多

bscroll.on("pullingUp",()=>{
	// 发送网络请求,请求更多页数据
	// 等数据请求完,并且将新的数据展示出来后
	bscroll.finishPullUp(); // 因为只能上拉加载一次,执行完这个函数之后,才能进行下一次上拉加载更多
})

封装better-scroll与使用

  1. 在components/common下创建一个scroll文件夹,在里面封装Scroll
  2. 需要一个插槽
  3. 引入better-scroll,
  4. DOM被挂载时,初始化BScroll,querySelector不能明确的指定拿到的是哪个元素,绑定ref属性。
    例如在Home.vue中也有class=“wrapper”,在App.vue中也有class=“wrapper”,不知道到底取的是哪个,querySelector获取的可能是第一个。

ref如果绑定在组件中,this.$refs.refname获取到的是一个组件对象 。一般绑定在子组件。
ref如果绑定在普通的元素中,获取到的是一个元素

  1. home的高度太高了,相当于所有内容的高度,给一个100的视口高度,在Home中给滚动的区域content一个固定的高度
#home{
  height: 100vh;
  position: relative;
}
.content{
  overflow: hidden;
  position: absolute;
  top:44px;
  bottom: 49px;
  left:0;
  right:0;
}

可滚动区域的问题

在决定有多少区域可以滚动时,根据scrollerHeight属性决定的,scrollerHeight属性是根据放在better-scroll的content中的子组件的高度,
但是在首页中,刚开始在计算scrollerHeight属性时,是没有将图片计算在内的,后来图片加载进来之后有了新的高度,但是scrollHeight属性并没有更新。

监听每一张图片是否加载完成,只要有一个图片加载完成了,执行一次refresh()。
如何监听图片是否加载完成?

原生js img.οnlοad=function() {}
vue @load=‘方法’
事件总线:和vuex很像,不是管理状态的,而是管理事件的。因为涉及到非父子组件通信。

在GoodsListItem中监听图片是否加载完成,

<img :src="showImage" alt="" @load="imageLoad"">
imageLoad(){
      this.$bus.$emit('itemImageLoad')
    },

在main.js中

Vue.prototype.$bus=new Vue()

在Scroll中

refresh(){
      this.scroll && this.scroll.refresh && this.scroll.refresh()
    },

在Home.vue的mounted中接收事件总线,refresh会执行很多次

this.$bus.$on('itemImageLoad',()=>{
      this.$refs.scroll.refresh()
    })

刷新频繁防抖处理
debounce

如果直接执行refresh,会执行很多次,可以将refresh函数传到debounce函数中,生成一个新的函数,之后在调用非常频繁的时候,就用新生成的函数,而新生成的函数,并不会非常频繁的调用,如果下一次执行来的非常快,那么会将上一次取消掉。
在common/utils.js中封装防抖函数,

export function debounce(func, delay){
  let timer = null
  // ...可以传几个参数
  return function (...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, delay);
  }
}

回到顶部

按钮的封装和使用

  1. 在components/content下创建一个backTop文件夹,在里面封装BackTop
  2. 在BackTop里面放一个图片,定位在右下角
  3. 在Home中引入,不需要放在滚动区域scroll中
  4. 在BackTop中监听回到顶部不太好,回到顶部需要拿到滚动区域scroll对象,所以监听回到顶部的点击事件放到Home中比较好
  5. 监听组件的点击事件,要调用.native修饰符(监听一个组件的原生事件时);也可以在BackTop.vue内部监听点击,用$emit传给父组件Home.vue。像button,div是可以直接监听点击的。组件不能直接监听点击。
<back-top @click.native="backTopClick"></back-top>
  1. 在Scroll中封装scrollTo()方法
scrollTo(x,y,time=300){
      this.scroll && this.scroll.scrollTo(x,y,time)
    },
  1. <scroll>绑定ref,拿到滚动的对象
backTopClick(){
      this.$refs.scroll.scrollTo(0,0,500)
    },

按钮的显示和隐藏

滚动到一定区域的时候才显示,回到的时候隐藏

  1. 在Scroll中监听滚动的位置,在props中定义probeType
props:{
    probeType:{
      type:Number,
      default:0,
    },
  1. 在Home中要监听scroll的实时滚动,不加冒号会把probe-type当成字符串
    <scroll class="content"
            ref="scroll"
            :probe-type="3">
  1. 在Scroll中要根据在不同地方的应用的时候,有没有传入probeType的值,决定别人要不要实时监听,在父组件中监听子组件传过来的自定义事件@scroll="contentScroll"
if(this.probeType===2 || this.probeType === 3){
      this.scroll.on('scroll',(position)=>{
      this.$emit('scroll',position)
    })
    }
<scroll class="content"
        ref="scroll"
        :probe-type="3"
        @scroll="contentScroll">

5.在Home中根据滚动的位置决定是否显示还是隐藏,用v-show ,默认一进去的时候是不显示的,isShowBackTop:false,

<back-top @click.native="backTopClick" v-show="isShowBackTop"></back-top>

contentScroll(position){
      //1.判断回到顶部是否显示
      this.isShowBackTop = (-position.y) > 1000
    },

完成上拉加载更多

  1. 监听什么时候滚动到底部。在Scroll中,定义pullUpLoad属性,
props:{
    pullUpLoad:{
      type:Boolean,
      default:false
    }
  },

在Home.vue中

    <scroll class="content"
            ref="scroll"
            :probe-type="3"
            @scroll="contentScroll"
            :pull-up-load="true">

在Scroll中,向父组件传递事件,pullingUp在一次上拉加载的动作后,这个时机一般用来去后端请求数据。

if(this.pullUpLoad){
      this.scroll.on('pullingUp',()=>{
        this.$emit('pullingUp')
      })
    }
  1. 在Home的<scroll>中定义属性 @pullingUp="loadMore",加载更多的时候是针对商品类型来加载的,选中谁就给谁上拉加载更多
<scroll class="content"
            ref="scroll"
            :probe-type="3"
            @scroll="contentScroll"
            :pull-up-load="true"
            @pullingUp="loadMore">
            
loadMore(){
      //只能上拉加载一次
      this.getHomeGoods(this.currentType)
    },

但是此时只能上拉加载一次,在数据加载完成之后,调用finishPullUp可以实现多次

getHomeGoods(type){
      const page=this.goods[type].page + 1;
      getHomeGoods(type,page).then(res=>{
      this.goods[type].list.push(...res.data.data.list)
      this.goods[type].page += 1
      //可以多次上拉加载
      this.$refs.scroll.finishPullUp()
    })
    },

Home离开时记录状态和位置

当滚动一半的时候,切换去了其他页面,再回到首页的时候,并没有保持在原来的位置,而是回到了顶部。

让Home不要随意销毁掉

使用keep-alive

<keep-alive>
	<router-view/>
</keep-alive>

让Home保持原来的位置

离开时,保存一个位置信息saveY
进来时,将位置设置为原来保存的位置信息saveY即可
进来的时候可能刷新一下,不然可能会回到顶部
在Scroll.vue中

getCurrentY(){
      return this.scroll ? this.scroll.y : 0
    }

在Home.vue中

 // 进来的时候设置位置
activated() {
    this.$refs.scroll.scrollTo(0,this.saveY,0)
    this.$refs.scroll.refresh();// 不然可能出现一些问题
  },
 // 离开的时候记录位置
deactivated() {
    this.saveY=this.$refs.scroll.getCurrentY()
  },

点击首页中的商品跳转到商品详情页

点击商品进去详情页,根据点击请求更加详细的信息,要传过来goodsItem的iid,根据id去服务器请求更加详细的信息;配置路由映射关系,点击进行跳转,带参数传递跳转。
在GoodsListItem中

itemClick(){
        this.$router.push('/detail/'+this.goodsItem.iid)
        /* this.$router.push({
          path:'/detail',
          query:{
            iid:this.goodsItem.iid
          }
        }) */
      }

但是获取到的iid在更换点击商品时没有改变,并没有发生新的请求,因为发生了路由跳转,router-view由keep-alive包着,不会每次销毁并重新创建,所以不会给iid给新的值,详情页不要使用keep-alive,使用exclude属性

<keep-alive exclude="Detail">
	<router-view></router-view>
</keep-alive>

版权声明:本文为qq_41217480原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。