让别人的小程序运行在自己的app中
概要
本文包括的内容:
- 小程序在微信开发者工具中,通过构建生成真正的执行代码和安装包,****.wxapkg。wxml和wxss在构建这一步就被转换成了html和css(virtual-DOM)。微信开发者工具中可以得到构建脚本和各个版本的js运行SDK文件。
- 小程序是以wxapkg文件的形式下发的,可以拿到wxapkg后解压拿到执行代码。同时解压微信.ipa拿到当前的js运行SDK文件,主要是WAWebView.js和WAService.js。
- 小程序在App中执行时的时候分为三个不同的模块,View/Service/Natvie,各司其职。
- 实现过程遇到大量的细节和坑。
介绍小程序原理的文章比较多,这篇讲的比较细:微信小程序架构分析。这篇文章的作者也成功的实现了wept,让小程序运行在自己的webapp里。
参考最多的是微店的Hera,完成度非常高的小程序框架,能够将小程序的demo代码在web/iOS/android运行起来,而且实现了很多工具。Hera的问题是开发于比较早期的版本,不兼容最新的版本了。Hera还有一个问题是他修改了小程序构建之后的目录结构,采用了service.html作为service部分的入口,跟小程序本身的实现尚有有一些区别。所以Hera只能够构建执行自己编写的小程序,不能执行别人编写的小程序。
我的目标是能够运行其它人开发的app,意味着我只能通过逆向的方式拿到wxapkg。但是因为拿不到源码,所以要尽可能在构建环节跟小程序保持一致。
经过数周的挣扎,目前已经实现了运行官方demo。已经达到"可行"的阶段,但是还远远谈不上“可用”,因为需要实现小程序大量的API,这是个体力活,依赖个人的力量难以完成。
构建
官方demo小程序原先的目录分为几类文件:
- 配置: 在这个目录中,app.json里保存了页面信息、tabbar信息、网络超时配置等等。 config.js保存了腾讯云后台服务解决方案的配置。
- js文件: js定义了各种函数接口逻辑
- wxml: 类似于html,定义了页面结构
- wxss: 类似css
demo
├── app.js
├── app.json
├── app.wxss
├── config.js
├── image
│ ├── green_tri.png
│ ├── ...
├── page
│ ├── API
│ │ ├── index.js
│ │ ├── index.json
│ │ ├── index.wxml
│ │ ├── index.wxss
│ │ ├── pages
│ │ │ ├── action-sheet
│ │ │ │ ├── action-sheet.js
│ │ │ │ ├── action-sheet.json
│ │ │ │ ├── action-sheet.wxml
│ │ │ │ └── action-sheet.wxss
│ │ │ ├── ...
│ │ └── resources
│ │ └── ...
│ ├── ...
├── project.config.json
├── util
│ └── util.js
└── vendor
└── qcloud-weapp-client-sdk
├── ...
经过小程序的开发环境构建后,生成了一个*.wxapkg文件。
这个文件可以通过从越狱的iPhone或者root的安卓手机上拿到。有部分人用charles通过https抓包拿到了下载链接,也拿到了包。
拿到后要进行解包。有大神已经通过反编译安卓apk的方式拿到了解包部分的代码,然后用python重写了一遍。源码见wechat-app-unpack。
解包后得到的目录如下:
1.wxapkg_dir
├── app-config.json
├── app-service.js
├── app-service.js.map
├── image
│ ├── green_tri.png
│ ├── ...
├── page
│ ├── API
│ │ ├── index.html
│ │ ├── pages
│ │ │ ├── action-sheet
│ │ │ │ └── action-sheet.html
│ │ │ ├── ...
│ │ └── resources
│ │ └── kind
│ │ ├── api.png
│ │ ├── ...
│ ├── ...
└── page-frame.html
转换过程可以分为三部分:
- 从wxml/wxss到html。 page-frame.html里定义了所有的virtural-dom,来自所有的wxss和wxml的转换,文件非常大。 page.html里很简单,就是从page-frame.html里提取对应的virtual-dom。包括wxss和wxml对应的逻辑。
- app-service.js是从之前所有的js文件转换而来。
- app-config.json是从app.json转换而来。
openVendor
命令可以在小程序中获取到构建脚本wcc和wcsc, 以及各个版本小程序的执行SDK ****.wxvpkg,这个SDK也可以用wechat-app-unpack解开,解开后里面就是WAService.js和WAWebView.js等代码。
wxml/wxss的构建原理
wxss 转换成了css,wxml转换成了inject_js,实际上就是virtual_dom。
是用什么工具转换的?小程序里是叫wcc和wcsc。在开源工具hera自己实现了一套wxss-transpiler和wxml-transpiler。而hera的前身wept是直接使用wcc和wcsc。
我们为了减少维护成本,直接采用wcc和wcsc。
因为我们没有wcc和wcsc的源码,所以只能借助wxss-transpiler和wxml-transpiler来帮助我们理解wxml/wxss的构建原理。
wxss-transpiler调用了一个PostCSS的插件,用来处理wxss。
PostCSS 提供了一种方式用 JavaScript 代码来处理 CSS。它负责把 CSS 代码解析成抽象语法树结构(Abstract Syntax Tree,AST),再交由插件来进行处理。插件基于 CSS 代码的 AST 所能进行的操作是多种多样的,比如可以支持变量和混入(mixin),增加浏览器相关的声明前缀,或是把使用将来的 CSS 规范的样式规则转译(transpile)成当前的 CSS 规范支持的格式。
wxml-transpiler:实现了一个转译器的工作,比如postcss也是转译器,包括解释器(parser),代码转换器(Transformer),代码生成器(Generator)。这个是闭源的。
- 根据输入的列表,读取所有文件
- 调用VUE的HTML Parser,解析输入的标签及属性,生成一颗DOM树。vue解决不了的js语言,用babylon库来处理。(Parse)
- 在解析组件的标签时,对其上包含的属性值进行解析(边Parse边Transform)
- 根据已有的AST生成JS文件(Generate)
更多实现原理见这篇文章
模块之前的通信
image小程序在App中执行时的时候分为三个不同的模块,View/Service/Native,各司其职。
View和Service都在WKWebView中执行,互相无法调用。他们之间通过Native层通信。
Native和WebView之间通过webkit.messagehandler和evaluateJavascript互相调用。
-
WeixinJSBridge.publish: view和service之间的透传,在WKWebView之间传递消息。
-
WeixinJSBridge.subscribe: 注册监听,监听view和service之间的消息调用。
-
WeixinJSBridge.invoke: View或者Service传递消息到Native,然后Native使用逻辑调用js callback。
-
WeixinJSBridge.on:监听Native的事件。
如何执行
这里以iOS为例介绍Native执行过程。安卓类似。
通过解压微信的ipa可以拿到WAService.js和WAWebView.js两个基础库文件,文件内容与hera的service.js/view.js已经有了较大的区别。
我们采用小程序的架构和hera的两个webView的方案,尽可能模仿小程序的执行过程。
View部分
View部分是比较直观的,就是WKWebView加载web页。这里需要在app-config.json里读取到首页的路径,然后加载该页面。这个路径下的xxx/index.html是无法直接加载的,需要做一些处理。要引入本地执行SDK里的index.css和view.js, 然后把page-frame.html
里的virtual-dom全部塞进该页面。 然后loadHTML即可。
View所有的WKWebView也是要注册WKUserContentController的,用于通信。
通过反汇编可以得知这个类在微信中叫YYWAWebView
,调用js是直接调用-evaluateJavaScript:completionHandler:
方法的。
view.js中包含的逻辑:
- WeixinJSBridge 对象处理消息通信: invoke invokeCallbackHandler on publish subscribe subscribe subscribeHandler。
- Reporter 对象
- wxparser 对象,提供 dom 到 wx element 对象之间的映射操作,提供元素操作管理和事件管理功能。
- virtual dom 渲染算法实现,提供 diff apply render 等方法,该模块接口基本与 virtual-dom 一致,这里特别的地方在于它所 diff 和生成的并不是原生 DOM,而是各种模拟了 DOM 接口的 wx element 对象
Service部分
Service部分的实现,Hera和微信小程序采取的了不同的架构。
Hera的实现较为简洁,跟View部分保持一致,采用了WKWebView,调用-evaluateJavaScript:completionHandler:
方法执行js,js回调OC时使用WKScriptMessageHandler。
通过反汇编可以得知这个类在微信中叫WAJSCoreService
,js和OC之间的调用是采用JavascriptCore互相调用。
JavascriptCore它首先要加载app-config.json并把这个配置赋给一个全局对象__wxConfig。然后他要加载service.js是SDK基础,再然后他要加载app-service.js,这里面包含了用户编写的js逻辑。最后它发出全局消息
WeixinJSBridge.publish('serviceReady',,);</script>
唤起小程序app的初始化。
Service.js中包含的逻辑:
- 跟 view.js 一样的 WeixinJSBridge 兼容模块
- view.js 一样的 Reporter 模块
- appServiceEngine 模块,提供 Page,App,GetApp 接口
Native部分
Native执行的问题比较复杂,因为基本是黑盒,里面发生了什么并不知道。
hera的方案在构建过程就已经跟小程序实际的方案有所区别,会提高维护成本。所以我们只能靠猜测来实现Native的执行过程。
Native部分就是作为入口,运行环境,跳转,转发消息,实现扩展。包括网络模块/摄像头/tabbar实现的都是扩展。
我们可以得知的是消息传递的协议。然后只能通过safari来调试webView,根据协议的名称和出入参来猜测协议的内容。
主要的困难点
- 由于压缩后的view.js和service.js基本不具备可读性,而virtual-dom又彻底不具备可读性...所以就算猜中了协议,最后也往往是魔改。可维护性极差。
- 工作量不小,因为调试困难,无法阅读。之后随着小程序SDK升级,能用多久也不可知。加上也有各种微逆向的操作,小程序封上任何一个接口,都会导致扑街。可持续性极差。总之非常佩服微店的同学能把Hera搞出来:)
小程序的性能启发
小程序是颠覆我对Web的固有印象,最初还以为是类似weex或者rn的调用原生的方式,没想到几乎完全是运行在WKWebView之上的。
- 采用virtual DOM,操作JS比html性能高很多,因为是diff后再操作dom,不需要全部重新渲染,快很多。
- WKWebView,滑动60fps,在独立于App之外的进程执行。
- 部分逻辑Native化,比如收发网络请求,比如数据持久化。 逐步用native组件来替换h5组件。比如tabbar。
- 重用webView以及提前初始化webView等等技巧。
存在的问题:
- 看消息传递的原理就能发现,传递的过程太长了,尤其是
setData:
这种传递整个model对象,是两次对象的深拷贝,可能会增加两次json的序列化和反序列化,如果model对象很复杂对性能影响比较大。 - 页面初始化/响应速度/UI细节还是跟原生有差距。
当然已经比纯web页强很多了。目前来看还是只适合轻量化的应用。受制于架构以及微信的平台,个人认为是对Web的替代和改善。但是就算在可见的未来,还是很难跟native抗衡。
现代浏览器和操作系统之间的界限越来越模糊。App的"下载/安装"过程本身就是一种妥协。只要小程序的体验足够好,应该没有人会拒绝。