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

目录

      • 1. 项目概述
      • 2. 项目基础架构
        • 2.1 跨域及解决
        • 2.2 项目目录结构
        • 2.3 插件的安装
        • 2.4 storage封装
        • 2.5 接口错误拦截
        • 2.6 Mock设置
      • 3. 商城首页的实现
      • 4. 登录页面的实现
      • 5. Vuex的使用
      • 6. 产品站的实现
      • 7. 退出功能的实现
      • 8. Element UI的使用
      • 9. 订单确认页面的实现
      • 10. 订单支付功能的实现
        • 10.1 支付宝支付
        • 10.2 微信支付
      • 11. 订单列表的实现
        • 11.1 分页器
        • 11.2 按钮加载
        • 11.3 滚动加载
      • 12. 项目优化
      • 13. 总结

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

1. 项目概述

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

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

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

商城系统整体架构图:
【练习】基于Vue全家桶的仿小米商城系统_第1张图片

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:组件
    【练习】基于Vue全家桶的仿小米商城系统_第2张图片

2.3 插件的安装

  • vue-lazyload :懒加载
  • element-ui :Element UI
  • node-sass :Sass
  • sass-loader: Sass加载
  • vue-awesome-swiper :首页轮播
  • vue-axios : 结合使用axios
  • vue-cookie :Cookie
    【练习】基于Vue全家桶的仿小米商城系统_第3张图片

需要注意的是: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文件,来定义一些常用的字体大小和颜色值:

【练习】基于Vue全家桶的仿小米商城系统_第4张图片

使用:

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%;
  }
}

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

  • 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 被删除),在过渡/动画完成之后移除。

对于这些在过渡中切换的类名来说,如果使用一个没有名字的 ,则 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使用的图示:
【练习】基于Vue全家桶的仿小米商城系统_第5张图片我们需要在主页面显示用户的名称以及购物车商品的数量,这些数据需要登录状态下才显示。使用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,然后这组件有一个吸顶的效果,下面来记录一下实现的过程。

【练习】基于Vue全家桶的仿小米商城系统_第6张图片

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

  <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);
    }

需要注意的是,当我们支付成功,如果回到刚才的订单页面,在点击微信支付,就会有错误:
【练习】基于Vue全家桶的仿小米商城系统_第7张图片

之前已经做了异常的拦截,但是那个咋请求成功的基础上,对业务请求进行拦截,而没有对状态码进行拦截,所以要对除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
}
  • 定义结构
// 距离底部多少像素的时候,进行加载 <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文件中华也就出现了相应的文件内容。
【练习】基于Vue全家桶的仿小米商城系统_第8张图片

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

【练习】基于Vue全家桶的仿小米商城系统_第9张图片
所以,如果想要真正的做到按需按需加载,就要清除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更加了解,真的很方便,提高了代码的可复用性
  • 最多的还是对业务流程、业务逻辑的了解

你可能感兴趣的:(练习,vue,javascript,css,js)