微信小程序

微信小程序包含下面四种文件:

  • js
  • json 配置文件
  • wxml 小程序专用 xml 文件
  • wxss 小程序专用 css 文件
<view>
    <text class="window">{{ text }}</text>
</view> 
Page({
  data:{
    text:"这是一个页面"
  },
  onLoad:function(options){
    // 页面初始化 options为页面跳转所带来的参数
  },
  // ........
}) 

微信小程序只能通过其 mvvm 的模板语法来动态改变页面,本身 js 并不支持 BOM 和 DOM 操作。

从开发工具看微信小程序架构

在 mac 端直接解压应用 发现 app.nw 文件夹,即开发工具源码。可以知道该项目由 nw.js 编写; 在 package.json 文件下找到应用入口:app/html/index.html。入口 js 为 dist/app.js 我们可以看到整个编辑器的大致逻辑。
但我们关心的是构建过程,在 weapp 文件夹下存在 build.js 文件。没有找到有用的信息,只看到了 upload 模块,包括对大小限制,上传包命名。
为此怀疑,微信小程序本身和 RN 类似。是在服务端打包成 native 语言的。但是通过 android 边框测试发现,微信小程序根本不是 native 原生内容。

原生界面效果:

alt

编译过程

继续在 trans 文件夹下发现了编译模板。

  • transWxmlToJs wxml 转 js
  • transWxssToCss wxss 转 css
  • transConfigToPf 模板页配置
  • transWxmlToHtml wxml 转 html
  • transManager 管理器

用到的内容:

  • 发现用到了一个模板:app.nw/app/dist/weapp/tpl/pageFrameTpl.js, app.mw/app.dist.weapp/tpl/appserviceTpl.js
  • wcc 可执行程序,wcc 用于转转 wxml 中的自定义 tag 为 virtual_dom
  • wcsc 可执行程序,用于将 wxss 转为 view 模块使用的 css 代码,使用方式为 wcsc xxx.wxss

在模板中,我们发现使用了 WAWebview.js 文件,WAService.js文件。 在 transWxmlToJs 中我们发现一段 generateFuncReady 事件的函数。对比注册该事件的函数在 WAWebview.js 中。
我们尝试使用 wcc 对input.xml 文件进行编译。

wcc -d input.xml

生成了一段脚本:

window.__wcc_version__ = 'v0.6vv_20161230_fbi'
var $gwxc
var $gaic = 
$gwx = function (path, global) {
    function _(a, b) {
        b && a.children.push(b);
    }
    ....

通过代码我们发现,调用 $gwx 函数会再生成一个有返回值的函数(前提是 path 填写正确);于是我们执行如下代码:

$gwx("input.xml")("test")

得出如下内容:

{
    "tag": "wx-page",
    "children": [
        {
            "tag": "wx-view",
            "attr": {
                "class": "section"
            },
            "children": [
                {
                    "tag": "wx-input",
                    "attr": {
                        "autoFocus": true,
                        "placeholder": "这是一个可以自动聚焦的input"
                    },
                    "children": []
                }
            ]
        }
    ]
} 

这应该是一个类似 Virtual dom 的对象,交给了 WAWebivew.js 来渲染,标签名为 wx-view, wx-input

WAWebview.js

  1. 代码在最一开始提供的是兼容性工具,还有一个 WeixinJSBridge 引入。

  2. 接下来是一个 Reporter 对象,它的作用就是发送错误和性能统计数据给后台。

alt
  1. wx 核心对象,包含了 wx 对象下的 api。但是这里的 api 数量远远少于官方的 api 文档数量。
alt

我们可以在代码里面发现,wx 下注册的 api 最终都会调用 WeixinJSBridge 方法。这个方法应该是在打包的时候端上注入的。我们也可以在 WAServeice.js 中找到该方法的定义。

alt

所以我们得到了一个结论,WAService.js 是编辑器用来接受 wx 方法回调的代码。

  1. wxparser 对象,提供 domwx element 对象之间的映射操作,提供元素操作管理和事件管理功能。

  2. 之后代码是对 exparser 对象的处理,包括注册 WeixinJSBridge 全局事件,Virtual dom 算法实现,样式注入等。介绍几个组件重要的内容

  • exparser.registerBehavior 注册组件基础行为,供组件继承。 alt

  • exparser.registerElement 为各种内置组件,注册模板,行为,属性,监听器等内容 alt

这里我们观察到,组件:wx-video, wx-canvas, wx-contact-button, wx-map, wx-textarea 等 behaviors 都含有 "wx-native" 属性。这是不是意味着,这类组件都是 native 原生实现的呢。我们打开边框检查,发现这类组件确实都是原生的组件。

alt

综上,微信小程序的界面有部分组件使用原生方式实现的,native 组件层在 WebView 层之上。大部分还是用前端实现的,这样解释了微信小程序的一个bug。

微信官方文档:
alt

因为 scroll-view 是前端实现,在里面使用 native 组件,这样就无法监听滚动了。

WeixinJSBridge

组件是需要数据来渲染的,查看文档我们知道发送请求的 api 为 wx.request;通过上面分析,我们知道 wx.request 实际调用的是 WeixinJSBridge。现在我们看看 WeixinJSBridge

alt

WeixinJSBridge 真正发送处理数据请求的是这段代码;如果当前环境是 ios, 那么调用 WKWebviewwindow.webkit.messageHandlers.invokeHandler.postMessage。如果所处环境是 android 则调用 WeixinJSCore.invokeHandler (调用的时候,默认会带上当前 webviewID)。

WAService.js

在对 WeixinJSBridge.js 分析中,我们并没有发现前端的通讯功能,路由能力,数据绑定等内容。进一步查看找到了一个 WAService.js 文件。 查看 WAService.js 文件源码:

  1. 在代码最开始,跟 WAWebview.js 一样的 WeixinJSBridge 兼容模块
  2. 然后是跟 WAWebview.js 一样的 Reporter 模块。
  3. WAWebview.jswx 功能更为丰富 wx 接口模块。(剩余部分 wx api 都在这里)
  4. appServiceEngine 模块,提供 PageAppGetApp 接口
  5. window 对象添加 AMD 接口 require define

综上,WAService.js 主要实现的功能:

  • App( ) 小程序的入口;Page( ) 页面的入口
  • wx API;
  • 页面有的作用域,提供模块化能力
  • 数据绑定、事件分发、生命周期管理、路由管理

到这里我们得出结论,小程序的架构方案: alt

整个小程序由两个 webview 组成,代码分为 UI 层和逻辑层。UI 层运行在第一个 WebView 当中,执行 DOM 操作和交互事件的响应,里面是 WAWebview.js 代码及编译后的内容。逻辑层执行在(第二个webview 中)独立的 JS 引擎中(iOS:JavaScriptCore, android:X5 JS解析器;统称 JSCore;开发工具中,nwjs Chrome 内核),WAService.js 代码和业务逻辑。

当我们对 view 层进行事件操作后,会通过 WeixinJSBridge 将数据传递到 Native 系统层。Native 系统层决定是否要用 native 处理,然后丢给 逻辑层进行用户的逻辑代码处理。逻辑层处理完毕后会将数据通过 WeixinJSBridge 返给 View 层。View 渲染更新视图。

架构的讨论

微信的这种架构,对逻辑和UI进行了完全隔离,小程序逻辑和UI完全运行在2个独立的Webview里面来处理。那么这么做的好处是啥?总感觉更加麻烦了。除了小程序外,还有人采用这种架构设计么?
在网上搜索了一下,目前使用这种架构的项目还真有一个:去哪儿最新的 YIS 框架

YIS 采取了类似小程序的架构,分为逻辑层和UI层。UI 层运行在 WebView 中,而逻辑层运行在独立的 JS 引擎中。相应地,整个应用的代码,也分为两个大的部分,一部分运行在 WebView 中,一部分运行在JS引擎中。JS引擎计算DOM结构输出给WebView,WebView转发用户的点击事件给JS引擎。

该项目做法和小程序十分类似,唯一缺少的就是没有 native 的组件吧。然而官方文档上也没有任何介绍,为什么要这么做,只是说更流畅了。

一些看法

传统 web 页面显示需要经历一下几个步骤:

  1. webview 初始化
  2. 加载 HTML, CSS, JS
  3. 编译 JS
  4. Render 计算
  5. DOM Path

而利用小程序架构后,我们就可以将上述过程拆解成两部分并行执行: webview 部分:

  1. webview 初始化
  2. 加载 HTMLCSS, JS (经过拆分后,体积大幅度减小)
  3. 编译 JS
  4. 等待页面需要的数据
  5. 反序列化数据
  6. 执行 Patch
  7. 渲染页面
  8. 等待更多消息

jscore 部分:

  1. 初始化
  2. 加载框架 js 代码
  3. 编译 js
  4. 加载业务逻辑 js 代码
  5. 编译 js
  6. 计算首屏虚拟 DOM 结构
  7. 序列化数据,传输
  8. 等待 webview 消息,或者 Native 消息

这样渲染进程和逻辑进程分离,并行处理:加速首屏渲染速度;避免单线程模型下,js 运算时间过长,UI 出现卡顿。 完全采用数据驱动的方式,不能直接操作 DOM,利用定制开发规范的方式避免低质量的代码的出现。

当然这种架构方案也有一定的缺点:

  1. 不能灵活操作 DOM,无法实现较为复杂的效果
  2. 部分和 NA 相关的视图有使用限制,如微信的 scrollView 内不能有 textarea
  3. 页面大小、打开页面数量都受到限制
  4. 需要单独开发适配,不能复用现有代码资源。
  5. 在 jscore 中JS 体积比较大的情况下,其初始化时间会产生影响。
  6. 传输数据中,序列化和反序列化耗时需要考虑

参考资料

果博电脑版