【知识】【小程序】微信小程序代码分析

最近在逆向小程序的代码,主要目标是实现小程序静态分析工具,因此整理一下小程序包从获取、解包到代码结构的相关知识。

小程序包

wxapkg 包获取

安卓设备中存储在 /data/data/com.tencent.mm/MicroMsg/{UserIdHash}/appbrand/pkg/ 下 *.wxapkg

wxapkg 包处理

二进制文件,没有对内容加密,没有对文件压缩,是各个数据段的拼接,比较好还原,各段如下图。头部含一些字段,比较重要的有数据段长度、文件数目。索引段表示各文件的文件名长度、文件名、文件在数据段的未知、文件大小等。数据段则是每个文件的文件数据。

小程序包结构

小程序代码结构

框架

小程序的运行环境分成渲染层和逻辑层,其中 WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。
框架的核心是一个响应的数据绑定系统,可以让数据与视图非常简单地保持同步。当做数据修改的时候,只需要在逻辑层修改数据,视图层就会做相应的更新。

小程序框架结构

1
2
3
<!-- This is our View -->
<view> Hello {{name}}! </view>
<button bindtap="changeName"> Click me! </button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// This is our App Service.
// This is our data.
var helloData = {
name: 'Weixin'
}
// Register a Page.
Page({
data: helloData,
changeName: function(e) {
// sent data change to view
this.setData({
name: 'MINA'
})
}
})

包结构

小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。一个小程序主体部分由三个文件组成,必须放在项目的根目录 app.json、app.js、app.wxss。

小程序包结构示例

pages中代码构成 pages/xxx/index.[html | js | json | wxml]

小程序包内单个页面路径下文件示例

-json 后缀的 JSON 配置文件
-wxml 后缀的 WXML 模板文件
-wxss 后缀的 WXSS 样式文件
-js 后缀的 JS 脚本逻辑文件

app.json

当前小程序的全局配置,包括了小程序的所有页面路径、界面表现、网络超时时间、底部 tab 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
# 用于描述当前小程序所有页面路径,这是为了让微信客户端知道当前你的小程序页面定义在哪个目录
"pages":[
"pages/index/index",
"pages/logs/logs"
],
# 定义小程序所有页面的顶部背景颜色,文字颜色定义等
"window":{
"backgroundTextStyle":"light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "Weixin", # 通常理解为小程序名
"navigationBarTextStyle":"black"
}
}

app.service

逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈。开发者写的所有代码最终将会打包成一份 JavaScript 文件,并在小程序启动的时候运行,直到小程序销毁。
在 JavaScript 的基础上,微信增加了一些功能(js-callgraph分析以此为基础):
-增加 App 和 Page 方法,进行程序注册和页面注册。
-增加 getApp 和 getCurrentPages 方法,分别用来获取 App 实例和当前页面栈。
-提供丰富的 API,如微信用户数据,扫一扫,支付等微信特有能力。
-提供模块化能力,每个页面有独立的作用域。

app-service.js 是当前包中所有代码的汇总,之前介绍case时提到,解包解不出 pages/xxx/index.js时可以在 app-service.js中找到相应代码。

app.js

每个小程序都需要在 app.js 中调用 App 方法注册小程序实例,绑定生命周期回调函数、错误监听和页面不存在监听函数等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app.js
App({
onLaunch (options) {
// Do something initial when launch.
},
onShow (options) {
// Do something when show.
},
onHide () {
// Do something when hide.
},
onError (msg) {
console.log(msg)
},
globalData: 'I am global data'
})

整个小程序只有一个 App 实例,是全部页面共享的。开发者可以通过 getApp 方法获取到全局唯一的 App 实例,获取App上的数据或调用开发者注册在 App 上的函数。

1
2
3
// xxx.js
const appInstance = getApp()
console.log(appInstance.globalData) // I am global data

pages/xxx/index.js

对于小程序中的每个页面,都需要在页面对应的 js 文件中进行注册,指定页面的初始数据、生命周期回调、事件处理函数等。简单的页面可以使用 Page() 进行构造。

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
//index.js
Page({
data: {
text: "This is page data."
},
onLoad: function(options) {
// 页面创建时执行
},
onShow: function() {
// 页面出现在前台时执行
},
onReady: function() {
// 页面首次渲染完毕时执行
},
onHide: function() {
// 页面从前台变为后台时执行
},
onUnload: function() {
// 页面销毁时执行
},
// 事件响应函数
viewTap: function() {
this.setData({
text: 'Set some data for updating view.'
}, function() {
// this is setData callback
})
},
// 自由数据
customData: {
hi: 'MINA'
}
})

Page 构造器适用于简单的页面。但对于复杂的页面, Page 构造器可能并不好用。
此时,可以使用 Component 构造器来构造页面。 Component 构造器的主要区别是:方法需要放在 methods: { } 里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Component({
data: {
text: "This is page data."
},
methods: {
onLoad: function(options) {
// 页面创建时执行
},
onPullDownRefresh: function() {
// 下拉刷新时执行
},
// 事件响应函数
viewTap: function() {
// ...
}
}
})

页面路由

实现页面之间的切换

路由方式 触发时机 路由前页面 路由后页面
初始化 小程序打开的第一个页面 onLoad, onShow
打开新页面 调用 API wx.navigateTo 使用组件 onHide onLoad, onShow
页面重定向 调用 API wx.redirectTo 使用组件 onUnload onLoad, onShow
页面返回 调用 API wx.navigateBack 使用组件 用户按左上角返回按钮 onUnload onShow
Tab 切换 调用 API wx.switchTab 使用组件 用户切换 Tab
重启动 调用 API wx.reLaunch 使用组件 onUnload onLoad, onShow

参考资料
https://lrdcq.com/me/read.php/66.htm
https://kangzubin.com/wxapp-decompile-1/
https://zhuanlan.zhihu.com/p/36710658
https://developers.weixin.qq.com/miniprogram/dev/framework/quickstart/