mpvue

Vue.js 小程序版, fork 自 vuejs/vue@2.4.1,保留了 vue runtime 能力,添加了小程序平台的支持。mpvue 是一个使用 Vue.js 开发小程序的前端框架。框架基于 Vue.js 核心,mpvue 修改了 Vue.js 的 runtime 和 compiler 实现,使其可以运行在小程序环境中,从而为小程序开发引入了整套 Vue.js 开发体验。

框架原理

两个大方向

  • 通过mpvue提供 mp 的 runtime 适配小程序
  • 通过mpvue-loader产出微信小程序所需要的文件结构和模块内容。

七个具体问题

要了解 mpvue 原理必然要了解 Vue 原理,这是大前提。但是要讲清楚 Vue 原理需要花费大量的篇幅,不如参考learnVue

现在假设您对 Vue 原理有个大概的了解。

由于 Vue 使用了 Virtual DOM,所以 Virtual DOM 可以在任何支持 JavaScript 语言的平台上操作,譬如说目前 Vue 支持浏览器平台或 weex,也可以是 mp(小程序)。那么最后 Virtual DOM 如何映射到真实的 DOM 节点上呢?vue为平台做了一层适配层,浏览器平台见 runtime/node-ops.js、weex平台见runtime/node-ops.js,小程序见runtime/node-ops.js。不同平台之间通过适配层对外提供相同的接口,Virtual DOM进行操作Real DOM节点的时候,只需要调用这些适配层的接口即可,而内部实现则不需要关心,它会根据平台的改变而改变。

所以思路肯定是往增加一个 mp 平台的 runtime 方向走。但问题是小程序不能操作 DOM,所以 mp 下的node-ops.js 里面的实现都是直接 return obj

新 Virtual DOM 和旧 Virtual DOM 之间需要做一个 patch,找出 diff。patch完了之后的 diff 怎么更新视图,也就是如何给这些 DOM 加入 attr、class、style 等 DOM 属性呢? Vue 中有 nextTick 的概念用以更新视图,mpvue这块对于小程序的 setData 应该怎么处理呢?

另外个问题在于小程序的 Virtual DOM 怎么生成?也就是怎么将 template 编译成render function。这当中还涉及到运行时-编译器-vs-只包含运行时,显然如果要提高性能、减少包大小、输出 wxml、mpvue 也要提供预编译的能力。因为要预输出 wxml 且没法动态改变 DOM,所以动态组件,自定义 render,和<script type="text/x-template"> 字符串模版等都不支持(参考)。

另外还有一些其他问题,最后总结一下

  • 1.如何预编译生成render function
  • 2.如何预编译生成 wxml,wxss,wxs
  • 3.如何 patch 出 diff
  • 4.如何更新视图
  • 5.如何建立小程序事件代理机制,在事件代理函数中触发与之对应的vue组件事件响应
  • 6.如何建立vue实例与小程序 Page 实例关联
  • 7.如何建立小程序和vue生命周期映射关系,能在小程序生命周期中触发vue生命周期

platform/mp的目录结构)

  1. .
  2. ├── compiler //解决问题1,mpvue-template-compiler源码部分
  3. ├── runtime //解决问题3 4 5 6 7
  4. ├── util //工具方法
  5. ├── entry-compiler.js //mpvue-template-compiler的入口。package.json相关命令会自动生成mpvue-template-compiler这个package。
  6. ├── entry-runtime.js //对外提供Vue对象,当然是mpvue
  7. └── join-code-in-build.js //编译出SDK时的修复

后面的内容逐步解答这几个问题,也就弄明白了原理

mpvue-loader

mpvue-loadervue-loader 的一个扩展延伸版,类似于超集的关系,除了vue-loader 本身所具备的能力之外,它还会利用mpvue-template-compiler生成render function

  • entry

它会从 webpack 的配置中的 entry 开始,分析依赖模块,并分别打包。在entry 中 app 属性及其内容会被打包为微信小程序所需要的 app.js/app.json/app.wxss,其余的会生成对应的页面page.js/page.json/page.wxml/page.wxss,如示例的 entry 将会生成如下这些文件,文件内容下文慢慢讲来:

  1. // webpack.config.js
  2. {
  3. // ...
  4. entry: {
  5. app: resolve('./src/main.js'), // app 字段被识别为 app 类型
  6. index: resolve('./src/pages/index/main.js'), // 其余字段被识别为 page 类型
  7. 'news/home': resolve('./src/pages/news/home/index.js')
  8. }
  9. }
  10. // 产出文件的结构
  11. .
  12. ├── app.js
  13. ├── app.json
  14. ├──· app.wxss
  15. ├── components
  16. ├── card$74bfae61.wxml
  17. ├── index$023eef02.wxml
  18. └── news$0699930b.wxml
  19. ├── news
  20. ├── home.js
  21. ├── home.wxml
  22. └── home.wxss
  23. ├── pages
  24. └── index
  25. ├── index.js
  26. ├── index.wxml
  27. └── index.wxss
  28. └── static
  29. ├── css
  30. ├── app.wxss
  31. ├── index.wxss
  32. └── news
  33. └── home.wxss
  34. └── js
  35. ├── app.js
  36. ├── index.js
  37. ├── manifest.js
  38. ├── news
  39. └── home.js
  40. └── vendor.js
  • wxml每一个 .vue 的组件都会被生成为一个 wxml 规范的 template,然后通过 wxml 规范的 import 语法来达到一个复用,同时组件如果涉及到 props 的 data 数据,我们也会做相应的处理,举个实际的例子:
  1. <template>
  2. <div class="my-component" @click="test">
  3. <h1>{{msg}}</h1>
  4. <other-component :msg="msg"></other-component>
  5. </div>
  6. </template>
  7. <script>
  8. import otherComponent from './otherComponent.vue'
  9. export default {
  10. components: { otherComponent },
  11. data () {
  12. return { msg: 'Hello Vue.js!' }
  13. },
  14. methods: {
  15. test() {}
  16. }
  17. }
  18. </script>

这样一个 Vue 的组件的模版部分会生成相应的 wxml

  1. <import src="components/other-component$hash.wxml" />
  2. <template name="component$hash">
  3. <view class="my-component" bindtap="handleProxy">
  4. <view class="_h1">{{msg}}</view>
  5. <template is="other-component$hash" wx:if="{{ $c[0] }}" data="{{ ...$c[0] }}"></template>
  6. </view>
  7. </template>

可能已经注意到了 other-component(:msg=”msg”) 被转化成了 。mpvue 在运行时会从根组件开始把所有的组件实例数据合并成一个树形的数据,然后通过 setData 到 appData,$c是 $children 的缩写。至于那个 0 则是我们的 compiler 处理过后的一个标记,会为每一个子组件打一个特定的不重复的标记。 树形数据结构如下:

  1. // 这儿数据结构是一个数组,index 是动态的
  2. {
  3. $child: {
  4. '0'{
  5. // ... root data
  6. $child: {
  7. '0': {
  8. // ... data
  9. msg: 'Hello Vue.js!',
  10. $child: {
  11. // ...data
  12. }
  13. }
  14. }
  15. }
  16. }
  17. }
  • wxss

这个部分的处理同 web 的处理差异不大,唯一不同在于通过配置生成 .css 为 .wxss ,其中的对于 css 的若干处理,在 postcss-mpvue-wxss 和 px2rpx-loader 这两部分的文档中又详细的介绍。

app.json/page.json1.1.1 以上

推荐和小程序一样,将 app.json/page.json 放到页面入口处,使用 copy-webpack-plugin copy 到对应的生成位置。

1.1.1 以下

这部分内容来源于 app 和 page 的 entry 文件,通常习惯是 main.js,你需要在你的入口文件中 export default { config: {} },这才能被我们的 loader 识别为这是一个配置,需要写成 json 文件。

  1. import Vue from 'vue';
  2. import App from './app';
  3. const vueApp = new Vue(App);
  4. vueApp.$mount();
  5. // 这个是我们约定的额外的配置
  6. export default {
  7. // 这个字段下的数据会被填充到 app.json / page.json
  8. config: {
  9. pages: ['static/calendar/calendar', '^pages/list/list'], // Will be filled in webpack
  10. window: {
  11. backgroundTextStyle: 'light',
  12. navigationBarBackgroundColor: '#455A73',
  13. navigationBarTitleText: '美团汽车票',
  14. navigationBarTextStyle: '#fff'
  15. }
  16. }
  17. };

同时,这个时候,我们会根据 entry 的页面数据,自动填充到 app.json 中的 pages 字段。 pages 字段也是可以自定义的,约定带有 ^ 符号开头的页面,会放到数组的最前面。

style scoped在 vue-loader 中对 style scoped 的处理方式是给每个样式加一个 attr 来标记 module-id,然后在 css 中也给每条 rule 后添加 [module-id],最终可以形成一个 css 的“作用域空间”。

在微信小程序中目前是不支持 attr 选择器的,所以我们做了一点改动,把 attr 上的 [module-id] 直接写到了 class 里,如下:

  1. <!-- .vue -->
  2. <template>
  3. <div class="container">
  4. // ...
  5. </div>
  6. </template>
  7. <style scoped>
  8. .container {
  9. color: red;
  10. }
  11. </style>
  12. <!-- vue-loader -->
  13. <template>
  14. <div class="container" data-v-23e58823>
  15. // ...
  16. </div>
  17. </template>
  18. <style scoped>
  19. .container[data-v-23e58823] {
  20. color: red;
  21. }
  22. </style>
  23. <!-- mpvue-loader -->
  24. <template>
  25. <div class="container data-v-23e58823">
  26. // ...
  27. </div>
  28. </template>
  29. <style scoped>
  30. .container.data-v-23e58823 {
  31. color: red;
  32. }
  33. </style>
  • compiler

生产出的内容是:

  1. (function(module, __webpack_exports__, __webpack_require__) {
  2. "use strict";
  3. // mpvue-template-compiler会利用AST预编译生成一个render function用以生成Virtual DOM。
  4. var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
  5. // _c创建虚拟节点,参考https://github.com/Meituan-Dianping/mpvue/blob/master/packages/mpvue/index.js#L3606
  6. // 以及https://github.com/Meituan-Dianping/mpvue/blob/master/packages/mpvue/index.js#L3680
  7. return _c('div', {
  8. staticClass: "my-component"
  9. }, [_c('h1', [_vm._v(_vm._s(_vm.msg))]), _vm._v(" "), _c('other-component', {
  10. attrs: {
  11. "msg": _vm.msg,
  12. "mpcomid": '0'
  13. }
  14. })], 1)
  15. }
  16. // staticRenderFns的作用是静态渲染,在更新时不会进行patch,优化性能。而staticRenderFns是个空数组。
  17. var staticRenderFns = []
  18. render._withStripped = true
  19. var esExports = { render: render, staticRenderFns: staticRenderFns }
  20. /* harmony default export */ __webpack_exports__["a"] = (esExports);
  21. if (false) {
  22. module.hot.accept()
  23. if (module.hot.data) {
  24. require("vue-hot-reload-api").rerender("data-v-54ad9125", esExports)
  25. }
  26. }
  27. /***/ })

compiler

compiler相关,也就是template预编译这块,可以参考《聊聊Vue的template编译》来搞明白。原理是一样的。

mpvue自己实现了export { compile, compileToFunctions, compileToWxml }(链接)其中compileToWxml是用来生成wxml,具体代码在这

另外mpvue是不需要提供运行时-编译器的,虽然理论上是能够做到的。因为小程序不能操作DOM,即便提供了运行时-编译器也产生不了界面。

详细讲解compile过程:

1.将vue文件解析成模板对象

  1. // mpvue-loader/lib/loader.js
  2. var parts = parse(content, fileName, this.sourceMap)

假如vue文件源码如下:

  1. <template>
  2. <view class="container-bg">
  3. <view class="home-container">
  4. <home-quotation-view v-for="(item, index) in lists" :key="index" :reason="item.reason" :stockList="item.list" @itemViewClicked="itemViewClicked" />
  5. </view>
  6. </view>
  7. </template>
  8. <script lang="js">
  9. import homeQuotationView from '@/components/homeQuotationView'
  10. import topListApi from '@/api/topListApi'
  11. export default {
  12. data () {
  13. return {
  14. lists: []
  15. }
  16. },
  17. components: {
  18. homeQuotationView
  19. },
  20. methods: {
  21. async loadRankList () {
  22. let {data} = await topListApi.rankList()
  23. if (data) {
  24. this.dateTime = data.dt
  25. this.lists = data.rankList.filter((item) => {
  26. return !!item
  27. })
  28. }
  29. },
  30. itemViewClicked (quotationItem) {
  31. wx.navigateTo({
  32. url: `/pages/topListDetail/main?item=${JSON.stringify(quotationItem)}`
  33. })
  34. }
  35. },
  36. onShow () {
  37. this.loadRankList()
  38. }
  39. }
  40. </script>
  41. <style lang="stylus" scoped>
  42. .container-bg
  43. width 100%
  44. height 100%
  45. background-color #F2F4FA
  46. .home-container
  47. width 100%
  48. height 100%
  49. overflow-x hidden
  50. </style>

调用parse(content, fileName, this.sourceMap) 函数得到的结果大致如下:

  1. {
  2. template: {
  3. type: 'template',
  4. content: '\n<view class="container-bg">\n <view class="home-container">\n <home-quotation-view v-for="(item, index) in lists" :key="index" :reason="item.reason" :stockList="item.list" @itemViewClicked="itemViewClicked" />\n </view>\n</view>\n',
  5. start: 10,
  6. attrs: {},
  7. end: 251
  8. },
  9. script: {
  10. type: 'script',
  11. content: '\n\n\n\n\n\n\n\n\nimport homeQuotationView from \'@/components/homeQuotationView\'\nimport topListApi from \'@/api/topListApi\'\n\nexport default {\n data () {\n return {\n lists: []\n }\n },\n components: {\n homeQuotationView\n },\n methods: {\n async loadRankList () {\n let {data} = await topListApi.rankList()\n if (data) {\n this.dateTime = data.dt\n this.lists = data.rankList.filter((item) => {\n return !!item\n })\n }\n },\n itemViewClicked (quotationItem) {\n wx.navigateTo({\n url: `/pages/topListDetail/main?item=${JSON.stringify(quotationItem)}`\n })\n }\n },\n onShow () {\n this.loadRankList()\n }\n}\n',
  12. start: 282,
  13. attrs: {
  14. lang: 'js'
  15. },
  16. lang: 'js',
  17. end: 946,
  18. ...
  19. },
  20. styles: [{
  21. type: 'style',
  22. content: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.container-bg\n width 100%\n height 100%\n background-color #F2F4FA\n\n.home-container\n width 100%\n height 100%\n overflow-x hidden\n\n',
  23. start: 985,
  24. attrs: [Object],
  25. lang: 'stylus',
  26. scoped: true,
  27. end: 1135,
  28. ...
  29. }],
  30. customBlocks: []
  31. }

2.调用mpvue-loader/lib/template-compiler/index.js导出的接口并传入上面得到的html模板:

  1. var templateCompilerPath = normalize.lib('template-compiler/index')
  2. ...
  3. var defaultLoaders = {
  4. html: templateCompilerPath + templateCompilerOptions,
  5. css: options.extractCSS
  6. ? getCSSExtractLoader()
  7. : styleLoaderPath + '!' + 'css-loader' + cssLoaderOptions,
  8. js: hasBuble ? ('buble-loader' + bubleOptions) : hasBabel ? babelLoaderOptions : ''
  9. }
  10. // check if there are custom loaders specified via
  11. // webpack config, otherwise use defaults
  12. var loaders = Object.assign({}, defaultLoaders, options.loaders)
  1. 调用mpvue/packages/mpvue-template-compiler/build.js的compile接口:
  1. // mpvue-loader/lib/template-compiler/index.js
  2. var compiled = compile(html, compilerOptions)

compile方法生产下面的ast(Abstract Syntax Tree)模板,render函数和staticRenderFns

  1. {
  2. ast: {
  3. type: 1,
  4. tag: 'view',
  5. attrsList: [],
  6. attrsMap: {
  7. class: 'container-bg'
  8. },
  9. parent: undefined,
  10. children: [{
  11. type: 1,
  12. tag: 'view',
  13. attrsList: [],
  14. attrsMap: {
  15. class: 'home-container'
  16. },
  17. parent: {
  18. type: 1,
  19. tag: 'view',
  20. attrsList: [],
  21. attrsMap: {
  22. class: 'container-bg'
  23. },
  24. parent: undefined,
  25. children: [
  26. [Circular]
  27. ],
  28. plain: false,
  29. staticClass: '"container-bg"',
  30. static: false,
  31. staticRoot: false
  32. },
  33. children: [{
  34. type: 1,
  35. tag: 'home-quotation-view',
  36. attrsList: [{
  37. name: ':reason',
  38. value: 'item.reason'
  39. }, {
  40. name: ':stockList',
  41. value: 'item.list'
  42. }, {
  43. name: '@itemViewClicked',
  44. value: 'itemViewClicked'
  45. }],
  46. attrsMap: {
  47. 'v-for': '(item, index) in lists',
  48. ':key': 'index',
  49. ':reason': 'item.reason',
  50. ':stockList': 'item.list',
  51. '@itemViewClicked': 'itemViewClicked',
  52. 'data-eventid': '{{\'0-\'+index}}',
  53. 'data-comkey': '{{$k}}'
  54. },
  55. parent: [Circular],
  56. children: [],
  57. for: 'lists',
  58. alias: 'item',
  59. iterator1: 'index',
  60. key: 'index',
  61. plain: false,
  62. hasBindings: true,
  63. attrs: [{
  64. name: 'reason',
  65. value: 'item.reason'
  66. }, {
  67. name: 'stockList',
  68. value: 'item.list'
  69. }, {
  70. name: 'eventid',
  71. value: '\'0-\'+index'
  72. }, {
  73. name: 'mpcomid',
  74. value: '\'0-\'+index'
  75. }],
  76. events: {
  77. itemViewClicked: {
  78. value: 'itemViewClicked',
  79. modifiers: undefined
  80. }
  81. },
  82. eventid: '\'0-\'+index',
  83. mpcomid: '\'0-\'+index',
  84. static: false,
  85. staticRoot: false,
  86. forProcessed: true
  87. }],
  88. plain: false,
  89. staticClass: '"home-container"',
  90. static: false,
  91. staticRoot: false
  92. }],
  93. plain: false,
  94. staticClass: '"container-bg"',
  95. static: false,
  96. staticRoot: false
  97. },
  98. render: 'with(this){return _c(\'view\',{staticClass:"container-bg"},[_c(\'view\',{staticClass:"home-container"},_l((lists),function(item,index){return _c(\'home-quotation-view\',{key:index,attrs:{"reason":item.reason,"stockList":item.list,"eventid":\'0-\'+index,"mpcomid":\'0-\'+index},on:{"itemViewClicked":itemViewClicked}})}))])}',
  99. staticRenderFns: [],
  100. errors: [],
  101. tips: []
  102. }

其中的render函数运行的结果是返回VNode对象,其实render函数应该长下面这样:

(function() {
  with(this){
    return _c('div',{   //创建一个 div 元素
      attrs:{"id":"app"}  //div 添加属性 id
      },[
        _m(0),  //静态节点 header,此处对应 staticRenderFns 数组索引为 0 的 render 函数
        _v(" "), //空的文本节点
        (message) //三元表达式,判断 message 是否存在
         //如果存在,创建 p 元素,元素里面有文本,值为 toString(message)
        ?_c('p',[_v("\n    "+_s(message)+"\n  ")])
        //如果不存在,创建 p 元素,元素里面有文本,值为 No message. 
        :_c('p',[_v("\n    No message.\n  ")])
      ]
    )
  }
})

其中的_c就是vue对象的createElement方法 (创建元素),_mrenderStatic(渲染静态节点),_vcreateTextVNode(创建文本dom),_stoString (转换为字符串)

// src/core/instance/render.js
export function initRender (vm: Component) {
  ...
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  ...
}

...
Vue.prototype._s = toString
...
Vue.prototype._m = renderStatic
...
Vue.prototype._v = createTextVNode
...
  1. 调用compileWxml方法生产wxml模板,这个方法最终会调用 mpvue/packages/mpvue-template-compiler/build.js的compileToWxml方法将第一步compile出来的模板转成小程序的wxml模板
// mpvue-loader/lib/template-compiler/index.js
compileToWxml.call(this, compiled, html)

以上解答了问题1、2

runtime

目录结构)

.
├── events.js //解答问题5
├── index.js //入口提供Vue对象,以及$mount,和各种初始化
├── liefcycle //解答问题6、7
├── node-ops.js //操作真实DOM的相关实现,因为小程序不能操作DOM,所以这里都是直接返回
├── patch.js //解答问题3
└── render.js //解答问题4

patch.js

和vue使用的createPatchFunction保持一致,任然是旧树和新树进行patch产出diff,但是多了一行this.$updateDataToMP()用以更新。

render.js

两个核心的方法initDataToMPupdateDataToMP

initDataToMP收集vm上的data,然后调用小程序Page示例的setData渲染。

updateDataToMP在每次patch,也就是依赖收集发现数据改变时更新(参考patch.js代码),这部分一样会使用nextTick和队列。最终使用了节流阀throttleSetData。50毫秒用来控制频率以解决频繁修改Data,会造成大量传输Data数据而导致的性能问题。

其中collectVmData最终也是用到了formatVmData。尤其要注意的是一句注释:

getVmData 这儿获取当前组件内的所有数据,包含 props、computed 的数据

我们又知道,service到view是两个线程间通信,如果Data含有大量数据,增加了传输数据量,加大了传输成本,将会造成性能下降。

events.js

正如官网所说的,这里使用eventTypeMap做了各事件的隐射

import { getComKey, eventTypeMap } from '../util/index'
// 用于小程序的 event type 到 web 的 event
export const eventTypeMap = {
  tap: ['tap', 'click'],
  touchstart: ['touchstart'],
  touchmove: ['touchmove'],
  touchcancel: ['touchcancel'],
  touchend: ['touchend'],
  longtap: ['longtap'],
  input: ['input'],
  blur: ['change', 'blur'],
  submit: ['submit'],
  focus: ['focus'],
  scrolltoupper: ['scrolltoupper'],
  scrolltolower: ['scrolltolower'],
  scroll: ['scroll']
}

使用了handleProxyWithVue方法来代理小程序事件到vue事件。

另外看下作者自己对这部分的思路

事件代理机制:用户交互触发的数据更新通过事件代理机制完成。在 Vue.js 代码中,事件响应函数对应到组件的 method, Vue.js 自动维护了上下文环境。然而在小程序中并没有类似的机制,又因为 Vue.js 执行环境中维护着一份实时的虚拟 DOM,这与小程序的视图层完全对应,我们思考,在小程序组件节点上触发事件后,只要找到虚拟 DOM 上对应的节点,触发对应的事件不就完成了么;另一方面,Vue.js 事件响应如果触发了数据更新,其生命周期函数更新将自动触发,在此函数上同步更新小程序数据,数据同步也就实现了。

getHandle这个方法应该就是作者思路当中所说的:找到对应节点,然后找到handle。

lifecycle.js

initMP方法中,自己创建小程序的App、Page。实现生命周期相关方法,使用callHook代理兼容小程序App、Page的生命周期。

官方文档生命周期中说到了:

同 vue,不同的是我们会在小程序 onReady 后,再去触发 vue mounted 生命周期

这部分查看,onReady之后才会执行next,这个next回调最终是vue的mountComponent。可以在index.js中看到。这部分代码也就是解决了”小程序生命周期中触发vue生命周期”。

export function initMP (mpType, next) {
  // ...
    global.Page({
      // 生命周期函数--监听页面初次渲染完成
      onReady () {
        mp.status = 'ready'

        callHook(rootVueVM, 'onReady')
        next()
      },
    })
  // ...
}

在小程序onShow时,使用$nextTick去第一次渲染数据,参考上面提到的render.js。

export function initMP (mpType, next) {
  // ...
  global.Page({
    // 生命周期函数--监听页面显示
    onShow () {
      mp.page = this
      mp.status = 'show'
      callHook(rootVueVM, 'onShow')

      // 只有页面需要 setData
      rootVueVM.$nextTick(() => {
        rootVueVM._initDataToMP()
      })
    },
  })
  // ...
}

在mpvue-loader生成template时,比如点击事件@click会变成bindtap="handleProxy",事件绑定全都会使用handleProxy这个方法。

可以查看上面mpvue-loader回顾一下。

最终handleProxy调用的是event.js中的handleProxyWithVue

export function initMP (mpType, next) {
  // ...
    global.Page({
      handleProxy (e) {
        return rootVueVM.$handleProxyWithVue(e)
      },
    })
  // ...
}

index.js

最后index.js就负责各种初始化和mount。

Class和Style为什么暂不支持组件

原因:目前的组件是使用小程序的 template 标签实现的,给组件指定的class和style是挂载在template标签上,而template 标签不支持 class 及 style 属性。

解决方案: 在自定义组件上绑定class或style到一个props属性上。

 // 组件ComponentA.vue
 <template>
  <div class="container" :class="pClass">
    ...
  </div>
</template>
<script>
    export default {
    props: {
      pClass: {
        type: String,
        default: ''
      }
    }
  }
</script>
<!--PageB.vue-->
<template>
    <component-a :pClass="cusComponentAClass"  />
</template>
<script>
data () {
    return {
      cusComponentAClass: 'a-class b-class'
    }
  }
</script>
<style lang="stylus" scoped>
  .a-class
    border red solid 2rpx
  .b-class
    margin-right 20rpx
</style>

但是这样会有问题就是style加上scoped之后,编译模板生成的代码是下面这样的:

 .a-class.data-v-8f1d914e {
   border: #f00 solid 2rpx;
 }
 .b-class.data-v-8f1d914e {
   margin-right 20rpx
 }

所以想要这些组件的class生效就不能使用scoped的style,改成下面这样,最好自己给a-class和b-class加前缀以防其他的文件引用这些样式:

 <style lang="stylus">
  .a-class
    border red solid 2rpx
  .b-class
    margin-right 20rpx
</style>

<style lang="stylus" scoped>
  .other-class
    border red solid 2rpx

   ...
</style>
  • 在定义组件上绑定style属性到一个props属性上:
 <!--P组件ComponentA.vue-->
 <template>
  <div class="container" :style="pStyle">
    ...
  </div>
</template>
<script>
  export default {
    props: {
      pStyle: {
        type: String,
        default: ''
      }
    }
  }
</script>
<!--PageB.vue-->
<template>
    <component-a :pStyle="cusComponentAStyle"  />
</template>
<script>
const cusComponentAStyle = 'border:red solid 2rpx; margin-right:20rpx;'
data () {
    return {
      cusComponentAStyle
    }
  }
</script>
<style lang="stylus" scoped>
  ...
</style>

也可以通过定义styleObject,然后通过工具函数转化为styleString,如下所示:

const bstyle = {
  border: 'red solid 2rpx',
  'margin-right': '20rpx'
}
let arr = []
for (let [key, value] of Object.entries(bstyle)) {
  arr.push(`${key}: ${value}`)
}

const cusComponentAStyle = arr.join('; ')
  • 当然自定义组件确定只会改变某个css样式,通过pros传入单个样式的值,然后通过:style绑定肯定没问题:
<!--组件ComponentA.vue-->
 <template>
  <div class="container" :style="{'background-color': backgroundColor}">
    ...
  </div>
</template>
<script>
    export default {
    props: {
      backgroundColor: {
        type: String,
        default: 'yellow'
      }
    }
  }
</script>
<!-- PageB.vue -->
<template>
    <component-a backgroundColor="red"  />
</template>

分包加载

package.json修改

  • 升级: “mpvue-loader”: “\^1.1.2-rc.4” “webpack-mpvue-asset-plugin”: “\^0.1.1”
  • 新增: “relative”: “\^3.0.2”

注意事项

  • 1.1.2-rc.5 修复 slot 文件路径生成错误的问题
  • 1.1.x 版本还不是很稳定,对稳定性要求较高的项目建议暂时使用 1.0.x 版本

移动src/main.js中config相关内容到同级目录下main.json(新建)中

export default {
  // config: {...} 需要移动
}

to

{
 "pages": [
   "pages/index/main",
   "pages/logs/main"
  ],
  "subPackages": [
    {
      "root": "pages/packageA",
     "pages": [
       "counter/main"
     ]
   }
 ],
 "window": {...}
}

webpack 配置配合升级指南

  • 本次升级意在调整生成文件目录结构,对依赖的文件由原来的写死绝对路径该改为相对路径
  • mpvue-loader@1.1.2-rc.4 依赖 webpack-mpvue-asset-plugin@0.1.0 做依赖资源引用
  • 之前写在 main.js 中的 config 信息,需要在 main.js 同级目录下新建 main.json 文件,使用 webapck-copy-plugin copy 到 build 目录下
  • app.json 中引用的图片不会自动 copy 到 dist 目录下json 配置文件是由 webapck-copy-plugin copy 过去的,不会处理依赖,可以将图片放到根目录下 static 目录下,使用 webapck-copy-plugin copy 过去

build/webpack.base.conf.js

+var CopyWebpackPlugin = require('copy-webpack-plugin')
+var relative = require('relative')

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

-function getEntry (rootSrc, pattern) {
-  var files = glob.sync(path.resolve(rootSrc, pattern))
-  return files.reduce((res, file) => {
-    var info = path.parse(file)
-    var key = info.dir.slice(rootSrc.length + 1) + '/' + info.name
-    res[key] = path.resolve(file)
-    return res
-  }, {})
+function getEntry (rootSrc) {
+  var map = {};
+  glob.sync(rootSrc + '/pages/**/main.js')
+  .forEach(file => {
+    var key = relative(rootSrc, file).replace('.js', '');
+    map[key] = file;
+  })
+   return map;
 }

   plugins: [
-    new MpvuePlugin()
+    new MpvuePlugin(),
+    new CopyWebpackPlugin([{
+      from: '**/*.json',
+      to: 'app.json'
+    }], {
+      context: 'src/'
+    }),
+    new CopyWebpackPlugin([ // 处理 main.json 里面引用的图片,不要放代码中引用的图片
+      {
+        from: path.resolve(__dirname, '../static'),
+        to: path.resolve(__dirname, '../dist/static'),
+        ignore: ['.*']
+      }
+    ])
   ]
 }

build/webpack.dev.conf.js

module.exports = merge(baseWebpackConfig, {
   devtool: '#source-map',
   output: {
     path: config.build.assetsRoot,
-    filename: utils.assetsPath('js/[name].js'),
-    chunkFilename: utils.assetsPath('js/[id].js')
+    filename: utils.assetsPath('[name].js'),
+    chunkFilename: utils.assetsPath('[id].js')
   },
   plugins: [
     new webpack.DefinePlugin({
    module.exports = merge(baseWebpackConfig, {
     // copy from ./webpack.prod.conf.js
     // extract css into its own file
     new ExtractTextPlugin({
-      filename: utils.assetsPath('css/[name].wxss')
+      filename: utils.assetsPath('[name].wxss')
     }),
    module.exports = merge(baseWebpackConfig, {
       }
     }),
     new webpack.optimize.CommonsChunkPlugin({
-      name: 'vendor',
+      name: 'common/vendor',
       minChunks: function (module, count) {
         // any required modules inside node_modules are extracted to vendor
         return (
        module.exports = merge(baseWebpackConfig, {
       }
     }),
     new webpack.optimize.CommonsChunkPlugin({
-      name: 'manifest',
-      chunks: ['vendor']
+      name: 'common/manifest',
+      chunks: ['common/vendor']
     }),
-    // copy custom static assets
-    new CopyWebpackPlugin([
-      {
-        from: path.resolve(__dirname, '../static'),
-        to: config.build.assetsSubDirectory,
-        ignore: ['.*']
-      }
-    ]),

build/webpack.prod.conf.js


    var webpackConfig = merge(baseWebpackConfig, {
   devtool: config.build.productionSourceMap ? '#source-map' : false,
   output: {
     path: config.build.assetsRoot,
-    filename: utils.assetsPath('js/[name].js'),
-    chunkFilename: utils.assetsPath('js/[id].js')
+    filename: utils.assetsPath('[name].js'),
+    chunkFilename: utils.assetsPath('[id].js')
   },
   plugins: [
    var webpackConfig = merge(baseWebpackConfig, {
     }),
     // extract css into its own file
     new ExtractTextPlugin({
-      // filename: utils.assetsPath('css/[name].[contenthash].css')
-      filename: utils.assetsPath('css/[name].wxss')
+      // filename: utils.assetsPath('[name].[contenthash].css')
+      filename: utils.assetsPath('[name].wxss')
     }),
     // Compress extracted CSS. We are using this plugin so that possible
     // duplicated CSS from different components can be deduped.
    var webpackConfig = merge(baseWebpackConfig, {
     new webpack.HashedModuleIdsPlugin(),
     // split vendor js into its own file
     new webpack.optimize.CommonsChunkPlugin({
-      name: 'vendor',
+      name: 'common/vendor',
       minChunks: function (module, count) {
         // any required modules inside node_modules are extracted to vendor
         return (
     var webpackConfig = merge(baseWebpackConfig, {
     // extract webpack runtime and module manifest to its own file in order to
     // prevent vendor hash from being updated whenever app bundle is updated
     new webpack.optimize.CommonsChunkPlugin({
-      name: 'manifest',
-      chunks: ['vendor']
-    }),
+      name: 'common/manifest',
+      chunks: ['common/vendor']
+    })
-    // copy custom static assets
-    new CopyWebpackPlugin([
-      {
-        from: path.resolve(__dirname, '../static'),
-        to: config.build.assetsSubDirectory,
-        ignore: ['.*']
-      }
-    ])
   ]
 })

config/index.js


module.exports = {
     env: require('./prod.env'),
     index: path.resolve(__dirname, '../dist/index.html'),
     assetsRoot: path.resolve(__dirname, '../dist'),
-    assetsSubDirectory: 'static', // 不将资源聚合放在 static 目录下
+    assetsSubDirectory: '',
     assetsPublicPath: '/',
     productionSourceMap: false,
     // Gzip off by default as many popular static hosts such as
@@ -26,7 +26,7 @@ module.exports = {
     port: 8080,
     // 在小程序开发者工具中不需要自动打开浏览器
     autoOpenBrowser: false,
-    assetsSubDirectory: 'static', // 不将资源聚合放在 static 目录下
+    assetsSubDirectory: '',
     assetsPublicPath: '/',
     proxyTable: {},
     // CSS Sourcemaps off by default because relative paths are "buggy"

问题与展望

技术的更新迭代是很快的,很多内容在写的时候还是这样。过了几天就发生了变化。又仔细看了小程序的文档,发现小程序原生开发深受vue影响啊,越来越像了。

希望mpvue能够使用wx.nextTick链接,尝试来代替50毫秒

希望能够解决使用脏检查优化每次更新数据时都会传输大量数据的问题, 解决删除回退, 列表忽然滚动到顶部等问题。也许可以靠下面的自定义组件。

使用自定义组件代替template,这样可以解决诸如:

在小程序完善了自定义组件之后,我现在的倾向变成了自搭或者网上找脚手架来工程化项目,使用诸如:NPM、PostCSS、pug、babel、ESLint、图片优化等功能。然后使用小程序原生开发的方式来开发,因为它做的越来越好,越来越像vue了。