【练习】基于Vue全家桶的仿小米商城系统


前言: 本文是对整个练习过程的记录,记录重点知识以及不太了解的知识。

1. 项目概述

本次练习是基于Vue全家桶的仿小米商城系统,商城的流程如下:

  • 登录 -> 产品首页 -> 产品站 -> 产品详情
  • 购物车 -> 订单确认 -> 订单支付 -> 订单列表

总共有上面的八个页面,还有若干个组件。

商城系统整体架构图:
在这里插入图片描述

2. 项目基础架构

2.1 跨域及解决

跨域时浏览器为了安全而做出的限制策略,浏览器请求必须遵循同源策略:同域名、同端口、同协议。

这里使用接口代理的方式来解决跨域问题:

接口代理就是通过修改Nginx服务器配置来实现(前端修改,后台不变)

在根目录创建配置文件:vue.config.js,在里面配置以下内容:

module.exports = {
  devServer:{
    host:'localhost',
    port:8080,
    proxy:{
      '/api':{
        target:'url',
        changeOrigin:true,
        pathRewrite:{
          '/api':''
        }
      }
    }
  }
}
// 注意:在target里面需要写上接口代理的目标地址,这里就不写了,用url代替

原理: 因为我们需要使用的接口地址可能很多,不可能挨个去进行拦截。所以,这里可以设置一个虚拟的地址/api,实际上,是没有这个地址的。当拦截到/api时,就将主机的点设置为原点(changeOrigin:true),然后添加路径的转发规则,将/api 置为空,转发时就没有/api

2.2 项目目录结构

  • api: 对api的一些处理
  • util:对公共的方法的定义、封装
  • store:使用Vuex的目录
  • pages:项目页面文件
  • storage:数据储存相关
  • assets:小图片、样式文件等
  • components:组件
    在这里插入图片描述

2.3 插件的安装

  • vue-lazyload :懒加载
  • element-ui :Element UI
  • node-sass :Sass
  • sass-loader: Sass加载
  • vue-awesome-swiper :首页轮播
  • vue-axios : 结合使用axios
  • vue-cookie :Cookie
    在这里插入图片描述

需要注意的是:axios是一个库,并不是vue中的第三方插件,所以使用时需要在每个页面进行导入操作,这样就很麻烦。我们可以使vue-axiosaxios的作用域对象挂载到vue实例中,这样就可以在需要使用的时候用this来调用。

Vue.use(VueAxios, axios)

2.4 storage封装

Cookie、localStorage、 sessionStorage三者 区别?
这个问题可以参考之前写的一个总结:链接地址

storage本身虽然有API,但是只是简单的key/value形式,storage只能存储字符串,需要手工转化为json对象,并且storage只能一次性的清空,不能进行单个的清空,所有我们需要对storage进行封装。

这里封装的是sessionStorage,实际上就是可以在sessionStorage存储JSON对象,并且可以对这些对象进行一些操作:

// 设置一个key
const  STORAGE_KEY = 'mall';
export default{
  // 存储值
  setItem(key,value,module_name){
    if (module_name){
      // 如果模块名称存在,就递归找到这个模块,然后给这个模块设置key和value
      let val = this.getItem(module_name);
      val[key] = value;
      // 最后将设置的值存储到整个数据中
      this.setItem(module_name, val);
    }else{
      // 如果模块名称不存在,就直接进行设置,并存储在sessionStorage中
      let val = this.getStorage();
      val[key] = value;
      window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(val));
    }
  },
  // 获取某一个模块下面的属性
  getItem(key, module_name){
    // 如果模块的名称存在,就获取模块的名称,并将返回该模块中某个key的value值
    if (module_name){
      // 这里是不断往内层遍历,直到寻找到那个模块
      let val = this.getItem(module_name);
      if(val) {
        return val[key];
      }
    }
    // 如果模块名称不存在,就直接返回该key的value值
    return this.getStorage()[key];
  },
  // 获取Storage的信息
  getStorage(){
    // 获取sessionStorage中的整个数据,并将其转化为对象的形式
    return JSON.parse(window.sessionStorage.getItem(STORAGE_KEY) || '{}');
  },
  // 清空某一个值
  clear(key, module_name){
    // 首先要获取到整个对象的值
    let val = this.getStorage();
    // 如果这个模块存在
    if (module_name){
      // 如果这个模块的值为空,就直接返回
      if (!val[module_name])return;
      // 否则就删除这个模块中的key对应的值
      delete val[module_name][key];
    }else{
      // 如果模块不存在,就说明key就在第一层,直接删除key
      delete val[key];
    }
    // 删除之后,将删除之后的值设置到sessionStorage中
    window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(val));
  }
}

举个例子来解释一下上面的模块的概念:

mall = {
  'a': 1,
  'b':{
    'c': 2,
    'd':{
      'e': 3
    }
  }
}

在这个JSON对象中,如果我们想找到e,就需要进行递归,找到d模块中的key(也就是e),然后取出他的值。

2.5 接口错误拦截

对于接口请求,我们要将错误进行统一处理:

  • 统一报错
  • 未登录统一拦截
  • 请求值、返回值统一处理
// 由于使用的是接口代理的方式进行跨域,所以这里baseURL设置为/api,超时时间设置为8s
axios.defaults.baseURL = '/api'
axios.defaults.timeout = 8000

// 接口错误拦截,根据接口返回状态码,来进行不同的处理(状态码是后台设置的)
axios.interceptors.response.use(function(response){
  let res = response.data
  let path = location.hash
  
  if(res.status == 0){
    return res.data
  }else if(res.status == 10){
    if(path !== '#/index'){
      window.location.href = '/#/login'
    }
  }else{
    alert(res.msg)
    // 抛出异常,避免返回的错误信息进入成功的结果中
    return Promise.reject(res)
  }
})

2.6 Mock设置

在开发阶段,我们可能还不能拿到API文档,所以可以使用Mock模拟数据来进行数据的交互操作。Mock有以下特点:

  • 开发阶段,为了提高效率,需要提前Mock
  • 减少代码冗余,灵活插拔
  • 较少沟通,减少接口联调时间

使用mock的方法有很多:

  • 本地创建json:在本地创建json文件,然后进行调用
  • easy-mock平台:将baseURL设置为easy-mock的接口地址,调用时和正常调用一样
  • 集成Mock API

(1)首先要安装mockjs:npm install mockjs --save-dev
(2)在src中建Mock的API:src/mock/api.js

import Mock from 'mockjs'
Mock.mock('/api/user/login', {
 //接口数据...
})

(3)之后在main.js设置一个mock的开关:

const mock = true
if(mock){
  require('./mock/api')
}

需要注意的是requireimport是不同的,import是编译的时候就进行加载,而require是执行到这句代码的时候才执行。

3. 商城首页的实现

(1)由于在布局时,很多地方出现了代码的重复,所以可以建一个mixin文件,来定义一些css函数,再在样式中引用。例如,我们多次使用到了flex布局,多次使用到了背景图片的设置,可以定义一个函数(定义函数时,可以设置一些默认值):

@mixin flex($hov:space-between,$col:center){
  display:flex;
  justify-content:$hov;
  align-items:$col;
}
@mixin bgImg($w:0,$h:0,$img:'',$size:contain){
  display:inline-block;
  width:$w;
  height:$h;
  background:url($img) no-repeat center;
  background-size:$size;

/*使用定义的函数*/
@include bgImg(18px,18px,'/imgs/icon-search.png');
@include flex();

注意:

  • 不传参数就意味着使用默认值。
  • 使用之前要引入mixin.scss文件

(2)因为使用到的字体大小、颜色值有很多重复的,所以可以建立一个config.scss文件,来定义一些常用的字体大小和颜色值:

在这里插入图片描述

使用:

color:$colorA;
font-size: $fontA;

(4)首页轮播图使用的是swiper,但是在编译报错"Can’t resolve ‘swiper/dist/css/swiper.css’",经查,是因为swiper版本过高的问题,在安装vue-awesome-swiper的时候,会自动安装一个swiper。默认swiper是最高版本,但是我们此时使用的不是最高版本。最后的解决方法是,重新安装指定的vue-awesome-swiper和swiper,问题就解决了:

npm install swiper vue-awesome-swiper@3.1.3 --save-dev
npm install swiper swiper@3.4.2 --save-dev

(4)在首页,总共使用到了四个组件:头部导航栏、底部信息栏、下方服务条、弹窗组件。因为这些组件不仅会在首页使用,还会在其他的页面使用,所以把他们都拆分出来,在需要的时候进行引用。这里说一下弹窗组件。

弹窗组件总共分为三部分:左上角的弹窗标题,中间的弹窗内容,下方的按钮。因为在每个页面中使用的弹窗可能不太一样,所以要把每一部分都定义成活的,便于修改,中间的内容区域定义成插槽。

弹窗结构:

<template>
  <transition name="slide">
    <div class="modal" v-show="showModal">
      <div class="mask"></div>
      <div class="modal-dialog">
        <div class="modal-header">
          <span>{{title}}</span>
          <a href="javascript:;" class="icon-close" @click="$emit('cancel')"></a>
        </div>
        <div class="modal-body">
          <slot name="body"></slot>
        </div>
        <div class="modal-footer">
          <a href="javascript:;" class="btn" v-if="btnType==1" @click="$emit('submit')">{{sureText}}</a>
          <a href="javascript:;" class="btn" v-if="btnType==2" @click="$emit('cancel')">{{cancelText}}</a>
          <div class="btn-group" v-if="btnType==3">
            <a href="javascript:;" class="btn" v-on:click="$emit('submit')">{{sureText}}</a>
            <a href="javascript:;" class="btn btn-default" @click="$emit('cancel')">{{cancelText}}</a>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>

数据传值: 这里是父组件向子组件传值,使用props接收。

 export default {
    name: 'modal',
    props: {
      // 弹框类型:小small、中middle、大large、表单form
      modalType: {
        type: String,
        default: 'form'
      },
      // 弹框标题
      title: String,
      // 按钮类型: 1:确定按钮 2:取消按钮 3:确定取消
      btnType: String,
      sureText: {
        type: String,
        default: '确定'
      },
      cancelText: {
        type: String,
        default: '取消'
      },
      showModal: Boolean
    }
  }

首页使用弹窗:

    <modal 
      title="提示" 
      sureText="查看购物车" 
      btnType="1" 
      modalType="middle" 
      :showModal="showModal"
      @submit="goToCart"
      @cancel="showModal=false"
      >
      // 新版本的vue插槽必须使用template来包含内容,这里使用的是具名插槽
      <template v-slot:body>
        <p>商品添加成功!</p>
      </template>
    </modal>

这里对使用Vue的过渡动画来实现弹窗的过渡效果:

(这里就只写一下过渡效果的代码)

.modal{
  @include position(fixed);
  z-index: 10;
  transition: all .5s;
  &.slide-enter-active{
    top:0;
  }
  &.slide-leave-active{
    top:-100%;
  }
  &.slide-enter{
    top:-100%;
  }
}

需要注意的是,使用动画的部分必须要用<transition>标签进行包裹,在定义动画的时候,在以下类名中定义:

  • v-enter:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。

  • v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。

  • v-enter-to:2.1.8 版及以上定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter 被移除),在过渡/动画完成之后移除。

  • v-leave:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。

  • v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。

  • v-leave-to:2.1.8 版及以上定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave 被删除),在过渡/动画完成之后移除。

对于这些在过渡中切换的类名来说,如果使用一个没有名字的 <transition>,则 v- 是这些类名的默认前缀。因为这里定义nameslide,所以以slide-开头。

(5)图片懒加载

适用于片的懒加载可以在一定程度上提高网页的性能。在vue中使用图片的懒加载还是比较简单的,来看一些具体的步骤:

  • 安装vue-lazyload插件
  • mian.js 中引入:import VueLazyLoad from 'vue-lazyload'
  • 注册并配置插件:
Vue.use(VueLazyLoad, {
  loading: '/imgs/loading-svg/loading-bars.svg'  // 设置了一个加载时的动画效果
})
  • 使用vue-lazyload:将:src换成v-lazy即可
// 原来
<img :src="item.mainImage">
// 懒加载形式
<img v-lazy="item.mainImage">

4. 登录页面的实现

对于登录功能,主要使用vue-cookie来储存登录信息,使得登录后保持登录状态。

vue-cookie的用法如下:

  • 安装vue-cookie插件
  • mian.js中引入并注册
import VueCookie from 'vue-cookie'
Vue.use(VueCookie) 
  • 使用vue-cookie
data () {
    return {
      username: '',
      password: '',
      userId: ''
    }
  },
methods: {
    login () {
      // 这里使用ES6的解构赋值来获取this中的两个值
      const { username, password } = this
      this.axios.post('/user/login', {
        username,
        password
      }).then((res) => {
        // 设置cookie值:将userId设置为res.id,并设置cookie的过期时间expires
        this.$cookie.set('userId', res.id, { expires: 'Session' })
        // 进行页面的跳转
        this.$router.push('/index')
      })
    }
 }

在登录完之后,控制台报错:Error: Avoided redundant navigation to current location:,这个报错显示是路由重复,虽然没有影响功能使用,但是看着很难受,所以就查了一些解决方案。

在引入VueRouter的时候加上下面代码就解决了:

import Router from 'vue-router'

Vue.use(Router)
const originalPush = Router.prototype.push
Router.prototype.push = function push(location) {
  return originalPush.call(this, location).catch(err => err)
}

5. Vuex的使用

Vue官网中关于Vuex使用的图示:
在这里插入图片描述我们需要在主页面显示用户的名称以及购物车商品的数量,这些数据需要登录状态下才显示。使用Vuex将获取到的的数据保存在store中,在需要的时候调用。

使用比较规范的目录定义形式:

src
 └──store
      ├── index.js          # Store实例化
      ├── state.js          # 存储共享的数据
      ├── actions.js        # 解决异步改变共享数据
      ├── mutations.js      # 用来注册改变数据状态
      └── getters.js        # 对共享数据进行过滤操作(本次未用到)
  • main.js中注册store
import store from './store'

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
  • index.js中将三个特性进行实例化:
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mutations from './mutations'
import actions from './actions'
Vue.use(Vuex);

export default new Vuex.Store({
  state,
  mutations,
  actions    
})

注意:mutationsactions不要写成mutationaction

  • state.js中定义要共享的数据:
export default {
  username: '',
  cartCount: 0
}
  • 触发异步:

触发actions:

this.axios.post('/user/login', {
        username,
        password
      }).then((res) => {
        this.$store.dispatch('saveUserName', res.username);
      })

当我们刷新页面时,发现刚刚获取到的数据又不能显示在页面上了。这是因为接口获取的接口还没有存储,所以需要在APP.vue中再次设置:

mounted(){
    this.getUser()
    this.getCartCount()
  },
methods:{
    getUser(){
            this.axios.get('/user').then((res) => {
                this.$store.dispatch('saveUserName', res.username);
            })
        },
    getCartCount(){
            this.axios.get('/carts/products/sum').then((res) => {
                this.$store.dispatch('saveCartCount', res);
            }) 
        }
  }

这样无论怎么刷新页面,数据都不会消失了。

  • actions:传输数据
export default {
  saveUserName (context, username) {
    context.commit('saveUserName', username)
  },
  saveCartCount (context, count) {
    context.commit('saveCartCount', count)
  }
}
  • mutations:存储数据
export default {
  saveUserName (state, username) {
    state.username = username
  },
  saveCartCount (state, count) {
    state.cartCount = count
  }
}
  • 使用数据
{{usrname}}
{{cartCount}}

computed:{
    username(){
      return this.$store.state.username
    },
    cartCount(){
      return this.$store.state.cartCount
    }
  }

这里使用到了computed计算属性,如果我们将这些数据直接定义在data中,他就是纯渲染,没有请求的时间。当我们进入APP.vue文件时,会执行两个接口请求,执行请求需要一定的时间,执行完获得数据之后,页面数据早已经渲染出来,渲染的值时请求之前的默认值,所以数据就不对了。

使用computed属性,当数据的值发生变化时,computed就会执行,来更新数据,这样就可以保证数据是正确的了。

6. 产品站的实现

(1)吸顶效果的实现

在商品详情页有一个顶部的信息组件,这个组件我们可以单独的定义成一个组件ProductParam,然后这组件有一个吸顶的效果,下面来记录一下实现的过程。

在这里插入图片描述

也就是上图中红色方框中的内容,在页面滚动到它的顶部的时候,就吸附在顶部,当滚动回来的时候,就还是原来的样子:
在这里插入图片描述
组件的结构:

  <div class="nav-bar" :class="{'is_fixed':isFixed}">
    <div class="container">
      <div class="pro-title">{{title}}</div>
      <div class="pro-param">
        <a href="javascript:;">概述</a><span>|</span>
        <a href="javascript:;">参数</a><span>|</span>
        <a href="javascript:;">用户评价</a>
        <slot name='buy'></slot>
      </div>
    </div>
  </div>

吸顶的实现:

data(){
    return {
      isFixed: false
    }
  },
  mounted(){
    // 监听页面的滚动事件
    window.addEventListener('scroll', this.initHeight)
  },
  destroyed(){
    // 销毁页面的滚动事件
    window.removeEventListener('scroll', this.initHeight)
  },
  methods: {
    initHeight(){
      // 定义事件的监听的内容
      let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
      this.isFixed = scrollTop > 152;
    }
  }

其中, pageYOffset 属性返回文档在窗口垂直方向滚动的像素。如果找不到就找滚动的距离,chrome中使用document.documentElement.scrollTop,IE浏览器使用document.body.scrollTop来定义。由于上面Header组件的高度为152px,所以只要滚动距离大于152,就给组件添加定位属性。

定位实现:

 .nav-bar{
    &.is_fixed{
      position: fixed;
      top: 0;
      width: 100%;
    }
 }

(2)视频动画实现

视频内容的基本结构:

 <div class="video-bg" @click="showSlide='slideDown'"></div>
 <div class="video-box" v-show="showSlide">
    <div class="overlay"></div>  // 遮罩层
    <div class="video" :class="showSlide"> // 视频盒子
       <span class="icon-close" @click="closeVideo"></span>  // 关闭按钮
       <video src="/imgs/product/video.mp4" muted autoplay  controls="controls"></video>  // 视频
    </div>
 </div>

可以是使用translation来实现,这里我们使用animation动画来实现一下,点击出现遮罩层,视频实现在屏幕正中央,点击关闭,视频划走,遮罩层消失。

 .video-box{
       // 定义进入的动画
       @keyframes slideDown{
         from{
           top:-50%;
           opacity:0;
         }
         to{
           top:50%;
           opacity:1;
         }
       }
       // 定义出去的动画
       @keyframes slideUp{
         from{
           top:50%;
           opacity:1;
         }
         to{
           top:-50%;
           opacity:0;
         }
       }
    .video{
         position:fixed;
         top:-50%;
         left:50%;
         transform:translate(-50%,-50%);
         z-index:10;
         width:1000px;
         height:536px;
         opacity:1;
         
         // 执行完动画之后, top还会变回-50%,所以需要我们手动设置为50%
         &.slideDown{
           animation:slideDown .6s linear;
           top:50%;
         }
         &.slideUp{
           animation:slideUp .6s linear;
         }
     }

animation三个参数分别是:动画的名称、动画的执行时间、进入的形式(这里是匀速进入)

有一个小问题就是,当关闭视频之后,视频整个盒子还在,所以要对其进行设置:

<div class="video-box" v-show="showSlide"></div>

// 点击关闭按钮,执行离开的动画,然后0.6s动画执行完,就将showSlide置为空,这样整个盒子就隐藏了
closeVideo () {      
      this.showSlide = 'slideUp'
      setTimeout(() => {
        this.showSlide = ''
      }, 600)
    }

还有一个小问题尚未解决,没有找到合适的方法,就是关闭视频实际上是将视频放在了我们看不到的地方,实际上视频依旧在播放着,没有暂停,需要手动设置进行暂停。之后看看有没有什么比较好的解决方案…

7. 退出功能的实现

退出功能的实现,需要考虑以下因素:

  • 退出后要清空顶部的用户名称
  • 退出后要清空购物车内商品的数量
  • 退出后要清空cookie值
  • 接口优化

(1)首显示定义退出功能的结构:

<a href="javascript:;" v-if="username" @click="logout">退出</a>

(2)逻辑实现

logout(){
      this.axios.post('/user/logout').then(() => {
        // 清空cookie,将cookie过期时间设置为-1,就是立刻失效
        this.$cookie.set('userId', '', {expires: '-1'})
        // 将Vuex中的用户名称和购物车商品数量进行初始化(清空)
        this.$store.dispatch('saveUserName', '')
        this.$store.dispatch('saveCartCount', '0')
        Message.success('退出成功')
      })
}

(3)当我们点击退出之后,虽然vuex中的数据清空了,这时购物车内数量显示为0,但是在我们重新登录之后显示购物车的数量还是显示为0。这是因为这个应用是单页面应用,从退出到登录,这只是单页面的跳转,并没有重新调用APP.vue这个入口文件,所以不会再请求购物车商品的数量。所以需要在首页中在含有退出功能的NavHeader组件中重新请求一次购物车内商品的数量,来让他显示。

但是这样的话,每次进入主页面都会记进行购物车数量请求,会影响性能,而我们只是想在登录之后才进行请求,所以可以在登录时设置一个参数,若主页面接收到这个参数,说明是从登录页面过来的,就进行数据请求:

getCartCount(){
        this.axios.get('/carts/products/sum').then((res=0) => {
            this.$store.dispatch('saveCartCount', res);
        }) 
 }
// 在 login中设置一个from参数吗,这里使用params传参,这样的话,跳转路径必须使用名称的形式
 this.$router.push({
      name: 'index',
      params: {
        from: 'login'
      }
  })
// 接收参数,如果参数是login,就进行数据的请求       
mounted(){
    let params = this.$route.params
    if(params && params.from == 'login'){
      this.getCartCount()
    }
  },

(4)优化

APP.vue中,我们默认每次打开页面时就进行数据请求,如果没有登录,就会报错,这样实际上也会造成资源的浪费,我们可以判断是否登录,只有登录状态下才进行请求:

mounted(){
    if(this.$cookie.get('userId')){
      this.getUser()
      this.getCartCount()
    }
  }

在登陆之后,会储存一个后台的会话ID,它的持续时间为Session(也就是当浏览器关闭之后,就自动清空,结束会话),所以我们的cookie过期时间和它的会话持续时间保持一致就可以了:
在这里插入图片描述

this.$cookie.set('userId', res.id, { expires: 'Session' })

8. Element UI的使用

在使用Element UI的时候,按照官网的导入方式,遇到报错 Error: Cannot find module 'babel-preset-es2015'问题,需要在 .babelrc 文件中,进行如下修改:

{
    "presets": [["@babel/preset-env", { "modules": false}]],
    "plugins": [
        [
            "component",
            {
                "libraryName": "element-ui",
                "styleLibraryName": "theme-chalk"
            }
        ]
    ]
}

这样问题就解决了。

9. 订单确认页面的实现

由于对收货地址的操作有三个:添加、编辑、删除。为减少代码的冗余,我们可以将每个操作定义一个标识符,对点击的标识符进行判断,根据不同标识符来发起不同的请求:

  submitAddress(){
      // 使用解构赋值来来获取data中的数据
      let {checkedItem,userAction} = this;
      let method,url,params={};
      if(userAction == 0){
        method = 'post',url = '/shippings';
      }else if(userAction == 1){
        method = 'put',url = `/shippings/${checkedItem.id}`;
      }else {
        method = 'delete',url = `/shippings/${checkedItem.id}`;
      }
      //表单验证略过...
      // params中的参数是在表单中解构赋值出来的
      params = {receiverName,receiverMobile,receiverProvince,receiverCity,receiverDistrict,receiverAddress,receiverZip}
      this.axios[method](url,params).then(()=>{
        this.closeModal(); // 关闭弹窗
        this.getAddressList();  // 重新刷新地址列表
        Message.success('操作成功');
      });
    }

10. 订单支付功能的实现

10.1 支付宝支付

支付宝支付的逻辑比较简单。首先,请求之后,跳转至一个新的页面。我们这里使用window.open来实现空白页面的打开,点击支付宝支付后,触发下面的方法:

window.open('/#/order/alipay?orderId='+this.orderId,'_blank')

进入该页面后之后,触发提交的请求,后台会返回一个content,这是包含支付的一个表单代码,触发这段代码就会跳转到支付宝的支付页面。这里我们使用document.forms[0].submit()来触发这个表单的提交:

<template>
  <div class="ali-pay">
    <loading v-if="loading"></loading>
    <div class="form" v-html="content"></div>
  </div>
</template>
// 获取订单的Id
orderId:this.$route.query.orderId,
// 支付请求
paySubmit(){
      this.axios.post('/pay', {
        orderId: this.orderId,
        orderName: '小米商城',
        amount: 0.01,
        payType: 1
      }).then((res) => {
        this.content = res.content
        setTimeout(() =>{
          document.forms[0].submit()
        }, 100)
      })
    }

这里还使用了一个loading组件来作为从支付页面到支付宝的页面的过渡。

这两步完成之后,就可以跳到了支付宝支付页面,然后就可以进行支付操作了。

10.2 微信支付

微信支付相对于支付宝支付就相对复杂一些了,来看一下具体的步骤:

-首先是点击微信支付,发起请求

this.axios.post('/pay', {
        orderId: this.orderId,
        orderName: '小米商城',
        amount: 0.01,
        payType: 2
      }).then((res) => {
        QRCode.toDataURL(res.content)
          .then(url => {
            this.showPay = true;
            this.payImg = url;
            this.loopOrderState();
          })
          .catch(() => {
            Message.error('微信二维码生成失败,请稍后重试');
          })
      })

返回的结果是微信支付的链接:
在这里插入图片描述

我们需要将返回的结果显示页面上,所以创建于了一个ScanPayCode的组件,用来显示二维码。

而想要将链接转化为二维码,需要使用一个插件:qrcode

  • 安装:npm install --save qrcode
  • 引入:import QRCode from 'qrcode'
  • 使用:
 QRCode.toDataURL(res.content)
          .then(url => {
            this.showPay = true; // 展示二维码页面
            this.payImg = url;  // 将图片赋给页面
            this.loopOrderState();  // 轮询
          })
          .catch(() => {
            Message.error('微信二维码生成失败,请稍后重试');
          })

最后就是轮询订单支付的状态,如果支付完成,就清除定时器,跳到订单列表页面:

 loopOrderState(){
      // 设置定时器
      this.T = setInterval(() => {
        this.axios.get(`/orders/${this.orderId}`).then((res) => {
          if(res.status == 20) {
            clearInterval(this.T) // 清除定时器
            this.goOrderList()  // 跳转到订单列表
          }
        })
      }, 1000);
    }

需要注意的是,当我们支付成功,如果回到刚才的订单页面,在点击微信支付,就会有错误:
在这里插入图片描述

之前已经做了异常的拦截,但是那个咋请求成功的基础上,对业务请求进行拦截,而没有对状态码进行拦截,所以要对除200以外的状态码进行拦截:

axios.interceptors.response.use((response) => {
  let res = response.data
  let path = location.hash
  if(res.status == 0){
    return res.data
  }else if(res.status == 10){
    if(path !== '#/index'){
      window.location.href = '/#/login'
    }
    return Promise.reject(res)
  }else{
    Message.warning(res.msg)
    return Promise.reject(res)
  }
}, (error) => {
  let res = error.response
  Message.error(res.data.message)
  return Promise.reject(error)
})

实际上,拦截器的第一个参数方法是对业务请求的拦截,第二个参数方法是对状态信息的拦截,只要获取到错误信息,提示用户,并将错误抛出,避免记性res的状态里,就可以了。

11. 订单列表的实现

订单列表页面主要就是订单的加载,这里记录一下订单加载更多的三种方式。

11.1 分页器

  • 这里使用的是element ui的分页器,在页面按需引入,并注册:
  import {Pagination} from 'element-ui'
  // 由于我们引入的是Pagination,而使用时前面有一个el-,所以使用这种方式加载
  components:{
      [Pagination.name]: Pagination,
  }
  • 定义结构
<el-pagination
    class="pagination"  // 样式
    background    // 背景
    layout="prev, pager, next"  // 分页
    :pageSize = "pageSize"   // 每页订单数
    :total="total"     // 总共的数量
    @current-change="handleChange" // 触发分页器
     ></el-pagination>
  • 数据交互
handleChange(pageNum){
        this.pageNum = pageNum  // 更改页面
        this.getOrderList()  // 刷新列表
      }

之前也用过element ui 的分页器了,还是比较简单的。来看一下之前没有用到过的方法。

11.2 按钮加载

  • 在最底部放一个“加载更多”的按钮,这里也使用element uI的button:
import { Button } from 'element-ui'

  components:{
     [Button.name]: Button,
  }
  • 定义结构

这里定义一个数据showNextPage,默认为true,就是可以显示下一页

<div class="load-more" v-if="showNextPage">
     <el-button type="primary" :loading="loading" @click="loadMore">加载更多</el-button>
 </div>
  • 数据交互

这里需要注意,我们想要的是加载更多之后与前面加载的数据进行拼接,所以需要对订单的List进行改造:

getOrderList(){
        this.loading = true
        this.axios.get('orders', {
          params:{
            pageNum: this.pageNum
          }
        }).then((res) => {
          
          this.loading = false;
          // 将数据与前一页进行拼接
          this.list = this.list.concat(res.list)
          this.total = res.total
          // 判断是否还有下一页,如果没有就会隐藏加载按钮
          this.showNextPage = res.hasNextPage
          this.busy = false
        }).catch(() => {
          this.loading = false
        })
      }

loadMore(){
        // 页数加一
        this.pageNum++
        // 重新刷新订单列表
        this.getOrderList()
      }

11.3 滚动加载

  • 滚动加载需要使用到一个插件:vue-infinite-scroll,首先要安装并引入、注册插件:
// 安装
npm install vue-infinite-scroll --save
// 引入
import infiniteScroll from 'vue-infinite-scroll'
// 注册(与data同级)
directives:{
      infiniteScroll
}
  • 定义结构
<div class="scroll-more"
  v-infinite-scroll="scrollMore"  // 触发滚动
  infinite-scroll-disabled="busy"    // 是否禁用
  infinite-scroll-distance="410">    // 距离底部多少像素的时候,进行加载
     <img src="/imgs/loading-svg/loading-spinning-bubbles.svg" alt="" v-show="loading">
 </div>
  • 数据交互
getList(){
        this.loading = true;
        this.axios.get('/orders',{
          params:{
            pageSize:10,
            pageNum:this.pageNum
          }
        }).then((res)=>{
          this.list = this.list.concat(res.list);
          this.loading = false;
          if(res.hasNextPage){
            this.busy=false;
          }else{
            this.busy=true;
          }
        });
      }

 scrollMore(){
        this.busy = true;
        setTimeout(()=>{
          this.pageNum++;
          this.getList();
        },500);
      },

其中,busy代表是否触发加载,如果是true就加载,反之就不加载。

总之,这三种方法都能对列表加载更多数据,只是方式不同,还要根据需求去使用。

12. 项目优化

懒加载使用import的方式,由于import方式是ES7的语法,所以我们需要引入一个插件,来解析ES7的语法:@babel/plugin-syntax-dynamic-import

安装:npm install --save-dev @babel/plugin-syntax-dynamic-import

然后将路由改成按需加载的形式:

   {
     path: 'confirm',
     name: 'order-confirm',
     component: () => import('./pages/orderConfirm.vue')
   }

这样就实现了路由的懒加载,但是在首页刷新的时候,还是会在空闲时间把所有的js代码都加载下来,在network中的js看不到,只能在other看到。只有在需要加载时,js内容才会出现在script中,js文件中华也就出现了相应的文件内容。
在这里插入图片描述

这时,所有的js文件都被放在<link rel="prefetch" ></link>中,它告诉浏览器,这段资源将会在未来某个导航或者功能要用到,但是本资源的下载顺序权重比较低。也就是说prefetch通常用于加速下一次导航,而不是本次的。被标记为prefetch的资源,将会被浏览器在空闲时间加载。

在这里插入图片描述
所以,如果想要真正的做到按需按需加载,就要清除prefetch。在vue.config.js文件中加入以下代码:

chainWebpack:(config)=>{
    config.plugins.delete('prefetch');
  }

13. 总结

(1)做了什么?

  • 完成了11个页面组件的开发,9个小组件的开发
  • 对SessionStorage进行封装
  • 解决了跨域的问题
  • 对路请求进行拦截,接口统一管理,避免重复拦截,代码冗余
  • 使用cookie来管理用户登录的权限
  • 使用Vuex来管理共享的数据
  • 使用微信、支付宝进行支付
  • 使用element ui来丰富商城的内容
  • 使用Vue 过渡动画以及CSS3 animation动画效果
  • 使用了几个npm 的插件
  • 对页面进行优化,提高页加载的性能
  • 使用路由懒加载来提高性能
  • 页面布局,页面的逻辑实现
  • 使用Sass、mixin来对对公共样式抽离,减少冗余代码

(2)难的是什么?

个人觉得比较难的地方是以下几点:

  • Vuex状态管理,过程不是很熟悉
  • 插件的使用,不是很熟练
  • Bug的解决,有时不知道问题出在哪
  • 业务逻辑的实现,有时缺少条件,致使不能实现想要的功能
  • 页面布局(个人不是很擅长)
  • 项目优化不知从哪里入手
  • 项目部署

(3)收获是什么?

  • 更加理解组件化的开发的概念,将页面重复的地方拆分成组件,然后进行复用,就减少代码的冗余
  • 页面动画的实现,Vue动画过渡、CSS3动画
  • 了解了多种跨域的解决方案
  • 对支付流程有所了解(微信支付、支付宝支付)
  • 之前没有用过cookie来管理权限,这次有所了解了
  • 对Sass更加了解,真的很方便,提高了代码的可复用性
  • 最多的还是对业务流程、业务逻辑的了解
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页