细说微信小程序

近来微信小程序十分火热,终于解放了我手机的存储空间,那么抛开这些不说,小程序到底是怎么实现的呢?是真正的Native吗?本文以iOS端为例,一探究竟!

小程序简介

先看下官方的简单描述:

小程序开发框架的目标是通过尽可能简单、高效的方式让开发者可以在微信中开发具有原生 APP 体验的服务。

框架提供了自己的视图层描述语言 WXML 和 WXSS,以及基于 JavaScript 的逻辑层框架,并在视图层与逻辑层间提供了数据传输和事件系统,可以让开发者可以方便的聚焦于数据与逻辑上。

可以看出微信提供了自己的视图层描述语言和逻辑框架,也就是微信给了一个框架帮你处理数据和视图的绑定,生命周期等等,开发者只需要关注自己的界面和逻辑编写。那么所谓的这套框架是不是类似于RN呢?还是只是一套前端框架+开放出来的JSApi。

其实我们可以在 细节点 看到相应的说明:

  • 在 iOS 上,小程序的 javascript 代码是运行在 JavaScriptCore 中,是由 WKWebView 来渲染的,环境有 iOS8、iOS9、iOS10
  • 在 Android 上,小程序的 javascript 代码是通过 X5 JSCore来解析,是由 X5 基于 Mobile Chrome 37 内核来渲染的
  • 在 开发工具上, 小程序的 javascript 代码是运行在 nwjs 中,是由 Chrome Webview 来渲染的

根据猜测就是在移动端还是由web来渲染的,并不是原生的。可能除了部分组件是Native,里面的控件都是web渲染的,后面我们来验证一下。

框架

根据官方的描述它的框架是这样的:

image

视图层(WXML和WXSS)负责页面的布局和样式,逻辑层(JavaScript)负责业务代码逻辑的编写,对于微信提供的这个框架,我们并不需要关心逻辑层到视图层的数据传输,视图层到逻辑层的事件系统。可以直接在逻辑业务层操作数据就行了。

开发工具

首先来看一下小程序的开发工具微信web开发者工具,显示包内容可以看到如下内容:

image

从里面的结构可以看出,它是NodeWebkit封装的Web应用。然后看编辑器的界面:

image

它是基于monaco,基于浏览器的代码编辑器。

我们知道微信小程序是基于微信提供的视图层(WXML+WXSS),当然这个只是一个描述,最终需要转换成js、html或css的代码,可以在开发工具里面找到:

image

这些工具负责WXML和WXSS的转换。

上面的Pf指的是pageFrame模板文件:

image

我们所写的代码最终都会转成这样的一样文件,里面包括转换后的WXML WXSS 以及写的js,除了这些还会有微信提供的处理工具,如:WAWebviewwebviewSDK等等。后面会讲到pageFrame,WAWebviewWAService

里面还提供了两个工具:

  • wcc: 转换wxml中的自定义标签为json描述树。
  • wcsc: 转换wxss到样式。

所以从这里看来还都是和web相关的东西,接着往下看。

Button标签

现在开始从Button标签入手,来看下Button标签是怎么转换成最终手机上显示的Button的。

首先编写wxml文件hello.wxml

1
<button>I'm a Button!</button>

然后使用上面提到的wcc这个工具,做一个转换:

1
./wcc -d hello.wxml -o hello.js

得到的这个js文件其实是一个生成工具,来生成我们Json的描述树。编写一个简单的index.html, 去调用这个js文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="./hello.js"></script>
<script type="text/javascript">
var out = JSON.stringify($gwx('hello.wxml')());
document.write(out);
</script>
</head>
<body>
</body>
</html>

最终的输出为:

image

这是一个描述界面节点关系的JSON树结构,后面我们会看到这里面的wx-button就是手机里面加载的自定义标签。

WAWebview.js

刚刚看到了从一个WXML写的button到一个wx-button的转换过程,现在来看一下wx-button到底是什么,可以在WAWebview.js里面找到这些自定义标签的定义:

image

这里会定义标签的行为是web还是native,以及标签的属性和事件处理。还可以看看其它标签:

image

image

这里可以猜测,buttontext都是web的,而textarea是Native的,而在wx-textarea里面也可以找到这样的一个方法:

image

这里通过WeixinJSBridge调用了Native的insertTextArea方法去插入一个原生的textarea控件。后面可以再进一步验证一下。

WAService.js

除了刚刚提到的WAWebview.js外,还有一个非常主要的js文件就是WAService.js。这个文件提供了官网的api接口,比如:wx.uploadFile

image

里面通过JSBridge调用原生的uploadFile

验证

有了一个初步的了解之后,我们来通过几个方面来验证一下。首先是界面,然后看看内部的加载。

界面分析

在微信打开官方的小程序示例,点击表单组件里面的button, 然后来查看它的界面结构:

image

可以看到右边除了导航栏是原生的以外,里面的按钮demo部分都是在一个YYWKWebView的控件里面。

而从dump出来的头文件里面可以看到:

1
2
3
4
5
6
7
@interface YYWKWebView : WKWebView <WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler, YYWebViewInterface>
{
BOOL scalesPageToFit;
BOOL _bDisablePopup;
id <YYWebViewDelegate> wvDelegate;
YYWKWebViewScriptMessageHandler *_scriptMessageHandler;
}

YYWKWebView继承自WKWebView, 所以里面就是一个WebView的容器。

而看到的Button也是webview里面的控件,不是原生的。通过同样的方法看了一下官方提供的其它控件,以下控件是原生Native的空间。

  • canvas
  • contact-button
  • map
  • textarea
  • video
  • pickview
  • textfield
  • scrollview

image

webview分析

既然里面是webview,那么使用Safari提供的调试工具来看下里面webview加载的代码吧。

手机端:

进入设置-Safari-高级 打开Web检查器

Safari:

偏好设置-高级 启用开发菜单。

选择微信里面的webview加载的url。

image

查看页面上的元素:

image

可以看到里面的控件对应就是web里面的自定义标签。比如右边的button对应的就是html里面的自定义标签<wx-button>

那么来看下在开发的时候这个页面是怎么写的?就拿那个Loading的按钮吧。

源代码:

1
<button type="primary" loading="true">页面主操作 Loading</button>

加载后的代码:

1
<wx-button loading="" type="primary">页面主操作 Loading</wx-button>

小程序会把wxml里面的标签转成对应的自定义标签。

直接在Safari里面修改对应的html代码也会马上反映到界面上面:

image

小程序生成过程

我们开发编写的文件分三种wxmlwxssjswxml的转换刚刚也看到了首先转成json的结构树,然后根据WAWebview.js里面的定义转成自定义标签或者原生控件。wxss转成最终的css样式,这里就是拿的weui那一套。js转成真正的js代码。最后都会转成一个文件page-frame.html

所以在Safari调试工具里面看到的页面都是page-frame.html

小程序加载

接下来来简单看看从点击小程序到小程序加载的大致过程:

首先来到发现里面的小程序,点击进去后就能看到打开过的小程序列表,还是从点击官方的小程序示例, 开始!

1
2
3
4
5
6
7
8
9
10
11
cy# [[[UIWindow keyWindow] rootViewController] _printHierarchy].toString()
`<MMTabBarController 0x16a86340>, state: appeared, view: <UILayoutContainerView 0x169181f0>
| <MMUINavigationController 0x16a69ad0>, state: disappeared, view: <UILayoutContainerView 0x16a6a2f0> not in the window
| | <NewMainFrameViewController 0x15bcbe00>, state: disappeared, view: (view not loaded)
| <MMUINavigationController 0x16a72800>, state: disappeared, view: <UILayoutContainerView 0x16a72b60> not in the window
| | <ContactsViewController 0x1637e800>, state: disappeared, view: (view not loaded)
| <MMUINavigationController 0x16a78a70>, state: appeared, view: <UILayoutContainerView 0x16a78d60>
| | <FindFriendEntryViewController 0x16384e00>, state: disappeared, view: <MMUIHookView 0x16a7b020> not in the window
| | <WAMainListViewController 0x15c72a00>, state: appeared, view: <UIView 0x16be5770>
| <MMUINavigationController 0x16a7fd30>, state: disappeared, view: <UILayoutContainerView 0x16a80020> not in the window
| | <MoreViewController 0x16382c00>, state: disappeared, view: (view not loaded)`

当前的ViewControllerWAMainListViewController, 里面是个tabview,所以点击事件的相应函数是:

1
-[WAMainListViewController tableView:didSelectRowAtIndexPath:]

从IDA里面看流程就是从历史列表中取出当前点击的对象WAAppItemData,然后调用:

1
WAAppContactPreLoader openAppWithUserName:navigationController:fromScene:debugMode:onSuccess:onFailed:

这里的username是一个小程序id的标示:gh_d43f693ca31f@app,通过手机扫码等到的url,解密后也会有一个这样的标示。

如果已经加载过了的会直接打开,否则会有一个loading加载的页面去加载资源。

如果是新的小程序会根据这个标示去查询小程序的信息, 包括小程序的名字,描述以及webappinfo, 然后去拉取url。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{
AppConfig = {
VersionList = (
{
type = 1;
version = 3;
}
);
};
Appid = wxe5f52902cf4de896;
Network = {
DownloadDomain = (
"https://stream.weixin.qq.com",
"https://54330679.qcloud.la",
"https://14592619.qcloud.la"
);
RequestDomain = (
"https://stream.weixin.qq.com",
"https://54330679.qcloud.la",
"https://14592619.qcloud.la"
);
UploadDomain = (
"https://stream.weixin.qq.com",
"https://54330679.qcloud.la",
"https://14592619.qcloud.la"
);
WsRequestDomain = (
"wss://stream.weixin.qq.com",
"wss://ws.qcloud.com",
"wss://14592619.qcloud.la",
"wss://14592619.ws.qcloud.la"
);
};
RoundedSquareIconUrl = "http://mmbiz.qpic.cn/mmbiz_png/Cr5VxZfYgAIeW02ZwDHUJmWlzbE3jhKUcLfMic4UrYHp2cdv30wDb6mXCVG4cJ9vtRA9prjWjMVhv27AnOQicWAA/0?wx_fmt=png";
RunningFlagInfo = {
RunningFlag = 512;
StopServiceTime = 1472041769;
};
Setting = {
ExpiresAtList = 5184000;
MaxBackgroundLifespan = 300;
MaxCodeSize = 1;
MaxDownloadConcurrent = 10;
MaxFileStorageSize = 10;
MaxLocalstorageSize = 10;
MaxRequestConcurrent = 10;
MaxUploadConcurrent = 10;
MaxWebviewDepth = 5;
};
}

然后会获取到小程序主界面的url, 生成一个page-frame.html的页面, 再通过webview打开。

1
https://res.servicewechat.com/weapp/release/wxe5f52902cf4de896/13/page/component/index.html

小程序的生命周期是通过WAWebViewController调用JSApi告诉小程序。小程序里面的接口也是通过JSApi接口来和Native交互的。

上面的流程分析的不是很完整,很多细节没有去分析了~~~

开发流程

目前小程序只面向企业政府,媒体,认证通过之后可以创建一个小程序,然后会给一个appid。里面分为三个身份:

  • 管理员
  • 开发者
  • 体验者

流程是这样的:

image

题外

微信小程序只能通过摄像头扫码识别打开,不能通过本地图片识别二维码打开。在前面看到加载小程序的类WAAppContactPreLoader,里面有个-[WAAppContactPreLoader openAppWithQRFullUrl:fromScene:],scene表示二维码识别的来源,1012表示图片识别的,1011表示摄像头扫码的,改一下就能从本地长按打开小程序了。

最后

所以这样来看微信小程序主要是通过它定义的描述层,把我们写的标签根据WAWebview.js转成对应的自定义标签或Native控件,小程序提供的API由WAService.js提供。这次只是一个很简单的分析,很多东西都还没有深入研究,大家请轻拍。

AloneMonkey wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!