超详细的基于Vue全家桶的移动端商城项目(仿蘑菇街)

超详细的基于Vue的移动端商城(仿蘑菇街)案例制作过程,本案例基于B站coderwhy老师(点进去即是项目的视频)的学习,从13个大部分讲了整个案例的制作过程,项目的准备工作、首页、详情页、购物车以及bug的解决和项目的继续优化,最后还讲了项目的部署,项目源码GitHub地址,如果觉得文章太长的话,可以点这里,拆解成了多个部分,也有自己基于该老师所记的Vue学习笔记,里面有配套的思维导图地址

Vue移动端商城案例

git仓库初始化

这里,有两种方案去git自己的项目,一个是在创建好项目文件夹后,连接到事先创建好的git仓库并push,另外一种是在创建好git仓库后,在本地文件夹下clone并创建自己的项目

这里说一下第一种方法

  1. 首先就是新建一个仓库了
    在这里插入图片描述

  2. 然后就是用脚手架创建vue项目,接着在根目录git bash
    在这里插入图片描述

  3. 执行下面代码

    git remote add origin git@github.com:Dong-666/vue-te.git
    git branch -M main
    git push -u origin main
    

ok,项目以及仓库都初始化完成

划分目录结构

src下的目录结构

  • network (网络模块)→相关网络请求(axios)
  • components (组件)
    • common (公共模块)→可以多项目复用
    • content (业务模块)→该项目可以复用
  • views (路由页面模块)
  • common (公共js文件–常量…)
  • assets (静态资源)
  • router (路由)→页面跳转
  • store (vuex状态管理)

设置CSS初始化和全局样式

适应不同浏览器端对页面的适配以及设置好项目的主题颜色

css样式初始化:normalize.css

主题颜色以及css常量设置:base.css

路径别名以及代码风格设置

在项目根目录下创建一个vue.config.js文件,并填入相关配置

module.exports = {
   
  configureWebpack: {
   
    resolve: {
   
      alias: {
   
        'assets': '@/assets',
        'common': '@/common',
        'components': '@/components',
        'network': '@/network',
        'views': '@/views'
      }
    }
  }
}

从项目代码规范方引入.editorconfig文件,代码不摆了,每个项目有各自的风格

举例

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

tabbar引入以及模块划分(项目结构初始化)

将之前创建好的tabbar模块引入,方法同之前tabbar项目,并将首页等各view文件夹和页面html创建出来,路由表的创建等,注意路径

更换图标

在pubic文件夹下的index.html文件中有下列代码

其中,它利用了正则表达式去获取图标位置(知识待补充)

我们只需将同级目录下的favicon.ico文件替换成自己的图标即可

首页开发

首页导航栏的封装(navbar)

因为导航栏的通用性,在其它项目中可能可以用到,所以我们把它封装到components中的common里面去,命名为NavBar.vue

因为导航栏可能因为内容的改变而会有不同位置出现不同的内容,所以我们为其定义三个插槽,分别为左中右,同时定义样式

<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>

<script>
  export default {
    name: "NavBar"
  }
</script>

<style scoped>
  .nav-bar {
    height: 44px;
    display: flex;
    text-align: center;
    line-height: 44px;
    box-shadow: 0 1px 1px rgba(100,100,100,.1)
  }
  .center {
    flex: 1;
  }
  .right, .left {
    width: 60px;
  }
</style>

接着,当然就是在首页引用该组件啦,在首页导入该组件,同时在插槽处添加所需加的内容,然后就是样式的相关修改了

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

<script>
  import NavBar from 'components/common/navbar/NavBar.vue'

  export default {
    name:'Home',
    components: {
      NavBar
    },
    data () {
      return{
      }
    },
    created() {
    },
    computed: {
    }
  }
</script>

<style scoped>
  #home {
    padding-top: 44px;
    position: relative;
  }
  .home-nav {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    color: white;
    z-index: 10;
    background-color: var(--color-tint);
  }
</style>

请求首页的多个数据

请求,就意味着我们需要书写网络请求相关代码了,安装axios插件

npm i axios --save

来到network下新建home.js文件,作为统一的网络请求

引入相关模块,并导出网络请求,使用函数getHomeMultidata封装该请求

import {
   request} from './request.js'

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

接着,在Home组件中,在其被创建(create)时加入该网络请求

 import {
   getHomeMultidata, getHomeGoods} from 'network/home.js'

 export default {
   
    name:'Home',
    created() {
   
       getHomeMultidata().then(res => {
   
          this.banners = res.data.banner.list
          this.recommends = res.data.recommend.list
       })
    }
  }

定义一个data数据用来存储请求返回得到的数据(熟悉垃圾回收)

    data () {
   
      return{
   
        banners: [],
        recommends: []
      }
    },

轮播图(swiper)

轮播图的制作后面要补上(vue实现)

这里根据设计图,将首页再次划分成几大模块
在这里插入图片描述

好吧,图有点丑,但不妨碍理解,这里,我们最终要呈现的是swiper这个大的组件,所以,为了让首页的逻辑更加清晰点,我们只定义上述图中的组件,而每个组件的内部实现,则由各自的组件去实现

所以,我们在home文件夹下新建childComps文件夹,新建HomeSwiper.vue,用来封装swiper相关组件

在HomeSwiper里,引入两个swiper相关模块并设置样式,好的,到这里,你就会发现父组件的数据咋办,是的,用props

HomeSwiper.vue

  props: {
   
    banners: {
   
      type: Array,
      default() {
   
        return []
      }
    }
  }

Home.vue

<home-swiper :banners='banners'></home-swiper>

ok,放出HomeSwiper全部代码

<template>
  <swiper>
    <swiper-item v-for="(item,index) in banners">
      <a :href="item.link">
        <img :src="item.image" alt="">
      </a>
    </swiper-item>
  </swiper>
</template>

<script>
import { Swiper, SwiperItem } from 'components/common/swiper/index.js'
export default {
  name: 'HomeSwiper',
  props: {
    banners: {
      type: Array,
      default() {
        return []
      }
    }
  },
  components: {
    Swiper,
    SwiperItem
  }
}
</script>

<style scoped>
</style>

推荐信息的展示

这里没啥可说的,主要是用到v-for去展示推荐信息

放上HomeRecommend代码

<template>
  <div class="recommend-view">
    <div v-for="item in recommends" class="recommend-item">
      <a :href="item.link">
        <img :src="item.image" alt="">
        <div>{
  {item.title}}</div>
      </a>
    </div>
  </div>
</template>

<script>
export default {
  name: 'HomeRecommendView',
  props: {
    recommends: {
      type: Array,
      default() {
        return []
      }
    }
  },
  components: {
  }
}
</script>

<style scoped>
  .recommend-view {
    display: flex;
    width: 100%;
    /* 水平居中 */
    text-align: center; 

    font-size: 12px;
    padding: 10px 0 20px;
    border-bottom: 10px solid #eee;
  }
  .recommend-item {
    flex: 1;
  }
  .recommend-item img {
    width: 70px;
    height: 70px;
    margin-bottom: 10px;
  }
</style>

FeatureView的实现

这个更简单,案例直接写死了,直接放代码

<template>
  <div class="feature">
    <a href="https://act.mogujie.com/zzlx67">
      <img src="~assets/img/home/recommend_bg.jpg" alt="">
    </a>
  </div>
</template>

<script>
export default {
  name: 'FeatureView',
  props: {
  },
  components: {
  }
}
</script>

<style scoped>
  .feature img{
    width: 100%;
  }
</style>

TabControl

和推荐信息一个道理,没啥好说,最多就是样式上的修改

<template>
  <div class="tab-control">
    <div v-for="(item,index) in titles" 
    class="tab-control-item"
    :class = "{active: index === currentIndex}">
      <span>{
  {item}}</span>
    </div>
  </div>
</template>

<script>
  export default {
    name:'TabControl',
    components: {
    },
    data () {
      return {
        currentIndex: 0
      }
    },
    props:{
        titles: {
          type: Array,
          default() {
            return []
        }
      }
    },
    methods: {
      itemchange(index) {
        this.currentIndex = index
      }
    }
  }
</script>

<style scoped>
  .tab-control {
    display: flex;
    height: 40px;
    line-height: 40px;
    text-align: center;
    font-size: 15px;
    background-color: #fff;
  }

  .tab-control-item {
    flex: 1;
  }

  .active {
    color: var(--color-high-text);
  }

  .active span {
    border-bottom: 3px solid var(--color-tint);
  }
</style>

在home.vue,为了实现tabControl吸顶效果,这里要使用到css一个属性position: sticky

  .tab-control {
   
    position: sticky;
    top: 43px;
    z-index: 10;
  }

设计商品的数据结构

在这里,首页数据是根据TabControl切换而得到的,而每次上拉加载都刷新一定数量的数据,所以根据三个切换按钮在里面又定义了三个对象,而每次上拉加载都定义为加一页(默认第一页),所以定义page用来存储当前页数信息,用list来存储到当前页加载到的商品数量

        goods: {
   
          'pop': {
   page:0, list:[]},
          'new': {
   page:0, list:[]},
          'sell': {
   page:0, list:[]},
        }

首页数据的请求和封装

ntework里的home.js同第一个网络请求的函数一样,再次定义另外一个,获得商品数据

export function getHomeGoods(type,page) {
   
  return request({
   
    //接口已修改
    url: '/api/h8/home/data',
    params: {
   
      type,
      page
    }
  })
}

在home.vue调用该函数,并将其封装成方法,因为我们在网络请求后会做进一步的处理,所以我们用同名的函数将其封装起来,并在create()中调用这些网络请求以及具体的实现方法

    created() {
   
      this.getHomeMultidata()
      this.getHomeGoods('pop')
      this.getHomeGoods('new')
      this.getHomeGoods('sell')
    },
    methods: {
   
      //网络分析相关方法
      getHomeMultidata() {
   
        getHomeMultidata().then(res => {
   
          this.banners = res.data.banner.list
          this.recommends = res.data.recommend.list
        })
      },
      getHomeGoods(type) {
   
        const page = this.goods[type].page + 1
        getHomeGoods(type, page).then(res => {
   
          this.goods[type].list.push(...res.data.list)
          this.goods[type].page ++
        })         
      }
    }

商品展示

因为涉及到该项目的业务功能(好几个页面会用到该组件),所以在components中的content中新建goods文件夹,新建两个vue文件GoodsList以及GoodsListItem,一个用来描述单个商品的详细样式,另外一个用来展示整体商品的样式

GoodsList.vue

<template>
  <div class="goods-list">
    <goods-list-item v-for="item in goods" :goods-item="item"></goods-list-item>
  </div>
</template>

<script>
  import GoodsListItem from './GoodsListItem.vue'
  export default {
    name: 'GoodsList',
    props: {
      goods: {
        type: Array,
        default() {
          return []
        }
      }
    },
    components: {
      GoodsListItem
    }
  }
</script>

<style scoped>
  .goods-list {
    display: flex;
    padding: 2px;  

     /* 包裹 */
     flex-wrap: wrap;
     justify-content: space-around;
  }
</style>

GoodsListItem.vue

<template>
  <div class="goods-list-item">
    <img :src="goodsItem.show.img" alt="">
    <div class="goods-info">
      <p>{
  {goodsItem.title}}</p>
      <span class="price">{
  {goodsItem.price}}</span>
      <span class="collect">{
  {goodsItem.cfav}}</span>
    </div>
  </div>
</template>

<script>
export default {
  name: 'GoodsListItem',
  props: {
    goodsItem: {
      type: Object,
      default() {
        return {}
      }
    }
  },
  components: {

  }
}
</script>

<style scoped>
  .goods-list-item {
    position: relative;
    padding-bottom: 40px;
    width: 48%;    
  }

  .goods-list-item img {
    width: 100%;
    border-radius: 5px;
  }

  .goods-info {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 5px;
    overflow: hidden;
    text-align: center;
    font-size: 12px;
  }

  .goods-info p {
    overflow: hidden;
    /*  显示省略符号来代表被修剪的文本 */
    text-overflow: ellipsis;
    /* 文本不换行 */
    white-space: nowrap;
    margin-bottom: 3px;
  }

  .goods-info .price {
    color: var(--color-high-text);
    margin-right: 20px;
  }

  .goods-info .collect {
    position: relative;
  } 

  .goods-info .collect::before {
    content: '';
    position: absolute;
    left: -15px;
    top: -1px;
    width: 14px;
    height: 14px;
    background: url("~assets/img/common/collect.svg") 0 0/14px 14px;
  }  
</style>

这里主要有几个css的新知识点(或者遗忘点)

/*  显示省略符号来代表被修剪的文本 */
text-overflow: ellipsis;
/* 文本不换行 */
white-space: nowrap;

/* 包裹 在子组件定义宽度后,可以根据宽度自动排列布局*/
flex-wrap: wrap;
/* 可以使子组件的间距均匀 */
justify-content: space-around;

tabcontrol切换商品类型

回到tabcontrol,我们需要通过点击事件来切换不同的商品展示,所以这里就要动态绑定点击事件,同时,为了触发父组件的goods信息改变,我们需要将该事件点击所对应的tab的索引传给父组件

动态绑定事件

    <div v-for="(item,index) in titles" 
    class="tab-control-item"
    :class = "{active: index === currentIndex}"
    @click="itemchange(index)">

传出事件以及当前点击的索引

    methods: {
   
      itemchange(index) {
   
        this.currentIndex = index
        this.$emit('tabClick', index)
      }
    }

父组件接收点击事件

    <tab-control :titles="['流行', '新款', '精选']" class="tab" @tabClick="tabClick"></tab-control>

用一个变量获得当前请求的数据索引(默认首页)

    data () {
   
        currentType: 'pop'
    }

处理该事件

    methods: {
   
      //首页数据处理
      tabClick(index) {
   
        switch (index) {
   
          case 0:
            this.currentType = 'pop'
            break
          case 1:
            this.currentType = 'new'
            break
          case 2:
            this.currentType = 'sell'
            break
        }
      }
    }

再在computed中,动态改变当前请求的数据索引

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

在标签,动态绑定数据

    <goods-list :goods="showGoods"></goods-list>

这样就OK啦

Better-scroll使用

官网简介

BetterScroll 是一款重点解决移动端(已支持 PC)各种滚动场景需求的插件。它的核心是借鉴的 iscroll 的实现,它的 API 设计基本兼容 iscroll,在 iscroll 的基础上又扩展了一些 feature 以及做了一些性能优化。

BetterScroll 是使用纯 JavaScript 实现的,这意味着它是无依赖的。

因为网页在PC端上使用主要是用鼠标滚轮进行的,不会很卡,而如果你换到手机端,进行页面滚动的话就会发现明显的卡顿,这个时候就需要引入插件(或者自己写一个)。这里使用的是better-scroll,下面分两部分介绍它的基本使用(html页面和vue项目)

html使用

npm下载

npm i better-scroll --save

或者GitHub下载

然后找到dist文件夹,将该文件拖出来,你也可以直接js引用到该文件

  <script src="./better-scroll.min.js"></script>

使用该插件之前需要了解一下知识

你需要将加入到滚动的标签统一放在一个div(其它单独的双标签元素也可)下,之后在此基础下再加上一个div标签进行包裹,如下图所示

原理图

页面结构代码演示如下(你可以加上更多’汉堡包’,拖动体验感更佳),使用wrapper包裹content,再加上你需要滚动的元素

  <div  class="wrapper">
    <ul class="content">
      <li>🍔</li>
      <li>🍔</li>
      <li>🍔</li>
      <li>🍔</li>
      <li>🍔</li>
      <li>🍔</li>
      <li>🍔</li>
      <li>🍔</li>
      <li>🍔</li>
      <li>🍔</li>
      <li>🍔</li>
      <li>🍔</li>
      <li>🍔</li>
      <li>🍔</li>	
    </ul>
  </div>

接下来就是具体的js逻辑实现了

首先我们要new一个better-scroll,我把它称为better-scroll初始化

这里,需要添加配置项

probeType

  • 类型number
  • 默认值0
  • 可选值1|2|3
  • 作用:有时候我们需要知道滚动的位置。当 probeType 为 1 的时候,会非实时(屏幕滑动超过一定时间后)派发scroll 事件;当 probeType 为 2 的时候,会在屏幕滑动的过程中实时的派发 scroll 事件;当 probeType 为 3 的时候,不仅在屏幕滑动的过程中,而且在 momentum 滚动动画运行过程中实时派发 scroll 事件。如果没有设置该值,其默认值为 0,即不派发 scroll 事件

click

  • 类型boolean
  • 默认值false
  • 作用:BetterScroll 默认会阻止浏览器的原生 click 事件。当设置为 true,BetterScroll 会派发一个 click 事件,我们会给派发的 event 参数加一个私有属性 _constructed,值为 true。

pullUpLoad

  • 类型: boolean
  • **默认值:**false
  • **作用:**动态监测你是否滚动到最底部(在最新版,你已经看不到该配置项了,需要通过引入插件去使用,当然你也可以通过下载完整版去使用该插件)

click这里要补充一下,它对于本来就该具有的点击事件的元素是不会阻止的(button),它会阻止divimg等标签的点击事件,其它配置项可以去官网查看并试试

	let bs = BetterScroll.createBScroll(document.querySelector('.wrapper'), {
   
      probeType: 1,
      click: true,
      pullUpLoad: true
    })

然后你就可以试试滚动啦,你可以通过下列代码动态监测当前滚动的位置(position为当前所在坐标),然后做出对应处理(前提是必须probeType为2或3)

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

pullingUp可以检测你是否到底了(前提pullUpLoad: true),但只能检测一次,下次滚动到最低就没有了,你可以通过better-scrollfinishPullUp()方法多次检测

    bs.on('pullingUp', () => {
   
      console.log('你已经拉到底了')

      bs.finishPullUp()
    })

vue使用

这个用npm下载完使用会方便点(使用CLI4创建vue项目)

页面结构要求同html的使用方式

引入

  //添加scroll插件
  import BScroll from 'better-scroll'

在vue的生命周期函数mounted中去使用该插件,不能在created中使用,为什么呢,因为它刚初始化完,那些元素标签还没加载,直接用就会出现undefined或者null,同时,因为mounted的函数执行完就会boom的没了(函数栈还是内存的栈和堆的关系),所以你需要定义一个属性去接收你new出来的betterScroll对象

  export default {
   
    name:'Category',
    data() {
   
      return {
   
        bs: null
      }
    },
    mounted() {
   
      this.bs = new BScroll(document.querySelector('.wrapper'), {
   
        probeType: 3,
        pullUpLoad: true //该属性添加后,probeType的值直接为3(修改成别的也没用)
      })
      this.bs.on('scroll', (position) => {
   
        console.log(position)
      })
      this.bs.on('pullingUp', ()=> {
   
        console.log('达到最低了')
        this.bs.finishPullUp()
      })
    }
  }

好好享用吧

封装better-scroll

B站老师有着疯狂的封装想法,哈哈哈开玩笑,其实是为了项目的后期更新和维护,我们使用插件前一般都是将其封装完后再去各个组件中使用,避免后面插件不维护更换插件引起的代码修改困难(可能要面临重构,很苦的)

根据上面所学的better-scroll,我们为其定义所需的基本结构,后面在使用的时候就直接往插槽添加标签即可

<template>
  <div class="wrapper" ref="wrapper">
    <div class="content">
      <slot></slot>
    <
  • 13
    点赞
  • 75
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值