Vue初探 – 使用mpvue框架重构小程序 笔记

Vue初探

近期小程序调整了登录策略,如果强制要求用户登录的话是禁止上架的。团队的小程序项目是以个人日记和社交为基础的,上架阶段是遇到了层层阻碍。同时之前的小程序是在心情不好的时候完成的,代码较为混乱。于是决定使用mpvue框架进行重构,顺便学习一下Vue这个大名鼎鼎的前端框架。

我是通过官网的快速入门文档入手,同时参考了mpvue快速入门文档,对项目完成了重构工作。

接下来是我从对Vue的学习,以及mpvue和原生小程序开发之间的区别做的记录。

在写这篇文章的时候,我对小程序原生开发比较了解,也算是第一次通过大致阅读Vue官方文档粗略了解了一下Vue,所以我认为这篇文章更适合熟悉小程序原生开发,并且初入Vue的童鞋阅读。

创建项目

mpvue创建项目后,使用npm install安装依赖,然后npm run dev,项目是热重载的,所以每次保存时,会自动生成小程序代码。

入口文件分析

mpvue的入口文件是 项目目录/src/main.js,在 /src/ 目录下面还存在 app.vue, app.jsonpages文件夹componenets文件夹

* main.js 入口文件分析

import Vue from 'vue'
import App from './app'

// 阻止启动生产消息, 这个是控制浏览器的console页面是否打印某些vue相关信息的开关
Vue.config.productionTip = false;

// 与组件区分开,会以此参数创建Vue实例
App.mpType = 'app';

const app = new Vue(App);
app.$mount();

* 关于 app.$mount()

在一开始学习Vue的时候,demo代码是这样教我写入口文件的


<div id="vue_det"></div>

var vm = new Vue({
    el: '#app',
    data: {
    
    },
    methods: {
        details: function() {
            return "Hello World!";
        }
    }
})

或者通过挂载的方式

var vm = new Vue({
  router,
  store,
  render: h => h(App),
}).$mount('#app')

Vue的 $mount() 为手动挂载,在项目中可用于延时挂载(例如在挂载之前要进行一些其他操作、判断等),之后要手动挂载上。new Vue时,el$mount 并没有本质上的不同。

// Vue: src/core/instance/init.js _init()方法部分代码
if (vm.$options.el) {
    vm.$mount(vm.$options.el)
}

为什么mpvue必须通过 app.$mount() 的方式挂载

因为看mpvue的教程时经常听到一句话是说mpvue必须使用 app.$mount() 挂载,所以也不太保证 必须两个字是否太过于绝对。

new Vue(options) 的时候,调用了初始化方法 _init(options), 在 _init(options) 方法中,调用了如下方法

// Vue: src/core/instance/init.js _init()方法部分代码

// ...
callHook(vm, 'beforeCreate');
// ... 
callHook(vm, 'created')

我对比了一下vue和mpvue的 _init() 方法, 发现mpvue并没有做其他的改动。

接下来看 $mount() 方法

当然这个函数里面没有太多重点内容,只是说mpvue在系统方法的基础上,调用了一个 _initMP() 的方法

// Vue: src/platforms/web/runtime/index.js 部分代码

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

// ******** mpvue *********

// mpvue: src/platforms/mp/runtime/index.js 部分代码

Vue.prototype.$mount = function (el, hydrating) {
  const options = this.$options

  if (options && (options.render || options.mpType)) {
    const { mpType = 'page' } = options
    return this._initMP(mpType, () => {
      return mountComponent(this, undefined, undefined)
    })
  } else {
    return mountComponent(this, undefined, undefined)
  }
}

接下来看一下 _initMP() 方法

// mpvue: src/platforms/mp/runtime/lifecycle.js initMP() 部分代码

export function initMP (mpType, next) {
  
  // ...
  
  if (mp.status) {
    if (mpType === 'app') {
      callHook(this, 'onLaunch', mp.appOptions)
    } else {
      callHook(this, 'onLoad', mp.query)
      callHook(this, 'onReady')
    }
    return next()
  }
  // ...
}

所以我认为mpvue必须使用$mount()挂载是为了触发小程序的部分生命周期函数: onLaunch, onLoad, onReady

编写第一个界面

初学Vue的时候会很难受,觉得单页面应用不好调试。看了一个Vue项目的源代码(非mpvue),发现他的项目中每个页面只有.vue文件。
而在mpvue中,每个页面都会包含一个main.js。 最后编译成功之后,每个页面都会有js,css,html文件等,我认为更像是 一个page是一个Vue应用。

* props 和 data

data 像小程序的data,是当前页面的数据源,可以理解为是当前页面私有的数据。
props 是通过父组件传递下来的,在当前页面是只读的。

关于data是字典还是函数的问题

Vue中data有两种写法:

// 1.
data: {
  count: 0
}

// 2.
data: function () {
  return {
    count: 0
  }
}

一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝.

参考官方文档: https://cn.vuejs.org/v2/guide/components.html#data-%E5%BF%85%E9%A1%BB%E6%98%AF%E4%B8%80%E4%B8%AA%E5%87%BD%E6%95%B0

* 生命周期函数

created 和 mounted

created 会在实例创建完成后即被调用。 但是在上面提到,我认为mpvue构建的小程序 一个page就是一个实例,所以created会在小程序启动时创建多次,即一开始启动每个页面都会调用一次created,所以我认为为了效率问题尽量减少在此页面做太多的事情

mounted 在进入页面时调用。我认为和onLoad()类似,在上述代码中可以看到,应该是先调用 小程序原生的 onLoad() 等,才会调用Vue的 mounted() 方法

* export default 作用 module.exports

因为个人并不是主力学js的,所以很多知识点都只是知道了就用用,所在在我的原生项目中使用了大量的 module.exports, 而在mpvue的demo中都是使用的 export default, 这也算是JS的一个基础知识点吧。

这里应该涉及到两种用法

export default

ES6的写法, 使用 import 导入模块, 使用 export 导出模块, 使用 export default 为模块指定默认输出

module.exports

CommonJS的写法, 使用 require 导入模块, 使用 module.exports 暴露需要导出的接口。

* bindtap和catchtap

什么是事件冒泡

当一个子元素被点击的时候,不仅仅这个元素本身被点击了,因为这个元素也在其上一级父元素中(属于父级元素的地盘),所以相当于其父元素也被点击了,以此类推,一层一层往外推,最终整个文档也是被点击了,如果每个层级的节点元素都绑定了click事件,那么每个节点的click事件函数都会被执行。

bindtap和catchtap 的区别

bind事件绑定不会阻止冒泡事件向上冒泡,catch事件绑定可以阻止冒泡事件向上冒泡。

@click.stop 和 @click.self 的区别

@click.stop 阻止事件冒泡,即点击子试图时不会向上传递到父视图

@click.self 只当在 event.target 是当前元素自身时触发处理函数, 即事件不是从内部元素触发的.

在本项目中有一个需求,在半透明背景色div上有一个子视图,要求点击背景色div,就触发 dismiss 事件,如果单纯在背景div上添加 @click="dismiss" 或者 @tap="dismiss" ,那么点击子视图的时候也 会触发 dismiss事件, 所以现在要使用 @click.self="dismiss" 来确定点击的是背景色自己,而不是子视图。

* style scoped

在vue文件中的style标签上,有一个特殊的属性:scoped。当一个style标签拥有scoped属性时,它的CSS样式就只能作用于当前的组件,也就是说,该样式只能适用于当前组件元素。通过该属性,可以使得组件之间的样式不互相污染。如果一个项目中的所有style标签全部加上了scoped,相当于实现了样式的模块化。

v-for :key

key可以保证数据的唯一性, 这其中涉及到Vue的Diff算法

下面是网上的一个例子,

Diff算法的默认实现

即把C更新成F,D更新成C,E更新成D,最后再插入E

如果使用key作为每个节点的唯一标识,则Diff算法可以正确识别各个节点,变成下面的样子

* onLoad options 在mpvue的使用

如果在mpvue的 mounted() 方法中想要使用界面传过来的值,则可以 通过 this.$root.$mp.query 的方式调用;
如果是使用了原生小程序的 onLoad(options) 方法,则仍可以正常使用。

* 下拉刷新,上拉加载更多等不生效的问题

在实现下拉刷新,上拉加载更多时,发现功能不生效,于是对代码进行了如下排查。

  1. 检查是否在json中设置了 "enablePullDownRefresh": true
  2. 检查相应函数是否实现

在经过上述检查都正确之后,发现还是不生效。最后看了一下,发现有这样一个问题:
我们在实现mounted(), created(), onLoad()等方法时,都是直接在 export default 下实现的,而点击事件是在 methods 字典下实现的

重点来了,如果下拉刷新事件是写在 methods 下面的,则不会生效,如果像 mounted() 直接写在模块下,就可以正常下拉刷新。

export default {
    data () {
        a: 'Hello'
    },
    
    mounted () {
    
    },
    
    // 正确: 下拉刷新
    onPullDownRefresh () {
        // ...
    }
    
    methods () {
        // 错误: 下拉刷新
        onPullDownRefresh () {
            
            // 不生效
        }
    
        // 点击事件
        addButtonClick () {
        
        },
    }
}

还有两个比较高深的东西,以后再去填坑

什么是vuex

ESLint