阅读 664

教你写一个小程序跨端框架(一)

当你使用 megalompvue 这些框架的时候,看上去,写的是vue 的代码,一切仿佛就像在写一个 vue 项目,然后打包编译之后就可以运行在小程序内,是不是很神奇?

接下来,我会用一个系列文章,讲解这些跨端框架的核心原理,深入到源码底层去分析,揭开他们神秘的面纱。

小程序的跨端方案哪家强?

首先,这不是一个跨端框架的评测文章。

本质上,这几家的框架的核心都一样,甚至是 weex 这类框架,都差不多的。

这些框架做的事情,都是把 vue 框架拿过来改了改,借助了 vue 的能力,比如说,VueDSL、vue 的编译打包流程(也就是vue-loader), vue 的响应式双向绑定,虚拟dom, diff 算法,在涉及到具体平台的dom操作时,替换成对应平台的api。他们之间不同的点,在于具体问题的处理,优化策略的不同。

从另一个角度来说,无论选择哪家的框架进行源码分析,对于普通前端来说,都能从中学到很多知识。

目前想法是,先从 megalo 的源码开始分析,之后再对比其他框架的不同。

写在读源码之前

不要一头扎进去读源码,否则会找不到北。

要先了解一下整理流程,再去看细节和各种分支逻辑。

所以,在本章节不会先去看源码是怎么实现的,而是先介绍一下megalo 的整体流程,之后会再去读源码分析。

.vue 单文件,怎么跑在小程序里面?

首先,可能我们会有疑问,一个 .vue 的单文件,究竟做了啥,怎么就能跑在小程序里面了?

我们知道,对于微信小程序来说,只认 4份文件:.wxml.wxss.js.json

下面是去小程序官网截的图。

image-20191009151427474

一般来说,.vue 单文件有三个部分组成: <template><script><style>

我们第一步需要做什么呢?就是把 <template><script><style>这三个部分对应的代码,拆一拆,处理一下,分到 .wxml.wxss.js.json 这 4 份文件中。

  • 最简单是<style> 部分的css 样式,哪怕直接放到 .wxss,也是正常运行。
  • 稍微复杂一点的是 <template>.wxml,我们需要把 h5 的标签啊、vue的语法啊 替换成小程序的标签、小程序的语法。替换的工作我们称为 模板替换 ,下文会有一个章节用来介绍。
  • 最难的是<script>.js, 涉及到 vue 的运行时 如何和 小程序的实例通讯的问题,这一部分会用比较多的章节去介绍。

接下来,我们先看模板替换 ,也就是.vue.wxml 的过程。

<template>.wxml —— 模板替换

在进行 vue 开发的时候,我们是直接使用 vue 的语法进行开发的,与小程序的模版语法不一样,例如小程序里没有 div 标签,条件语句、循环语句在小程序里面都不是一样的。因此我们需要对模版进行转换。

例如,div 标签需要转换需要转换成 view 标签。

// 替换前
<div></div>
复制代码
// 替换后
<view></view>
复制代码

比如说,条件判断,v-if 则需要改变前缀,将 v- 改成 wx:

// 替换前
<div v-if=“a”>
复制代码
// 替换后
<view wx:if=“a”>
复制代码

再比如说,在 vue 里面绑定事件,常用 @事件名 的语法, 转成小程序模版则需要用 bind,同时用 tap 事件替换 click 事件:

// 替换前
<div @click=“b”>
复制代码
// 替换后
<view bindtap=“b”>
复制代码

Vue 和小程序插值表达式则是一样的,采用了双花括号,可以不需要做任何转换。

// 替换前
<div>{{ title }}</div>
复制代码
// 替换后
<view>{{ title }}</view>
复制代码

上面展示模板替换是最简单的转换,这种转换能实现最简单的插值表达式的数据映射。但后面我们会提到,它有局限性。

接下来,我们看一下, <script>.js 部分,这部分也可以称为 vue 运行时的改造

<script>.js —— vue 运行时的改造

.vue 单文件中的 script 部分中, 我们通常会写下面的代码,写的配置项传入Vue 构造函数中:

new Vue({
  data(){},
  methods: {},
  components: {}  
})
复制代码

new Vue() 会实例化出来一个 vm 实例。

但这并不是小程序想要的呀!

正如上图所示,我们在 <script> 中写的是 new Vue() 这样子的代码,而微信想要的是 Page()

Page() 方法是小程序官方提供的api。

题外话, Page() 方法是必须要有的。微信小程序会在进入一个页面时,扫描该页面中的.js 文件,如果没有调用 Page() 这个方法,页面会报错。

那么,应该怎么做呢?

megalo 的做法是,拓展了 Vue 的框架,如下面伪代码所示:

new Vue() {};
Vue.init = () => {
  Page()
}   
复制代码

在 vue 实例化的时候,调用 init 时会调用 Page() 函数,生成一个小程序的page实例。这就涉及到,vue 的数据和 page 实例的数据同步。

那么,如何做到数据同步呢?

为了更好的深入理解,接下里需要介绍一下 vue 的核心流程。

Vue 的核心流程

如下图左侧所示,简单来说, 一个 .vue 的单文件由三部分构成: template, script, style

image-20191009084523845

我们先看图中的橘黄色路径,也就是 template 部分的处理过程, template 部分, 会在编译的过程中,在 vue-loader 中通过 ast 进行分析,最终生成一段 render 函数,render函数可以在打包生成的文件中找到。如果你忘记了 render 长什么样子,可以看下面这个例子:

一段简单的 template 模板如下所示:

<div class="ctl-view" @click="handleClick">
  {{ a }}
</div>
复制代码

经过编译之后,通过 ast 进行分析,生成的 render 函数如下:

_c("div", { staticClass: "ctl-view", on: { click: _vm.handleClick } },[_vm._t("default")])
复制代码

上面的 render 函数,会被 vue 在运行时执行。那么执行 render 函数会生成什么呢?

是生成虚拟dom树 (对应图中,render 函数的下一个阶段)。

虚拟DOM树是对真实DOM树的抽象,树中的节点被称作 vnodevnode 有一个特点, 它保存了这个DOM节点用到了哪些数据 ,这一点非常重要,因为下文会介绍到megalo利用了该特点。

Vue拿到 虚拟dom树之后,就可以去和上次老的 虚拟dom树patch diff 对比。目的是找出,我们应该怎么样改动旧的DOM树,代价才最小。

patch 阶段之后,vue 就会使用真实的操作DOM的方法(比如说 insertBefore , appendChild 之类的),去操作DOM结点,更新视图。

接下来,我们再来看一下,蓝色的线条的路径。在实例化 Vue 的时候,会对数据 data 做响应式的处理。

这部分的内容已经有很多人写过源码分析了,这里就不再赘述。

简单来说,本质是通过 object.defineproperty 重写了数据的 gettersetter, 在 getter 中收集依赖,在 setter 中通知依赖。当数据发生改变,watcher 就会收到通知,从而触发 watch 实例的 update 方法。

watcher.update() 中,最终又会调用上文的 render 函数,生成最新的虚拟DOM树, 接着对比老的虚拟DOM 树进行 patch, 找出最小修改代价的vnode 节点进行修改。

上面介绍了 vue 的整体流程,下面趁热打铁,介绍megalo的核心流程。

megalo 的核心流程

megalo 、 mpvue 等小程序框架,本质都是对 vue 的拓展。

对比 vue 的核心流程图,我们发现,小程序跨端框架的流程图多了一个 Page() 方法,同时还新增了setData,替换掉 vue 原本的 DOM 操作。

megalo 等跨端框架,会在页面初始化的时候,实例化一个Vue 实例,还会偷偷的调用 Page() 用于生成了小程序的page 实例。

因此在实际页面打开之后,会同时创建 Page 实例 和 Vue 实例,Page 实例和 Vue 实例之间是有通讯关系的, Vue 实例向Page 实例的通讯,就是setData 方法。

setData() 是小程序官方提供的api,用来修改小程序 page 实例上维护的数据。

思考一个问题:小程序并不支持原生DOM操作,因此也就没有修改视图节点的能力。那么我们想控制一个小程序页面上的一个节点显示和隐藏,应该怎么做呢?

我们没有办法直接操作小程序视图,只能调用 setData() 更新小程序实例上的数据,从而更新视图。

来看下面的例子:

new Vue({
  data(){
    return {
      showToggle: true
    }
  }
})
复制代码
// 下面是经过 模板替换 之后的代码
<view wx:if="{{showToggle}}">
</view>
复制代码

在上面的例子中,showToggle 这个数据是写在Vue 实例上的。当该数据发生变化时, megalo 通过某种机制可以知道showToggle 发生了变化,调用小程序的 setData({showToggle: false}),修改小程序实例上的数据, 从而让小程序Service 层去更新视图,从而控制该节点的显示和隐藏。

通讯流程是: vm 实例 -> 调用 setData() -> 小程序 page 实例 -> 小程序 page 视图

下面这张图,是 megalo 官方的一张原理图。

上图中,应该不难理解。

小结

在本篇文章中,介绍了megalo 的模板替换、 vue 的核心流程、megalo 的核心流程。

但是这个流程还是很抽象的,一些具体的细节,会在下一篇文章中介绍。

比如说,小程序Page 实例的数据,真的是和 Vue 实例上的数据一一对应的吗?小程序 Page 实例中维护的数据结构是什么样子的?megalo 是如何保证高性能的?

这些问题,统统会在下一篇文章中介绍。

关注下面的标签,发现更多相似文章
评论