Vue 实现仿美团外卖APP的总结

一、前言

项目源自网上一个Vue项目。原项目是基于Vue Cli 2 并且采用了UI组件库。为了练手,我采用原生CSS写样式,然后自己进行了响应式设计。后台应用我是直接拿来用的,后期复习到Node的时候我会自己来做一遍。我做这个项目最重要的目的是把学过的理论知识实践一下,虽然当前还有一些bug还没修复,但是我想先把做过的东西总结一下。

前端项目地址:vue-shop
后端项目地址:vue-shop-server

当前完成的功能展示如下:


当前bug待解决:

  • 浏览器首次打开手机模拟器时,首页swiper以及better-scroll都无法滑动,刷新后就没事了,而我在自己的手机Chrome浏览器测试完全正常。

  • 内置浏览器版本过低,打开失败。

功能待完善:

  • 搜索列表显示。
  • 订单组件显示购物车食物。

下面会挑选我觉得项目中比较重要的地方来讲解。

二、使用Vue Cli 3构建项目

项目启动了半个月后,我发现低版本包传到Github上面后有安全问题,于是我决定将原来的版本更新到 Vue Cli 3,这里采用图形界面的方式创建新项目。

vue ui

环境的搭建我相信大家看过很多文章了,所以我不再赘述。创建项目过程中,会让你选一些工具,它们可以集成到项目中去,我选的是:

  • Babel
  • Vuex
  • Router
  • CSS Pre-processors(CSS预处理器)

Vue 实现仿美团外卖APP的总结_第1张图片

然后下一步选择预处理器类型,代码风格采用哪种模式,是否使用history模式的路由,最后选择什么时候自动修复错误格式的代码,我的选择如下:

Vue 实现仿美团外卖APP的总结_第2张图片
点击创建项目后,稍等片刻就好啦。然后你此次的配置会保存一个预设,你下次再次创建的时候就可以使用这个预设了。我简单介绍一下脚手架的目录。

Vue 实现仿美团外卖APP的总结_第3张图片

这款风格是带有分号的,如嫌麻烦可以在刚开始创建项目的时候选择标准模式(standard)。

三、项目配置

注意:本文提及的所有模块的已经默认安装好了。

3.1 环境变量配置

项目启动的时候不同的环境会载入不同的环境变量。官方提供了以下几种方式来载入环境变量:

.env   # 在所有的环境中被载入
.env.local # 在所有的环境中被载入,但会被 git 忽略
.env.[mode] # 在特定环境下载入,由mode决定
.env.[mode].local # 在特定环境下载入,由mode决定,但会被 git 忽略

被指定模式的环境变量比没有指定的优先级要高,但如果脚手架工具启动时已经存在一个环境变量,它将用有最高优先级。那他们的环境是如何确定的呢?

脚手架工具中在process.env中已经定义好了一个环境变量NODE_ENV。我们可以在package.json中的script属性中指定--mode参数来指定NODE_ENV的值从而确定一个模式,比如我们指定一个alpha模式:

"script": {
	"alpha": "vue-cli-service build --mode test"
}

脚手架工具已经集成了三种模式:developmentproductiontest,通过运行以下命令来启动不同环境,然后加载.env.*文件载入环境变量:

  • development 模式用于 vue-cli-service serve
  • production 模式用于 vue-cli-service build 和 vue-cli-service test:e2e
  • test 模式用于 vue-cli-service test:unit

下面是我项目中的脚本命令:
Vue 实现仿美团外卖APP的总结_第4张图片
这三种环境其实已经能满足我们日常开发需求了。项目根目录下我创建了三个文件来载入不同环境的变量:

在这里插入图片描述
环境变量命名的风格也是有规定,它是一种键-值的结构,而且最好是VUE_APP_开头,因为如果你在配置文件中使用的话,不是以VUE_APP_开头的变量就不会载入。

我在development环境中定义了两个环境变量:

VUE_APP_BASE = "http://localhost:4000/" # 代理服务器地址
VUE_APP_IMAGE = "https://fuss10.elemecdn.com" # 图片地址

大家可以根据自己的实际开发需求来定义,我为了方便就只模拟了development环境。

3.2 vue.config.js 配置

Vue cli 3 创建的项目会集成一些常见的webpack配置,但是有时候我们还是要自己去配置一些选项,官方提供了vue.config.js,它会在项目启动的时候加载。

我主要配置了三个地方,其他的配置大家可以参考官方文档。

  • 配置全局的stylus样式
  • 配置别名(alias
  • 配置代理(proxy
const path = require('path')

function resolve(dir) {
  return path.join(__dirname, dir)
}

// 全局mixins.styl样式
function addStyleResource(rule) {
  rule
    .use('style-resource')
    .loader('style-resources-loader')
    .options({
      patterns: [resolve('src/assets/styles/stylus/index.styl')]
    })
}

module.exports = {
  lintOnSave: process.env.NODE_ENV !== 'production', // 开发环境保存时开启代码检查
  productionSourceMap: false, // 生产环境下关闭SourceMap
  chainWebpack: config => {
    const types = ['vue-modules', 'vue', 'normal-modules', 'normal']
    types.forEach(type =>
      addStyleResource(config.module.rule('stylus').oneOf(type))
    )
    config.resolve.alias
      .set('styles', resolve('src/assets/styles'))
      .set('images', resolve('src/assets/images'))
      .set('components', resolve('src/components'))
  },
  devServer: {
    proxy: {
      '/': {
        target: process.env.VUE_APP_BASE, // 代理地址
        changeOrigin: true // 是否支持跨域
      }
    }
  }
}

3.3 代码风格和约束

不同的团队有不同的规范,这里是我自己个人比较喜欢的代码风格,以后同团队协作开发的时候还是要向团队看齐的。

3.3.1 eslint + prettier配置

有两种方式来创建eslint配置:

  • 内置注释
  • 创建.eslintrc.*文件或在package.json中的eslintConfig属性中配置

我选择的是后者。

 "eslintConfig": {
    "root": true, // 将ESLint限制在当前项目,不再向上查找配置
    "env": {
      "node": true // 指定node环境,使用该环境下全局变量的预设
    },
    "extends": [
      "plugin:vue/essential", // 使用vue插件
      "@vue/prettier", // prttttier 预设
      "eslint:recommended" //使用eslint推荐的规则
    ],
    "rules": { // 拓展或者覆盖默认规则
      "prettier/prettier": [
        "error", //被prettier标记的地方抛出错误信息。
        {
          "tabWidth": 2, // 几个缩进空格
          "useTabs": false, // 是否使用tab缩进
          "semi": false, // 句尾是否有分号
          "singleQuote": true, // 是否是用单引号
          "bracketSpacing": true,
          "jsxBracketSameLine": false,
          "space-before-function-paren": true
        }
      ]
    },
    "parserOptions": {
      "parser": "babel-eslint" // 解析器,支持检测ES6+语法
    },
    "overrides": [
      {
        "files": [
          "**/__tests__/*.{j,t}s?(x)",
          "**/tests/unit/**/*.spec.{j,t}s?(x)"
        ],
        "env": {
          "jest": true
        }
      }
    ]
  }

3.3.2 组件约束

在我的项目中组件分为两种:路由组件UI组件。组件命名规则如下:

  • 组件采用驼峰命名,而且必须是一个文件夹,文件夹里面放一个index.vue存放父组件。
  • UI组件无逻辑的必须是BASE开头。
  • 组件命名不超过三个英文单词。
  • 组件根目录下面有必要的话创建components存放父组件下的其他组件,必须是耦合命名,比如Home父组件下的HomeShoplist

Vue 实现仿美团外卖APP的总结_第5张图片
然后就是组件属性的书写顺序(常见的):

  • el (根组件)
  • name
  • components
  • props
  • data
  • computed
  • watch
  • 生命函数钩子
  • methods

四、响应式设计

响应式指的是不同的终端设备下都能很好的显示网页,通常一套代码就可以搞定。

我采用的方案是viewport + vw + rem + media query + %的方式来实现。

4.1 设置meta标签

<meta name="viewport" content="width=device-width,initial-scale=1.0">

它的作用如下:

  • 设置初始视口(visual viewport)大小为设备的宽度
  • 设置初始缩放比例为1.0(作用等同于width=device-width

这个标签只对移动端页面初始渲染的时候起作用,只不过它还不是一个W3C规范。

4.2 响应式单位

除了上面提到的百分比单位%,还有类似有vw,vh,rem,em等常见单位,另外配上媒体查询(media query)和JS来辅助,我们的响应式设计就基本上完美了。

我看了很多文章,下面是我个人比较喜欢的两种方案:

  • 核心:rem + js 配菜:em + media query + %
  • 核心:vw + rem 配菜: em + media query + %

实现思路如下:

  • rem 是相对于html的font-size大小,默认16px(不同浏览器有差异),也就是说1rem = 16px

  • 将布局视口分成100份,动态获取初始布局视口的1%的宽度,将值设置给font-size,这样就得到了不同设备下的单位rem值

  • 算出某个元素和画布(设计稿)的占比,得出rem的单位系数。

下面说说两种方案怎么个整法。

4.2.1 rem + js 方案

(1)动态获取布局视口宽度,将1%的值赋值给html的font-size。

	(function(){
		var html = document.documentElement;
		window.addEventListener('resize', function() {
			html.style.fontSize = html.clientWidth / 100 + 'px';
		})
	}())

(2)算出某个元素与设计图的占比,然后把%换成rem即可。这里计算过于繁琐,可以在stylus的中定义一个函数,将设计图某个元素的px转化成rem单位,此后我们只需要在项目中的CSS使用这个函数即可。

// $px 就是设计图某个元素的px
$ueWidth = 375
px2rem($px)
  (($px * 100 / $ueWidth))rem

这个方案有个缺点,就是JS和CSS有一定的耦合性。如果不考虑这个小缺点,这个方案还是相当不错的。另外说一点就是,font-size是具有继承性的,那整个页面的字体就变得很诡异了。除此之外,字体使用rem单位后,字体并不会线性变化,所以我们需要配菜上场了。

(3)通过媒体查询初始化设备字体大小。

这里分割点大家可以参考设计师或者网上给的设计方案,我这里参考了浏览器手机模拟器上的分割点。

/* 分割点 */

/* iphone 5 */
@media screen and (min-width: 320px) {
  body {
    font-size: 14px;
  }
}
/* iphoneX */
@media screen and (min-width: 375px) {
  body {
    font-size: 16px;
  }
}
/* iphone6 7 8 plus */
@media screen and (min-width: 414px) {
  body {
    font-size: 18px;
  }
}
/* ipad */
@media screen and (min-width: 768px) {
  body {
    font-size: 20px;
  }
}
/* ipad pro */
@media screen and (min-width: 1024px) {
  body {
    font-size: 24px;
  }
}

然后所有的子元素的字体使用em相对单位来设置,注意这个单位是相对父级元素的字体大小,当然如果你不想计算,你也可以在stylus定义相关函数。总体来说,虽然有一点点瑕疵,但是这种方案对移动端兼容性很好,应用的人数也很多。

4.2.2 vw + rem

vw是CSS3中出现的,它是视口单位,桌面端指的是浏览器的可视区域;移动端指的就是布局视口。1vw代表布局视口的1%,如果不考虑兼容性,我们给html的font-size为1vw不可以替换上面的那种方案了吗。我们只需要把JS代码去掉就可以了。

html {
 font-size: 1vw;
}

以上就是我实践的响应式设计,虽然到目前为止还有许多瑕疵,但是我会慢慢完善下去的。

五、对Axios进行二次封装

5.1 axios请求的简单配置

下面是Axios的简单配置,根据项目需求合理地去添加配置,具体可以参考官方文档。

src目录下建立一个utils文件夹,主要是放所有的工具模块,比如正则验证等,然后在此目录下创建request.js

引入相关依赖模块

import Axios from 'axios' // 引入axios库
import router from '@/router' // 引入路由
import store from '@/store'

定义公共变量和函数

const SECONDS = 12 // 请求超时秒数

/**
 * 跳转登录页
 * 携带当前页面路由,在登录页面完成登录后返回当前页面
 */
const toLogin = () => {
  router.replace({
    path: '/login',
    query: {
      redirect: router.currentRoute.fullPath
    }
  })
}


/**
 * 错误统一处理
 * @param {Number} status 状态码
 */
const errorHandle = (status, other) => {
  // 状态码判断
  switch (status) {
    // 401: 未登录状态,跳转登录页
    case 401:
      toLogin()
      break
    // 404请求不存在
    case 404:
      alert('请求的资源不存在')
      break
    case 500:
      alert('服务器错误')
      break
    default:
      alert(other)
      break
  }
}

注意:这里的错误提示完全可以使用UI组件来替代,我比较懒就写了个alert,其次是状态码,这个需要和后台协商确定。

创建Axios实例


// 创建axios实例
const instance = axios.create({
  baseURL: '', // 之前已经在环境变量中配置好了,这个必须为空,否则会报严重的错误
  timeout: 1000 * SECONDS
})

默认配置

instance.default.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'

这样配置后,post传递数据默认格式就是类似这样的name=laocao&age=22了,当然还有其他默认配置,根据PM给的需求来定,这是我自己的项目,所以…需求我自己定。

拦截器

/**
 * 请求拦截器
 * 每次请求前,如果存在token则在请求头中携带token
 */

instance.interceptors.request.use(
  config => {
    // 登录流程控制中,根据本地是否存在token判断用户的登录情况
    // 但是即使token存在,也有可能token是过期的,所以在每次的请求头中携带token
    // 后台根据携带的token判断用户的登录情况,并返回给我们对应的状态码
    // 而后我们可以在响应拦截器中,根据状态码进行一些统一的操作
    const token = store.state.token
    token && (config.headers['Authorization'] = token)
    return config
  },
  error => Promise.error(error)
)

// 响应拦截器
instance.interceptors.response.use(
  // 状态码判断
  res => (res.status === 200 ? Promise.resolve(res) : Promise.reject(res)),
  // 请求失败错误处理
  error => {
    const { response } = error
    if (response) {
      // 请求已发出,但是不在2xx的范围
      errorHandle(response.status, response.data.message)
      return Promise.reject(response)
    } else {
      // 处理断网的情况
      // eg:请求超时或断网时,更新state的network状态
      // network状态在app.vue中控制着一个全局的断网提示组件的显示隐藏
      // 关于断网组件中的刷新重新获取数据,会在断网组件中说明
      if (!window.navigator.onLine) {
        store.commit('changeNetwork', false)
      } else {
        return Promise.reject(error)
      }
    }
  }
)

响应拦截器中的token过期处理和断网这两种情况我没有考虑到项目中。

断网处理具体做法是dispatch一个action或者直接commit一个mutation来改变组件的状态,可以引入相关断网组件提示用户,我这里犯懒就没写了。

导出实例

export default instance

到这里,简单的配置我们算是做完了,实际中肯定不是那么简单的,我也是刚学习,不求快,快则囫囵吞枣会噎着。

5.2 统一管理Api请求

我们需要在src目录下创建一个api文件夹,然后在该目录下创建一个index.js来统一管理api。

引入axios实例

import axios from '@/utils/request'

统一管理url

// 统一url管理
const url = {
  getLocationUrl: '/position',
  getShoplistsByLocationUrl: '/shops',
  ...
}

请求函数

请求我只考虑了get和post请求,通过实例来调用axios中的getpost方法就可以实现请求,并且它们都会返回一个Promise对象。为了减少篇幅,就列举三种比较有代表性的请求函数。

/**
 * 获取食品分类列表
 *
 * @returns {Promise}
 */
function getShopCategoryApi() {
  return axios.get(url.getShopCategoryUrl)
}

/**
 * 请求手机验证码
 *
 * @param {Number} phone
 * @returns {Promise}
 */
function getMessageCodeApi(phone) {
  return axios.get(url.getMessageCodeUrl, {
    params: {
      phone
    }
  })
}

/**
 *  用户使用密码登陆
 *
 * @param {Object} { name, pwd, captcha }
 * @returns {Promise}
 */
function postUserLoginByPasswordApi({ name, pwd, captcha }) {
  return axios.post(url.postUserLoginByPasswordUrl, {
    name,
    pwd,
    captcha
  })
}

注意的是,get请求如果需要传参需要在get方法的第二个参数传入一个对象,该对象有一个params属性,它也是一个对象,参数写在里面即可。post方法和get不同的是,参数直接就在方法的第二个参数写就行。

导出api

最后,我们统一把这些api导出去。

export default {
  getLocationApi,
  getShoplistsByLocationApi,
  getShopCategoryApi,
  ...
}

添加链接描述

5.3 挂载到Vue的原型上

引入

import api from '@/api'

挂载

Vue.prototype.$axios = api // 将所有api请求挂在Vue原型中

使用
以后我们就可以通过以下的方式来访问某个api啦:

this.$api.getLocationApi()

六、Vuex结构设计

项目采用了Vuex来作状态管理,来存储组件的公共状态。我这个项目不是特别大,所以我就没有采用Module方式来进行模块化管理,否则会变得很复杂。但是项目特别大的话,推荐采用模块化管理Vuex的状态。

6.1 目录设计

我们都知道Vuex有一些核心概念:stateactionmutationgetter,它们构成了Vuex整个结构。简单介绍一下,在组件中我们想改变它的状态我们首先需要分发action,它可以是异步的也可以是同步的,然后通过commit触发相关的mutation,从而进行状态更新,最后在组件中通过计算属性响应式来数据然后驱动更新视图。

Vue 实现仿美团外卖APP的总结_第6张图片

上面的mutation官方把它近似地看作了事件,我觉得这十分合理。事件具有两个很重要的特点:事件类型和回调函数。所以我们可以专门定义一个mutation-types来管理事件类型。下面我们来说说如何请求数据和管理数据吧。

6.2 流程

我就拿一个例子来说吧,比如现在有一个需求:请求首页食物分类信息数据并展示在页面上。

6.2.1 state

定义好一个categories数组,存放分类信息数据:

export default {
 categories: [], // 食品分类
}

在需要相关数据的组件中来引入状态:

import { mapState } from 'vuex'

// 通过计算属性来响应数据变化
computed: {
 ...mapState(['categories'])
}

当然还有其他方式来引入状态,不过还是要推荐上面那种方式,因为当组件依赖的状态越来越多时,很显然上面的那种方式更好。

6.2.2 mutation-type

接下来,我们要定义一个事件类型来标识事件,从而触发响应类型的状态变更。

export const RECEIVE_CATEGORIES = 'receive_categories' // 接收食物分类信息

6.2.3 mutation

有了事件类型,我们可以根据类型来注册对应的mutation。/我们可以使用 ES6 风格的计算属性命名功能来命名一个事件:

// 引入事件类型
import { RECEIVE_CATEGORIES } from './mutation-type'

// 注册事件,回调
export default {
	// 当RECEIVE_CATEGORIES事件触发时,执行回调改变状态
	[RECEIVE_CATEGORIES](state, { categories }) {
		state.categories  = categories 
	}
}

上述回调中,有两个参数:statepayload。它们都是对象,第二个参数来在action提交上来的数据,可以通过解构赋值直接拿到数据。

6.2.4 action

action都是请求数据变更的操作,通常都是异步操作,我们可以借助async和await来拿到数据,并且把数据提交给响应的mutation。这里会有一个小问题就是,我们之前把api挂载到了Vue原型上面,但在Vuex里面的this指向的是store对象,所以我们在index.js需要重新挂载一下。

// 引入事件类型
import { RECEIVE_CATEGORIES } from './mutation-type'
// 请求食物分类信息
async getCategories({ commit }) {
 const {
   data: { data }
 } = await this.$axios.getShopCategoryApi()
 commit({
   type: RECEIVE_CATEGORIES,
   categories: data
 })
}

我们只需要在组件中通过dispatch就可以触发action了:

mounted() {
	this.$store.dispatch('getCategories')
}

当然也完全可以采用mapAction的方式:

// 引入mapAction
import { mapActions } from 'vuex'
// 注册函数
methods: {
 ...mapActions(['getCategories'])
}

6.2.5 index

结构写完后,我们需要在index里面把store对象暴露给全局,这样所有组件才能访问到Vuex。getters是计算属性,和state差不多,所以就没说明了。

import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mutations from './mutations'
import actions from './actions'
import getters from './getters'
import api from '@/api'

Vuex.Store.prototype.$axios = api // 将所有api请求挂在Vuex原型中
Vue.use(Vuex)

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

就这样,我们的Vuex设计就结束了,其余的请求无非就是照搬这个模式来做了。

七、使用Mockjs来模拟接口

首先我们需要在根目录下创建mock文件夹,然后在此目录下创建一个data目录,存储不同接口的数据,然后在index中讲mock接口导出,最后在main.js中加载即可。当然,我这个过于简单了,可以通过模块化管理不同的mock数据接口。
`Vue 实现仿美团外卖APP的总结_第7张图片
下面是index中的代码:

import Mock from 'mockjs'
import { goods, ratings, info } from './data/shop.json'

// 请求mock接口
Mock.mock('/foods', { code: 0, data: goods })
Mock.mock('/commends', { code: 0, data: ratings })
Mock.mock('/info', { code: 0, data: info })

八、项目难点和问题

8.1 难点

8.1.1 swiper组件分页逻辑

分析

目前有一个iconList数组,这个数组存储了食物分类的信息,但是每一页只能显示8个icon,当数组长度超过了8我们就需要放到下一页显示。那么很显然我们需要一个二维数组,一维数组存储每一页,二维数组存储每一页的icon。我们可以遍历这个数组,通过下标来判断是否需要分页,当下标为8时,说明需要放到第2页显示。

代码实现

// 定义一个计算属性
computed: {
	page() {
		let pages = []
		// 遍历iconList
		this.iconList.forEach((item, index) => {
			// 得到当前页码
			let page = Math.floor(index / 8)
			// 创建二维数组
			if (!pages[page]) {
				pages[page] = []
			}
			pages[page].push(item)
		})
		// 返回该二维数组
		return pages
	}
}

下面的步骤就是在组件遍历数组了,这个比较简单,就不写代码了。

8.1.3 better-scroll实现分类联动

Vue 实现仿美团外卖APP的总结_第8张图片
我希望滑动右边列表然后左边列表切换到对应的分类,并且样式也同步变化,同样的,我点击左边的分类右边也需要滑动到对应的分类位置。

分析

在渲染左边分类列表的时候,每一个分类项都会有一个下标index,当滑动右边分类列表的时候,我们希望动态计算出当前分类的currentIndex是否和左边的index相等,如果相等的话,就给左边当前分类添加样式就可以了。现在问题就是我们该如何动态计算右边当前分类的下标呢?思路是这样的:

  • 获取右边分类初始渲染后每个分类距离父级的高度,并且放到数组中
  • 监听右边分类的滑动事件,实时获取滑动距离
  • 如果滑动的距离在某个高度范围内,计算出那个高度范围内所在的分类下标
  • 判断左边分类下标是否等于右边分类下标决定是否显示样式

而点击左边分类项滑动右边的分类就比较简单了:

  • 得到左边被点击分类的下标
  • 使用scrollTo方法让右边滑动到对应下标的分类最顶端

实现

定义初始距离数据,实时滑动距离


data() {
 return {
 	scrollY: 0,
 	detailTops: []
 }
}

获取初始分类高度和实时滑动距离:

methods: {
    // 初始化Bscroll对象
    _initDetailScroll() {
      this.detailScroll = new BScroll('.foods-detail', {
        probeType: 3, // 滑动类型,必须指定,否则无法滑动
        click: true // 是否允许原生的click事件
      })
      // 监听滑动事件,实时获取滑动距离
      this.detailScroll.on('scroll', ({ y }) => {
        this.scrollY = Math.abs(y)
      })
      this.detailTops = this._getClientHeight('detail')
    },
   _getClientHeight(ele) {
	   const eleLists = [...this.$refs[ele].children]
	
	   let tempArr = []
	   let top = 0
	
	   tempArr.push(top) // 将第一个li元素的距离放入数组
	   eleLists.forEach(item => {
	     top += item.clientHeight
	     tempArr.push(top)
	   })
	   return tempArr
 },
}

定义计算属性currentIndex

computed: {
    currentIndex() {
      let index = 0
      index = this.detailTops.findIndex((ele, index, args) => {
        return this.scrollY >= ele && this.scrollY < args[index + 1]
      })
      return index
    }
}

而点击左边分类来让右边滑动到对应的位置就很简单了,只需要得出左边分类当前index,然后通过BSroll对象相关API就可以让右边滑动了:

 // 点击左边分类,右边滑到目标位置
 this.detailScroll.scrollTo(0, -this.detailTops[index], 300)

8.2 问题

8.2.1 使用better-scroll首次加载不滑动

这里我们需要注意Bscroll对象创建的时机,必须是数据请求到了之后才能创建这个对象,我们可以在进行异步请求后执行回调去创建Bscroll对象。但是创建Bscroll对象又涉及到了更新DOM,这又是一个异步的过程,所以我们需要使用this.$nextTick方法。

  mounted() {
    // 请求数据后才初始化Bscroll
    this.$store.dispatch('getShopLists', () => {
      this.$nextTick(() => {
        this._initCategoryScroll()
        this._initDetailScroll()
      })
    })
  },

然后我们在对应的action中进行判断,如果回调存在就执行,否则不执行。

  async getShopLists({ commit }, callback) {
    const { data } = await this.$axios.getShopFoodListsApi()
    if (data.code === 0) {
      commit({
        type: RECEIVE_SHOP_FOODS,
        foodLists: data['data']
      })
      callback && callback()
    }
  },

8.2.2 向已有数据对象添加新属性不会有响应式效果

官方推荐使用Vue.set方法来解决这个问题:

Vue.set(object, property, value)

九、总结

这篇文章写完花了2天多,我写下来发现有很多多余的东西,浪费了不该浪费的时间,写文章真的是一件很有技巧的事情,可能我学的还不够精细。另外,还有很多问题需要解决,我才发现前端这条路上的坑真的够多,没办法既然已经入坑,那就继续玩里头深挖吧。

参考

【1】 Vue.js
【2】Rem布局的原理解析
【3】vue中Axios的封装和API接口的管理
【4】视口简介

你可能感兴趣的:(Vue.js)