支付、打包优化

支付页

组件准备

创建支付组件文件

// 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
      }
    })
  }
},
...

布局处理

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

  • 顶部为课程信息
  • 中间为账户信息
  • 底部为支付方式
    整体布局使用Vant的cell单元格组件,内部进行细节处理,代码:
// 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>

数据绑定

步骤:

  • 课程信息
    • props接收路由参数courseId
    • 引入course.js的getCourseById接口并请求数据
    • 绑定数据
  • 账户信息
    • 从store中读取用户信息
    • 使用计算属性将手机号中间四位替换为****
// 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>

逻辑处理

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

  • 支付页面打开后,需要根据商品ID(课程ID)创建商品订单
  • 用户操作选择支付方式(微信,支付宝)
  • 跳转到支付页,支付页会自动唤起对应的APP
  • 支付,并且跳转成功,跳转Learn组件

需要使用到的接口

  • 订单接口:
    • 创建商品订单接口:地址
  • 支付接口
    • 获取支付方式:地址
    • 创建订单(发起支付):地址
  • 查询订单(支付结果):地址

封装接口

新建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
  })
}

创建订单与获取支付方式

引入并调用接口

  • 调用创建商品订单接口获取订单号
  • 调用获取支付方式接口
  • 根据支付方式接口设置支付区域的radio数据
// 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
},
...

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

  • 微信支付地址为PC支付功能,无法正常唤起微信支付,为接口原因
  • 支付宝地址可以正确跳转App支付,所有课程无论标价多少,后台都是默认1分钱,需要真实支付才可以成功购买,所以要尽快完成调试,不要浪费你的小金库
    (支付过程就不截图了,肉疼环节)

查询支付结果

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

  • 设置定时器,每隔1000ms进行一次查询请求
  • 注意接口传入的orderNo不是商品订单号,而是支付订单号,从支付请求的响应内容中得到
// 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中添加配置

  • 文档要求在.babelrc中设置,.babelrc与babel.config.js文件作用相同,一般有babel.config.js就不用.babelrc了
...
  "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 引入,而不会将所有包都设置为这种方式(文件数多,首次的请求数也会变多)。

完毕

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

推荐阅读更多精彩内容