Vue练手项目:PC手机商城

本项目是本人2018年学习vue的练手项目,此文记录项目练习过程中的的一些细节和难点。(文章最后更新时间:2018/2/17)

本文目录

  • 1.创建项目
  • 2.项目文件夹
  • 3.MOCK数据
  • 4.设置代理服务器
  • 5.src文件夹
  • 6.配置路由
  • 7.全局使用css样式文件
  • 8.创建和使用vHeader组件
  • 9.在组件中发请求拿数据
  • 10.鼠标悬停展现对应数据
  • 11.使用动画插件操作v-for循环出的元素
  • 12.轮播图组件
  • 13.主图组件
  • 14.筛选框组件
  • 15.排序组件
  • 16.推荐组件
  • 17.通过vuex响应式操作全选框

1.创建项目

注意:本项目的构建是基于vue-cli2.x版本进行的
去要创建项目的文件夹下执行命令
vue init webpack shoppingMall
执行之后系统会自动远程下载项目模板文件,
接下来会出现一些选填项
因为我们的项目都是写在.vue文件中的,所以编译器选择用Runtime-only即可
如果我们用的是less来写样式代码的话,需要手动去安装less和less-loader
npm install less less-loader --save-dev
项目的启动命令是在package.json文件中的scripts中的start配置的
手动安装axios
npm install axios --save

2.项目文件夹

image.png

bulid文件夹=》存放的是webpack配置文件
config文件夹=》自己做项目时手动增加的一些对项目的配置文件,比如说项目运行端口、静态文件默认存放目录等等
node_modules=》第三方依赖
src=》项目的真正源码存放处
static=》静态资源文件夹

使用vue+webpack开发项目时,小图片可以不用使用精灵图,因为我们可以通过设置让小图片都转化成base64代码,从而使项目运行速度更快。

3.MOCK数据

在现在的SPA项目或者前后端分离项目中,前端人员都是通过ajax向后端获取数据的,现实中,前后端人员往往都是同时接到项目需求,这时候我们前端人员可以自己先通过mock数据来模拟API获得数据。
1.在项目中新建一个mock-server文件夹,然后cd到文件夹中
2.通过npm init将mock-server初始化成一个npm项目,所有选填项都可以回车。
3.安装json-server模块npm install json-server --save
4.至此,我们的项目框架已有,此时mock-server文件中有三个文件,一个是node_modules文件夹,一个是package.json,另一个是package-lock.json
5.在项目文件下新建一个db.js文件
在文件中创建一个变量,用来存放我们的数据
const data = {}
别忘了将data导出
module.exports = data
6.创建一个server.js文件 =>这是我们的项目服务文件

//创建一个变量,引入json-server模块
const JsonServer = require('json-server')
//将data数据也引入进来
const data = require('./db.js')
//通过json-server的create方法来实例化一个server服务器
const server = JsonServer.create()
//用json-server自带的router方法,并依赖data来创建一个router中间件
const router =JsonServer.router(data)
//创建middleware中间件
const middleware = JsonServer.defaults()
//使用中间件
server.use(middleware)
server.use(router)
//这个中间件是专门用来解析请求体的
server.use(JsonServer.bodyParser)
//启动服务
server.listen({
    host:'127.0.0.1',
    port:'8858'
},function(){
    console.log('json-server is running on 8858...')
})

7.刚才创建的db.js中的data是没数据的,接下来我们就把事先准备好的数据放入data对象中
8.接下来通过node server就可以启动服务
9.通过浏览器对应地址就可以对mock-server项目进行访问,其中的Resources就是数据的接口API地址

4.设置代理服务器

mack数据情况下解决跨域的方法
在项目目录中的config文件夹中的index.js中=》

module.exports = {
    dev:{
        proxyTable: {
            '/api':{
                 target:'http://localhost:8858',
                   changeOrigin:true,
                   pathRewrite:{
                        '/api':'/'
                      }
                 }
           }
    }
}

这样的话,直接在项目中用axios发请求就可以了,API接口前面加上/api

5.src文件夹

首先是assets文件夹,这文件是专门用来存放我们项目所需的静态资源的,为了分类,我们在assets文件夹中新建几个文件夹,如css、fonts、images

  • 一般情况下我们需要在css中先存放一个reset.css用来重置一个样式
  • 将从字体网站下载下来的图标文件都放入到fonts文件件中
  • 将项目所需图片都放入到images中

components文件夹是用来存放组件的,项目模块最开始展现的内容基本都是HelloWorld.vue展现出来的,图标和一些css样式是在App.vue中
router文件夹是用来存放路由配置的
我们再手动创建一个view文件夹,用来存放页面文件
App.vue是项目的根组件
main.js是项目的入口文件,这里面的配置都是全局生效的

6.配置路由

在router文件夹中的index.js中将HelloWorld组件的引入改成我们在view文件夹中新建的index.vue

import Index from '../view/index'
export default new Router({
  routes: [
    {
      path: '/',
      name: 'Index',
      component: Index
    }
 }

路由里面对页面的引入和使用,我们可以选择使用开头字母大写的形式
接来下所有页面都是现在view文件夹中创建页面文件,然后在router文件夹中的index.js进行引入和使用,然后就可以在App.vue中的<router-view/>进行展现。

7.全局使用css样式文件

直接引用在main.js中引用,就可以全局生效

//大神写好的清除浏览器自带样式的文件
import './assets/css/reset.css'
//字体的样式文件
import './assets/fonts/style.css'
//我们自己手写的一些想要全局使用的样式文件
import './assets/css/common.css'

字体图标的使用:
全局引用style.css文件后,我们在要使用字体图标的标签中添加指定样式即可,如span标签
<span class="icon-font icon-xxx"></span>

8.创建和使用vHeader组件

首先,在conponents文件夹中创建vHeader.vue
然后在index.vue页面文件中引入vHeader组件
import vHeader form '@/components/vHeader.vue'
进行注册

components:{
    vHeader
}

接下来在页面的template标签中的对应位置就可以通过<v-header></v-header>进行使用。

9.在组件中发请求拿数据

以刚创建的vHeader组件为例
在组件中引入axios
import axios from "axios";
先在data中定义一个变量

  data() {
    return {
      navData: [],
    }
  }

发请求的方法

methods: {
  async getNavDate() {
     const { data } = await axios.get("./api/nav");
     this.navData = data
  }
}

在合适的地方调用方法

mounted() {
   this.getNavDate();
}

10.鼠标悬停展现对应数据

首先给对应的元素绑定事件
<li v-for="(item,index) in navData" :key="index" @mouseenter="showChildrenData(item)"> </li>
当鼠标悬浮在上面的里的时候,触发showChildrenData,获取对应item下面的数据

showChildrenData(item) {
   this.childrenData = item.children
}

然后再将数据渲染到悬浮数据展示框中

<li class="children-item" v-for="(item,index) in childrenData" :key="item.pic" :data-index = 'index'>
      <img :src="item.pic">
      <p>{{item.name}}</p>
      <p>{{item.price}}</p>
</li>

11.使用动画插件操作v-for循环出的元素

vue提供了一个<transition-group></transition-group>来对v-for循环出来的元素进行动画操作

<transition-group tag="ul" @enter="enter">
    <li class="children-item" v-for="(item,index) in childrenData" :key="item.pic"  :data-index = 'index'>
              <img :src="item.pic">
              <p>{{item.name}}</p>
              <p>{{item.price}}</p>
    </li>
</transition-group>

注意:在这里li的key值绑定的不是index,因为如果绑定index的话,多个悬浮列表对应的数据都是相同的,vue经过检测后发现key相同,就不会灵敏的渲染悬浮列表的数据,所以我们要给key值绑定更加唯一化的数据
@enter触发的enter事件

enter(el){
}

事件函数的参数el就代表v-for循环的每一项元素
怎么去操作我们的el呢,我们这里选择用js的方法,比如用velocity
npm install velocity-animate --save
在页面引用
import Velocity from 'velocity-animate'
接下来我们就可以用velocity提供的方法来写动画了

 enter(el){
   const timeOut = el.dataset.index*100
   setTimeout(function () {
     Velocity(el,{
       'opacity':1,
       'translateX':'-50px'
     })
   },timeOut)
 }

悬浮列表的隐藏与显示我们是设置一个flag来通过v-show控制,另外悬浮列表的隐藏也应该加上一个数据的清空,这样下次再展现数据会重新渲染,动画才会重新做。

 gethidden() {
   this.isshow = false,
   this.childrenData= []
 }

12.轮播图组件

由于轮播图组件肯定会在项目的多个地方使用,同时轮播图的数据、宽度、高度、延迟时间在每个地方肯定都不同,所以我们在轮播图组件swiper.vue中设置一个props用来接收父组件传递过来的数据。

props:{
    data:{
        type:Array,
        default(){
            return[]
        }
    },
    height:{
        type:Number,
        default:500
    },
    width:{
        type:Number,
        default:1240
    },
    delay:{
        type:Number,
        default:1500
    }
}

swiper.vue组件的template代码

<template>
  <div class="swiper" :style="swiperSize">
    <ul :style="listWrapper" @transitionend = 'setDuration' class="swiperul">
      <!-- 因为data默认是个空数组,所以这里我们要设置拿到数据之后再去渲染 -->
      <li class="swiper-list" v-for="(item,index) in data"  v-if="data.length > 1" :key="data.length+1">
        <a :href="item.href">
          <img :style="swiperSize" :src="item.imgUrl" alt>
        </a>
      </li>
    </ul>
    <ul class="swiper-pagination">
      <li v-for="(item,index) in data" :key="index" @click = 'changeImg(index)' :class="{'active':activeIndex === index}"></li>
    </ul>
  </div>
</template>

父组件中引入和调用swiper组件
<swiper :data="swiperData"></swiper>
组件中通过computed来设置一个swiperSize,并将其动态绑定到对应的元素上,父组件没有传对应的值过来,就取props里的默认值,传值过来了,就以传值为准

computed:{
    swiperSize () {
        return {
            width: `${this.width}px`,
            height:`${this.height}px`
        }
    }

在data中设置一个动态的值来记录我们现在滚动到哪个图片了
activeIndex:0,
设置一个listWrapper来动态的计算整个轮播图大容器的宽度、高度以及位移距离

listWrapper(){
    return{
        width:`${(this.data.length+1) * this.width}px`,
        height:`${this.data.height}px`,
        transform:`translateX(-${this.width * this.activeIndex}px)`,
        transitionDuration:this.haveDuration ? '.3s' : ''
    }
}

将listWrapper绑定到轮播图的大容器ul上之后我们手动修改activeIndex就会发现图片发生了改变。接下来我们需要设置定时器来实现图片的自动播放。
首先在data定义一个命名定时器timertimer:null,
去methods定义方法

setTimer(){
    clearInterval(this.timer)
    this.timer = setInterval( ()=>{
        if(this.activeIndex === this.data.length){
            this.activeIndex =0
            this.haveDuration = false
        }else{
            this.activeIndex ++
            this.haveDuration = true
        }
    }
   ,this.delay )
}

在mounted挂载上面这个方法

mounted(){
    this.setTimer()
    haveDuration:true
}

haveDuration:true这个变量是用来控制过渡动画时间的阈值
至此,轮播图已经实现了自动切换,但还有很多功能没有完善
如:点击小圆点,切换到对应图片,其实就是给小圆点添加点击事件,点击对应的点,传递圆点所在的index,并赋值给activeIndex
小圆点在v-for循环时所对应的index就是我们需要的数据
优化:当轮播图自动播放到最后一张时,再回到第一张时会有一个快速闪动的画面,给人的感觉不是很友好。
比如说图片原来有五张,我们写死第六张,内容是和第一张是一样,当图片轮播到第六张时,我们取消掉过渡动画时间,然后快速切换到第一张,这时候再恢复过渡动画。
···
<li class="swiper-list" v-if="data.length > 1" :key="data.length+1">
<a :href="data[0].href">
<img :style="swiperSize" :src="data[0].imgUrl" alt>
</a>
</li>
···
当元素每次动画做完都会触发一个transitionend方法,给图片容器ul绑定事件<ul :style="listWrapper" @transitionend="setDuration" class="swiperul">,当图片播放完最后一张时,我们将阈值改为false,停掉过渡动画,并且将activeIndex改为0

setDuration(){
    if(this.activeIndex === this.data.length){
        this.activeIndex = 0
        this.haveDuration = false
    }
}

13.主图组件

需求:上面一张显示的大图,下面有五个小的缩略图,点击对应的缩略图会显示对应的大图

组件在不同地方使用的时候,样式肯定会有所不同,所以我们可以选择给组件写几套不同的样式,然后根据父组件传递过来的数据来确定使用哪套数据

props:{
      imgdata:{
          type:Array,
          default(){
              return [];
          }
      },
      type:{
          type:String,
          default:'small'
      }
  }

imgdata是组件所用的数据,type决定了组件使用哪一套样式。
html结构:

<div :class="{'img-wrapper':type === 'small','img-wrapper-big':type === 'big'}">
<img class="img" :src="data[activeIndex]" alt="">
<ul class="imgs">
  <li class="imgs-item" :class="{'active':activeIndex === index}" v-for="(item,index) in data" @click="changeImage(index)" :key="index">
    <img :src="item" alt="">
  </li>
</ul>
</div>

父组件引用的时候,传递对应type值就可以实现不同样式的切换了。
<imglist :data="detailData.colorImageUrls" :type="big"></imglist>

14.筛选框组件

因为此处的筛选涉及到多个条件,在这里不能简单的只是给对应点击的index加高亮样式,而是要创建一个对象,选择的数据要动态的加入到这个对象中,我们先在data中定义一个activeFilter: {}
给每一个可以点击选择的项添加点击事件,将其key值和value值传递给事件
@click="changeFilter(item.key,info.value)"
将值赋值给activeFilter

changeFilter(key, val) {
    this.$set(this.activeFilter, key, val);
    this.$emit("filter", this.activeFilter);
}

其中this.emit('filter',this.activeFilter)把选择好的值发射给父组件,父组件可以根据拿到的筛选数据进行筛选。 注意:通过点击事件拿到的值不能这样赋给activeFilter,这样vue检测不到数据的改变,应该使用vue提供的this.set
父组件将引用组件,传递数据data并且接受子组件传递过来的已选中的筛选数据activeFilter
<filterBox :data="fiterBoxData" @filter="getQuery"></filterBox>
父组件的商品列表数据中有一项是features,专门存储商品的各项属性的,我们可以根据此属性进行过滤,最终把符合条件的数据筛选出来。但是每次筛选都会把原始数据给搞乱,所以我们在父组件一开始拿到完整的商品列表数据的时候就拷贝一份就复制一份数据,这里不能是简单的等号,否则会形成引用关系
this.categoryListCopy = [].concat(data);
自定义事件filter触发了getQuery事件

getQuery(val) {
  this.currentQuery = val;
  this.sortGoods();
}

我们之所以在这里设置了 this.currentQuery = val以及把筛选代码移到sortGoods方法里,是因为我们需要对currentQuery做更多业务逻辑的处理。
下面是sortGoods方法的代码(这个方法会集中存放筛选和排序商品的代码)

sortGoods() {
  // 在每次筛选数据之前,我们都把备份数据再复制给categoryListData
  this.categoryListData = [].concat(this.categoryListCopy);
  // 当currentQuery存在的时候我们再进行过滤;
  if (this.currentQuery) {
    // 拿到Object.keys(val)的每一项key,形成一个数组
    Object.keys(this.currentQuery).forEach(key => {
      if (this.currentQuery[key]) {
        this.categoryListData = this.categoryListData.filter(item => {
          return item.features.indexOf(this.currentQuery[key]) >= 0;
        });
      }
    });
  }
  if (this.currentStock) {
    this.categoryListData = this.categoryListData.filter(item => {
      return item.available;
    });
  }
  if (this.currentKey) {
......
  }
},

Object.keys(obj) :
参数:要返回其枚举自身属性的对象
返回值:一个表示给定对象的所有可枚举属性的字符串数组

15.排序组件

排序组件中点击高亮的效果和点击选定的值都是存储在data中的activeSortKey中activeSortKey: "",
每一个a点击都会触发改变activeSortKey值的事件,并且高亮效果动态绑定
<a href="javascript:;" :class="{'active':activeSortKey === 'recommend'}" @click="setSortKey('recommend')" >推荐</a>
setSortKey事件还负责把选好的activeSortKey值发射给父组件

setSortKey(val) {
  this.activeSortKey = val;
  this.$emit("getKey", this.activeSortKey);
}

父组件根据自定义事件拿到值

getSortKey(key) {
  this.currentKey = key;
  this.sortGoods();
}

然后进行排序,现在sortGoods多了以下代码

sortGoods() {
    if (this.currentKey) {
    if (this.currentKey === "recommend") {
      this.categoryListData.sort((a, b) => {
        return b.shelveTime - a.shelveTime;
      });
    } else if (this.currentKey === "new") {
      this.categoryListData.sort((a, b) => {
        return b.publishedTime - a.publishedTime;
      });
    } else if (this.currentKey === "low") {
      this.categoryListData.sort((a, b) => {
        return b.goodsPrice - a.goodsPrice;
      });
    } else if (this.currentKey === "high") {
      this.categoryListData.sort((a, b) => {
        return a.goodsPrice - b.goodsPrice;
      });
    }
  }
}

排序的后面有个上下箭头,点击可以切换
a标签中的代码
<a href="javascript:;" @click="changePrice" :class="{'active':activeSortKey === 'low' || activeSortKey === 'high'}" > 价格<i class="icon-font arrow" :class="sortArrow"></i></a>
a标签click触发的changePrice事件

changePrice() {
  if (this.activeSortKey === this.activePrice) {
    this.activePrice = this.activeSortKey === "low" ? "high" : "low";
  }
  this.setSortKey(this.activePrice);
}

箭头绑定的sortArrow是个computed属性

sortArrow() {
  if (this.activePrice === "low") {
    return "icon-down";
  } else {
    return "icon-up";
  }
}

仅显示有货商品:在checkbox上v-model有一个值ifchecked,设置这个ifchecked值初始为false,默认点击就可以自动实现true和false之间的切换(选中时是true,未选中时是false),这时候watched这个值的变化,就可以实现watched里面逻辑代码的点击触发。

16.推荐组件

最外面的盒子宽度是1240px,margin:0 auto,里面的ul的宽度是在computed根据数据的长度动态计算出来的。

computed: {
   listWrapper() {
     return {
       width: `${this.data.length * 250}px`,
       transform:`translateX(-${this.activeIndex * 1240}px)`,
       transitionDuration :`.3s`
     };
   }
}

给左右翻页的箭头动态的绑定disable属性
左箭头<i class="icon-font icon-left pagination-item" @click="prev" :class="{'disabled':activeIndex === 0}"></i>,右箭头<i class="icon-font icon-right pagination-item" @click="next" :class="{'disabled':activeIndex === pageSize}"></i>
prev和next的事件代码:

prev(){
  if(this.activeIndex === 0) return
  this.activeIndex -= 1
},
next(){
  if(this.activeIndex === this.pageSize) return
  this.activeIndex += 1
}

17.通过vuex响应式操作全选框

首先在vuex中的getters中设置一个值

isAllChecked(state){
    let checked = true
    state.shopcartData.forEach(item =>{
       if(!item.checked){
           checked = false
       }
    })
    return checked
}

只要数据中有任何一个是false,isAllChecked都会变成false
在购物车界面引入getters(语法糖)
import { mapGetters } from 'vuex'
挂载到computed中

computed: {
   ...mapGetters([
       'isAllChecked',
   ])
}

接下来绑定到input全选框中
<input type="checkbox" :checked="isAllChecked" > <span>全选</span>
接下来实现:点击全选框,改变所有商品的选择状态
首先在mutations中设置一个方法

CHECKED_ALL_GOODS(state,checked){
   state.shopcartData.forEach(item =>{
       item.checked = !checked
   })
}

在购物车界面引入mutations(语法糖)
import { mapMutations } from 'vuex'
挂载到methods中

...mapMutations([
   'CHECKED_ALL_GOODS'
])

将点击事件绑定到input全选框中
<input type="checkbox" class="cart-checkbox" :checked="isAllChecked" @click="checkAllGoods"> <span >全选</span>
点击事件触发的checkAllGoods代码

checkAllGoods(){
     this.CHECKED_ALL_GOODS(this.isAllChecked)
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容