本文是学习 mpvue 源码的总结
使用 mpvue 模板
mpvue 有自己的 vue-cli 模板,基于 vue-cli@2.* 版本,具体初始化过程见 mpvue-docs | quickstart
1 | vue init mpvue/mpvue-quickstart my-mpvue-project |
下面,从 yarn run dev 开始进行分析
yarn run dev
yarn run dev
运行的脚本是:node build/dev-server.js wx
dev-server.js 整体流程
dev-server.js
中的 config
是从 ../config/index.js
中引入的:
1 | { build: |
Server 使用的是 express,使用 webpack 来进行构建,使用的 webpack 版本为:3.12.0,webpackConfig 来自于 webpack.dev.conf.js
首先,在 express 中使用 http-proxy-middleware
实现一些用户自定义的代理配置。
在 express 中使用 connect-history-api-fallback
来支持。有什么作用???
在对于路径进行处理的时候,使用的是 path.posix.join
这个方法,是为了在 Windows 操作系统上也支持类 Unix 类型的路径(POSIX 格式)
最后开启一个 Server,Server 首先找到可用的端口,然后启动 webpack-dev-middleware-hard-disk
,这个 webpack-dev-middleware-hard-disk
,是在 webpack-dev-middleware
基础上进行开发的。webpack-dev-middleware
这个库,使用在 development 环境下,可以监听文件的变化,然后自动执行编译函数,但是 webpack-dev-middleware
不会将编译后的输出写到硬盘上,而 webpack-dev-middleware-hard-disk
就是在其监听以及构建能力的基础上,将构建的结果输出到 webpackConfig.output.path 下面
dev-server.js webpackConfig 生成
1 | { entry: |
下面是几个需要关注的点:
Entry
entry 里面分了好几块:
1 | { |
app 是 src/main.js
,然后 pages 是 src/pages/**/main.js
。
每一个 entry 里面的内容都是一样的:
1 | import Vue from 'vue' |
说明每一个页面都被构建成单独的页面。也就侧面说明,小程序中的 pages 基本上都是独立的页面,而不是单页应用的形式。
target
target: require('mpvue-webpack-target')
1 | // mpvue webpack-target |
具体内容是什么呢,暂时不仔细分析,
output
1 | { path: '/Users/zhangyuchen/Documents/project/my-mpvue-project/dist/wx', |
resolve
1 | { extensions: [ '.js', '.vue', '.json' ], |
module.rules.mpvue-loader
主要是在下面的这个 rule 中指定解析 loader:
1 | { |
其中 vueLoaderConfig 的内容为:
1 | { loaders: |
在介绍完 plugins 之后,我们再对 mpvue-loader 中的源码进行介绍
plugins
1 | plugins: [ |
mpvue runtime
mpvue
这个库里面是 mpvue runtime 部分的代码,代码仓库在:Meituan-Dianping/mpvue | Github
mpvue 的源码入口文件在 /src/platforms/mpvue/entry-runtime.js
下面,主要做的是运行时将小程序的声明周期以及数据绑定映射到 Vue 本身的机制中。除了 runtime 以外,在 /src/platforms/mpvue/entry-compiler.js
中是编译的入口文件,主要做的是把 Vue 的代码在构建阶段编译成小程序使可以识别的代码。
entry-runtime.js
在 entry-runtime.js
中,首先添加一些平台相关的方法
1 | Vue.config.mustUseProp = mustUseProp // null,指定一个标签必须配套什么属性 |
1 | // 更新数据,里面的方法,将新旧vnode使用 diff算法进行比对,找出要替换的地方,这样更新dom的性能会有较大优化。最后会返回一个dom节点。 |
1 | // 挂载节点,将数据以及 vnode 对象挂载到真实的 DOM 上 |
1 | // render 时使用的更新数据的方法,一般为 render => (vm._render to get/set data)=> vnode => (vm._update) => vm.$el |
1 | // 将小程序的事件用 vue 的事件来进行处理 |
this._initMP
在挂载节点的时候,会使用 this._initMP
这个方法进行一层包装,我们来看一下这个方法中的内容。
目前看起来,是在小程序 APP 启动的时候,会进行一次 init 方法。小程序启动后,会对所有的页面都进行加载,现在所有的页面都会运行一次 init 方法。
_initMP
方法的内容是什么呢?很多,分步来看:
1 | // 在 Vue 的实例基础上,添加 $mp 属性,用来存放小程序相关的方法 |
this.$mp
是在哪里初始化的????
直接编译默认是 page,识别到启始页就是 app,component 是自定义的。
首先,当 mpType === 'app'
的时候,也就是注册 app
的时候:
1 | global.App({ |
小程序的声明周期中,global
中包含了五个方法,也就是小程序的五个初始化函数:
1 | global: { |
1 | // 小程序的生命周期中调用 Vue 模板中写的事件 |
当 mpType === 'page'
的时候
1 | const app = global.getApp() |
每一次页面初始化的时候,都会调用 rootVueVM._initDataToMP()
进行数据初始化,下面我们来看一下其中的代码:
1 | // rootVueVM._initDataToMP() |
每一次的 data 是会发生改变的,所以每一次在 onShow 的时候都需要调用 initDataToMP
1 | // data 的例子 |
其中,data
中数据的编号是如下定义的:
$root
后面的标签是从 0 开始表示最外层的根组件或者页面的数据。如果是该页面的子组件,则0,0
中后面的这个0
表示它的第一个子组件$k
指的是$root
本身的标识符$kk
指的是该组件或页面的子组件的标识符前缀$p
指的是该组件的父组件或页面的标识符
updateDataToMP 更新 Vue 对象的 Data
每一次页面事件,网络事件等对数据进行更新,都是通过 handleProxy
对 Vue 的数据进行更新,然后 Vue 的数据进行更新后,再对小程序页面的数据进行更新。更新小程序页面数据的方法,是通过修改 Vue 本身的 patch 方法,在对自身数据更新后,调用 updateDataToMP
来更新小程序的数据:
1 | // runtime/patch.js |
下面来看一下 updateDataToMP 做了什么:
1 | // 优化每次 setData 都传递大量新数据 |
声明周期
Vue 的声明周期:
- beforeCreate
- created
- beforeMount
- mounted
- beforeUpdate
- updated
- activated
- deactivated
- beforeDestroy
- destroyed
除了 Vue 本身的生命周期外,mpvue 还兼容了小程序生命周期,这部分生命周期钩子的来源于微信小程序的 Page, 除特殊情况外,不建议使用小程序的生命周期钩子。
app 部分:
- onLaunch,初始化
- onShow,当小程序启动,或从后台进入前台显示
- onHide,当小程序从前台进入后台
page 部分:
- onLoad,监听页面加载
- onShow,监听页面显示
- onReady,监听页面初次渲染完成
- onHide,监听页面隐藏
- onUnload,监听页面卸载
- onPullDownRefresh,监听用户下拉动作
- onReachBottom,页面上拉触底事件的处理函数
- onShareAppMessage,用户点击右上角分享
- onPageScroll,页面滚动
- onTabItemTap, 当前是 tab 页时,点击 tab 时触发 (mpvue 0.0.16 支持)
- onResize,页面尺寸改变时触发
Component 部分,目前 mpvue 不支持生成小程序的 Component,而是使用 template:
- created,在组件实例进入页面节点树时执行,注意此时不能调用 setData
- attached,在组件实例进入页面节点树时执行,不能获得节点信息
- ready,在组件布局完成后执行,此时可以获取节点信息
- moved,在组件实例被移动到节点树另一个位置时执行
- detached,在组件实例被从页面节点树移除时执行
mpvue compiler
在 runtime 的时候,主要处理的一些函数的绑定,以及生命周期的触发。但是小程序组件上定义的函数如何处理,什么时候对小程序的 data 进行更新,小程序的事件又如何处理呢,这些可以在 compiler 里面查看。
compiler 是被 wepback 在构建的时候调用的,入口文件在 platforms/mp/compiler/index.js
1 | import { baseOptions } from './options' |
compileToWxml
首先,先从 compileToWxml
开始看起。 compileToWxml
这个方法会接收两个参数,第一个参数是 compiled,这个 compiled 指的是从 Vue 的模板代码,被转化成 createElement 这种 JS 包裹的代码
1 | // compileToWxml |
我们先从最简单的例子看起:
1 | // logs.vue |
这个例子中,使用的最简单的在 Vue 的模板中绑定 data 中的数据。此时 compiled
参数就是经过 vue-template-compiler 处理后的代码:
1 | { ast: |
其中 render
部分被转换之后的代码,使用 _c
是 createElement
,_l
用 renderList
进行代替,可以获得 render
为
1 | // render |
下面,我们一点一点来看代码,首先:
1 | // log 方法,为 compiled 对象添加 mpErrors = [] 以及 mpTips = [],然后返回一个函数,向这两个数组中填充值 |
wxmlAst,将 Vue 解析结果转换成小程序语法树
1 | // wxmlAst 做的事情,就是将 Vue 编译出来的生成 vnode 的代码,转换成小程序的的语法树 |
我们来看一下将 createElement
解析成小程序的语法树的 wxmlAst
函数的内容:
1 | // wxmlAst |
tag 转换部分
1 | // tag 方法,进行 tag 的转换 |
attr 转换部分
1 | convertAttr (ast, log) { |
event,处理 vue 的事件转换到小程序的事件
1 | // key 是事件的名称,如 v-on:click,val 是赋给该事件的处理函数,如:bindViewTap,attr 是该节点的属性,tag 是该节点的标签名,这里的 tag 已经转换为小程序的标签名 |
handleProxy 的部分在 runtime 里面,在 runtime/lifecycle.js
有定义 handleProxy
这个方法,调用的是 handleProxyWithVue
:
1 | // runtime/lifecycle.js |
generate,将转换成的小程序语法树转换成小程序的代码
1 | //compiler/index.js |
compileToWxml 剩下的操作
1 | export function compileToWxml (compiled, options = {}) { |
这里会导出三个函数,但是这三个函数是怎么被调用的?又是在什么时候被调用的?从 mpvue compiler 中找不到,因为 mpvue-template-compiler 是通过 mpvue-loader 被引入到 webpack 中,所以这些问题都需要在 mpvue-loader 中找答案。
mpvue-loader
mpvue loader 的源码在 mpvue/mpvue-loader | Github 上