Vue

支付、打包优化

2021-03-18  本文已影响0人  amanohina

支付页

组件准备

创建支付组件文件

// views/pay/index.vue
<template>
  <div class="pay">支付功能</div>
</template>

<script>
export default {
  name: 'Pay'
}
</script>

<style lang="scss" scoped></style>

添加路由,注意,支付页要登录才能显示

// router/index.js
...
  // 支付页
  {
    path: '/pay/:courseId/',
    name: 'pay',
    component: () => import(/* webpackChunkName: 'pay' */'@/views/pay/index'),
    meta: { requiresAuth: true },
    props: true
  },
...

课程详细页中点击购买后跳转,并且传递课程ID

// course-info/index.vue
...
<van-button
  type="primary"
  @click="handlePay"
>立即购买</van-button>
...
<script>
...
handlePay () {
  // 检测是否登录
  if (this.$store.state.user) {
    // 如果已登录,跳转支付页
    this.$router.push({
      name: 'pay',
      params: {
        courseId: this.courseId
      }
    })
  } else {
    // 如果未登录,跳转登录页,并记录本页面信息,登录成功跳回到当前页
    console.log(this.$route.fullPath)
    this.$router.push({
      name: 'login',
      query: {
        redirect: this.$route.fullPath
      }
    })
  }
},
...

布局处理

支付组件分为上中下三部分

// pay/index.vue
<template>
  <div class="pay">
    <van-cell-group>
      <van-cell class="course-info">
        <img src="xxxxx-demo.png" alt="">
        <div class="price-info">
          <div class="course-name" v-text="示例课程名称"></div>
          <div class="discounts">¥100000</div>
        </div>
      </van-cell>
      <van-cell class="account-info">
        <div>购买信息</div>
        <div>购买课程后使用此账号登录【拉勾教育】学习课程</div>
        <div class="username">当前账号:1122334455</div>
      </van-cell>
      <van-cell class="pay-channel">
        <div class="title">支付方式</div>
      </van-cell>
    </van-cell-group>
  </div>
</template>
...
<style lang="scss" scoped>
// 让容器盛满屏幕,用于 #app 没有宽度,设置定位脱标,让元素参考窗口尺寸
.pay {
  position: absolute;
  width: 100%;
  height: 100%;
}
// 容器
.van-cell-group {
  width: 100%;
  height: 100%;
  background-color: #f8f9fa;
  display: flex;
}
// 课程信息
.course-info {
  height: 170px;
  padding: 40px 20px 0;
  margin-bottom: 10px;
  box-sizing: border-box;
}
// 让图片与右侧信息同行显示
.course-info .van-cell__value{
  display: flex;
}
// 课程图片
.course-info img {
  width: 80px;
  height: 107px;
  border-radius: 10px;
}
.price-info {
  height: 107px;
  padding: 5px 20px;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
.price-info .course-name {
  font-size: 16px;
}
.price-info .discounts {
  font-size: 22px;
  font-weight: 700;
  color: #ff7452;
}

// 账户信息
.account-info {
  height: 120px;
  margin-bottom: 10px;
}

.account-info div:nth-child(2) {
  font-size: 12px;
  color: #999;
}

.account-info .username {
  margin: 20px 0 10px;
  font-size: 16px;
}
</style>

数据绑定

步骤:

// pay/index.vue
...
        <!-- 课程图片 -->
        <img :src="course.courseImgUrl" alt="">
        <div class="price-info">
          <!-- 课程名称 -->
          <div class="course-name" v-text="course.courseName"></div>
          <!-- 课程价格 -->
          <div class="discounts">¥{{ course.discounts }}</div>
        </div>
      </van-cell>
      <van-cell class="account-info">
        ...
        <!-- 账号信息 -->
        <div class="username">当前账号:{{ username }}</div>
      </van-cell>
        ...
<script>
import { getCourseById } from '@/services/course'
export default {
  name: 'Pay',
  props: {
    courseId: {
      type: [String, Number],
      required: true
    }
  },
  data () {
    return {
      course: {}
    }
  },
  created () {
    this.loadCourse()
  },
  methods: {
    async loadCourse () {
      const { data } = await getCourseById({
        courseId: this.courseId
      })
      this.course = data.content
      console.log(data)
    }
  },
  computed: {
    username () {
      return this.$store.state.user.organization.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
    }
  }
}
</script>
...

支付结构

使用Vant的[Radio单选框]组件与cell一并使用
设置到页面中

// pay/index.vue
      ...
      <!-- 支付方式 -->
      <van-cell class="pay-channel">
        <div>
          <p>支付方式</p>
          <van-radio-group v-model="radio">
            <van-cell-group>
              <van-cell @click="radio = '1'">
                <!-- 将左侧标题设置为插槽,添加对应支付图标 -->
                <template #title>
                  <img src="http://www.lgstatic.com/lg-app-fed/pay/images/wechat_b787e2f4.png" alt="">
                  <span>微信支付</span>
                </template>
                <template #right-icon>
                  <van-radio name="1" />
                </template>
              </van-cell>
              <van-cell clickable @click="radio = '2'">
                <template #title>
                  <img src="http://www.lgstatic.com/lg-app-fed/pay/images/ali_ed78fdae.png" alt="">
                  <span>支付宝支付</span>
                </template>
                <template #right-icon>
                  <van-radio name="2" />
                </template>
              </van-cell>
            </van-cell-group>
          </van-radio-group>
        </div>
        <van-button>¥{{ course.discounts }} 立即支付</van-button>
      </van-cell>
    </van-cell-group>
  </div>
</template>

<script>
...
  data () {
    return {
      ...
      radio: '1'
    }
  },
    ..
</script>

<style lang="scss" scoped>
...
// 支付区域(占满剩余空间)
.pay-channel {
  flex: 1;
}
// 让 radio 与 按钮在上下两端
.pay-channel .van-cell__value {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
.pay-channel .van-cell {
  padding: 20px 10px;
}
// 左侧标题插槽
.pay-channel .van-cell__title {
  display: flex;
  align-items: center;
}
.pay-channel .van-cell img {
  width: 28px;
  height: 28px;
}
.pay-channel .van-cell span {
  font-size: 16px;
  margin-left: 10px;
}

// 右侧 radio 选中颜色
::v-deep .van-radio__icon--checked .van-icon{
  background-color: #fbc546;
  border-color: #fbc546;
}

// 底部按钮样式
.pay-channel .van-button {
  background: linear-gradient(270deg,#faa83e,#fbc546);
  border-radius: 20px;
  margin-bottom: 5px;
  font-size: 18px;
}
</style>

逻辑处理

支付功能需要有以下的步骤:

需要使用到的接口

封装接口

新建src/services/pay.js

// services/pay.js
import request from '@/utils/request'

// 创建商品订单接口
//  - goodsId 商品(课程)ID 必传
export const createOrder = data => {
  return request({
    method: 'POST',
    url: '/front/order/saveOrder',
    data
  })
}

// 获取支付方式接口
//  - shopOrderNo 订单号必传
export const getPayInfo = params => {
  return request({
    method: 'GET',
    url: '/front/pay/getPayInfo',
    params
  })
}

// 创建订单(发起支付)
//  - goodsOrderNo, channel, returnUrl 必传
export const initPayment = data => {
  return request({
    method: 'POST',
    url: '/front/pay/saveOrder',
    data
  })
}

// 查询订单(查询支付结果)
//  - orderNo 订单号必传
//  - 由于接口要求传递 JSON,所以进行 headers 设置
export const getPayResult = params => {
  return request({
    method: 'GET',
    url: '/front/pay/getPayResult',
    headers: { 'content-type': 'application/json' },
    params
  })
}

创建订单与获取支付方式

引入并调用接口

// pay/index.vue
...
<script>
...
import { createOrder, getPayInfo } from '@/services/pay'
...
data () {
    return {
      ...
      // 订单号
      orderNo: null,
      // 支付方式信息
      payInfo: {}
    }
  },
  created () {
    ...
    this.loadOrder()
  },
  methods: {
    // 创建订单,获取订单号
    async loadOrder () {
        // 创建订单,获取订单号
      const { data } = await createOrder({
        goodsId: this.courseId
      })
      this.orderNo = data.content.orderNo
      // 获取支付方式
      const { data: payInfo } = await getPayInfo({
        shopOrderNo: this.orderNo
      })
      this.payInfo = payInfo.content.supportChannels
    },
...
</script>
...
<van-radio-group v-model="radio">
  <van-cell-group>
    <van-cell @click="radio = payInfo[1].channelCode">
      ...
      <template #right-icon>
        <van-radio :name="1" />
      </template>
    </van-cell>
    <van-cell clickable @click="radio = payInfo[0].channelCode">
      ...
      <template #right-icon>
        <van-radio :name="2" />
      </template>
    </van-cell>
  </van-cell-group>
</van-radio-group>

支付请求

点击支付按钮时,发送请求

// pay/index.vue
...
<van-button @click="handlePay">...</van-button>
...
<script>
import { ..., initPayment } from '@/services/pay'
...
async handlePay () {
  // 发起支付请求
  const { data } = await initPayment({
    goodsOrderNo: this.orderNo,
    channel: this.radio === 1 ? 'weChat' : 'aliPay',
    returnUrl: 'http://edufront.lagou.com/'
  })
  // 接收响应地址,并进行跳转
  window.location.href = data.content.payUrl
},
...

设置完毕,在手机上进行测试

查询支付结果

发起支付请求后,需要轮询支付结果

// pay/index.vue
...
import { ..., getPayResult } from '@/services/pay'
...
async handlePay () {
  ...
  const timer = setInterval(async () => {
    // 发起查询支付结果请求(此处使用)
    const { data: payResult } = await getPayResult({
      orderNo: data.content.orderNo
    })
    // 如果支付结果成功,清除定时器,并提示购买成功,跳回到学习页
    if (payResult.content && payResult.content.status === 2) {
      clearInterval(timer)
      this.$toast.success('购买成功!')
      this.$router.push({
        name: 'learn'
      })
    }
  }, 1000)
},
...

打包优化

普通打包结果如下:



打包优化主要体现在两个方面

打包过程优化指的是打包速度方面,减少项目中没有使用到的包,去除没有使用到的样式等等

打包结果优化指的是打包后的文件体积,比如压缩文件大小等等
当然,也有很多优化项既可以对过程优化,也可以对结果进行优化

如果我们要对打包进行优化,就需要更改打包配置,由于Vue CLI是基于webpack构建的,打包配置其实就是webpack配置

Vue CLI配置文件

Vue CLI内部包含了对webpack的默认配置,所以项目中大多数情况都无需进行配置。如果需要手动添加或者修改配置,就需要在项目根目录下创建vue.config.js配置文件
对于该文件的项目配置,Vue CLI提供了详细的配置文档

productionSourceMap

当项目打包后,如果出现了代码错误,可以从控制台找到错误对应的源码位置,这是由于打包时生成了 .map 文件,可以帮助定位错误信息。
用户不可能对我们的代码进行调试,所以 .map 文件就没有存在的意义了。这时设置 productionSourceMap 为 false,不仅可以不生成 .map 文件,同时会对源码加密,防止代码被盗用。
vue.config.js:

module.exports = {
  productionSourceMap: false 
}

css.extract

打包时,css 默认会打包为独立文件,这样会增加页面的请求数量,由于项目单个页面组件的 css 体积通常不是很大,可以设置为行内引入方式,以减少网页请求次数。(到底用不用这个,取决于CSS在项目内占比大不大)
设置方式:

// vue.config.js
module.exports = {
  css: {
    extract: false
  },
  ...
}
上述两个方法使用之后的打包结果

图片压缩

图片压缩会在一定程度上影响图片的质量,使用时根据具体场景选择是否使用。图片有被压缩的必要才需要进行此步处理,如果网站需要高清展示图片就没必要这么做了

安装

需要使用image-webpack-loader首先安装

npm i image-webpack-loader -D

如果安装失败,则必须删除项目中的 node_modules 再重新安装依赖。
实在太慢可以考虑使用cnpm

配置

在vue.config.js中设置以下信息,用来对webpack进行loader配置

// vue.config.js
module.exports = {
  ...
  // 图片压缩 loader 配置
  chainWebpack: config => {
    // 配置图片压缩
    config.module
      .rule('images')
      .use('image-webpack-loader')
      .loader('image-webpack-loader')
      .options({
        bypassOnDebug: true
      })
      .end()
  }
}

Vant 自动按需引入组件

官方介绍中说到,引入所有组件会增加代码包的体积,不推荐整体引入。

自动按需引入需要使用 babel 的插件 babel-plugin-import,这个插件会在编译过程中将 import 写法自动转换为按需引入方式。

首先安装插件:

npm install babel-plugin-import -D

在babel.config.js中添加配置

...
  "plugins": [
    // 注意:webpack 1 无需设置 libraryDirectory
    ["import", {
      "libraryName": "vant",
      "libraryDirectory": "es",
      "style": true
    }]
  ]

在任意组件中按以下方式引入Vant组件,插件会在编译时自动转换为按需引入(引入JS与CSS)形式(哪里用了哪里都需要这样改正)

import { Button } from 'vant'
...
components: {
  VanButton: Button
}

Toast这种需要进行方法调用的组件需要在引入后将this.$toast更改为Toast()

// pay/index.vue 示例
import { ..., Toast } from 'vant'
...
// this.$toast.success('购买成功!')
Toast.success('购买成功!')

同时,main.js 中的整体引入就可以去除了。

// main.js
-  import Vant from 'vant'
-  import 'vant/lib/index.css'
-  Vue.use(Vant)

从打包结果来看,体积显著减小

错误说明

如果出现以下类似的报错,说明在不同组件中进行相同子组件引入(包括 Vant 组件)的顺序不同,例如 A 组件内先引组件 X 后引入组件 Y,B 组件内先引组件 Y 后引入组件 X,当 Webpack 将这些代码打包到同一个文件中时,就会无法处理从而导致 webpack 的 mini-css-extract-plugin 插件报错。调整引入顺序即可。
报错示意图如下:


CDN

在 public/index.html 中通过 CDN 的方式引入 Vue、Vant,这样就无需在 main.js 中进行引入了。
当我们在项目中使用 CDN 链接之后,就没必要下载打包第三方包了

// public/index.html
<!DOCTYPE html>
<html lang="">
  <head>
    ...
    <!-- Vant 样式 -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vant@2.12/lib/index.css" />
  </head>
  <body>
    ...
    <!-- 引入 Vue -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
    <!-- 引入 Vant -->
    <script src="https://cdn.jsdelivr.net/npm/vant@2.12/lib/vant.min.js"></script>
    ...
  </body>
</html>
// vue.config.js
module.exports = {
  ...
  configureWebpack: {
    // 通过 CDN 引入
    externals: {
      'vue': 'Vue',
      'vant': 'vant'
    }
  }
}

比较一下:
npm 安装方式打包结果



CDN安装方式


显而易见,打包体积有了明显的变化

这里演示的仅为 Vue 与 Vant 的 CDN 引入方式,其他工具也可以如此操作,但通常我们只会将体积比较大的第三方文件进行 CDN 引入,而不会将所有包都设置为这种方式(文件数多,首次的请求数也会变多)。

完毕

上一篇 下一篇

猜你喜欢

热点阅读