page-freme.html在小程序中使用 WXML 文件描述页面的结构, WXSS 文件描述页面的样式。工程中有一个 app.wxss 文件用于定义一些全局的样式,会自动被 import 到各个页面中;另外每个页面也都分别包含 page.wxml 和 page.wxss 用于描述其页面的结构和样式;同时,我们也会自定义一些公共的 xxxCommon.wxss 样式文件和公共的 xxxTemplate.wxml 模板文件供一些页面复用,一般在各自页面的 page.wxss 和 page.wxml 中去 import 。 当“编译”小程序后,所有的 .wxml 文件和 app.wxss 及公共 xxxCommon.wxss 样式文件的将被整合到 page-freme.html 文件中,而每个页面的 page.wxss 样式文件,将分别单独在各自的路径下生成一个 page.html 文件。 page-freme.html 文件的内容结构如下: <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" /> <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'"> <link rel="icon" href="data:image/ico;base64,aWNv"> <script> // 一些全局变量的声明 var __pageFrameStartTime__ = Date.now(); var __webviewId__; var __wxAppCode__ = {}; var __WXML_GLOBAL__ = { entrys: {}, defines: {}, modules: {}, ops: [], wxs_nf_init: undefined, total_ops: 0 }; // 小程序编译基础库版本 /*v0.6vv_20180125_fbi*/ window.__wcc_version__ = 'v0.6vv_20180125_fbi'; window.__wcc_version_info__ = { "customComponents": true, "fixZeroRpx": true, "propValueDeepCopy": false }; var $gwxc var $gaic = {} $gwx = function(path, global) { // $gwx 方法定义(最核心) } var BASE_DEVICE_WIDTH = 750; var isIOS = navigator.userAgent.match("iPhone"); var deviceWidth = window.screen.width || 375; var deviceDPR = window.devicePixelRatio || 2; function checkDeviceWidth() { // checkDeviceWidth 方法定义 } checkDeviceWidth() var eps = 1e-4; function transformRPX(number, newDeviceWidth) { // transformRPX 方法定义 } var setCssToHead = function(file, _xcInvalid) { // setCssToHead 方法定义 } setCssToHead([])(); // 先清空 Head 中的 CSS setCssToHead([...]); // 设置 app.wxss 的内容到 Head 中,其中 ... 为小程序工程中 app.wxss 的内容 var __pageFrameEndTime__ = Date.now() </script> </head> <body> <div></div> </body> </html> 相比其他文件, page-freme.html 比较复杂,微信把 .wxml 和部分 .wxss 直接“编译”并混淆成 JS 代码放入上述文件中,然后通过调用这些 JS 代码来构造 Virtual-Dom ,进而渲染页面。 其中最核心的是 $gwx 和 setCssToHead 这两个方法。 $gwx 用于通过 JS 代码生成所有 .wxml 文件,其中每个 .wxml 文件的内容结构都在 $gwx方法中被定义好并混淆了,我们只要传给它页面的 .wxml 路径参数,即可获取到每个 .wxml 的内容,再简单加工一下即可还原成“编译”前的内容。 在 $gwx 中有一个 x 数组用于存储当前小程序都有哪些 .wxml 文件,例如,“知识小集”小程序的 x 值如下: var x = ['./pages/detail/detail.wxml', '/towxml/entry.wxml', './pages/index/index.wxml', './pages/search/search.wxml', './towxml/entry.wxml', '/towxml/renderTemplate.wxml', './towxml/renderTemplate.wxml']; 此时我们可以在 Chrome 中打开 page-freme.html 文件,然后在 Console 中输入如下命令,即可得到 index.wxml 的内容(输出一个 JS 对象,通过遍历这个对象即可还原出 .wxml 的内容) $gwx("./pages/index/index.wxml") setCssToHead 方法用于根据几段被拆分的样式字符串数组生成 .wxss 代码并设置到 HTML 的 Head 中,同时,它还将所有被 import 引用的 .wxss 文件(公共 xxxCommon.wxss 样式文件)所对应的样式数组内嵌在该方法中的 _C 变量中,并标记哪些文件引用了 _C 中数据。另外在 page-freme.html 文件的末尾,调用了该方法生成全局 app.wxss 的内容设置到 Head 中。 因此,我们可以在每个调用 setCssToHead 方法的地方提取相应 .wxss 的内容并还原。 对于 page-freme.html 文件中 $gwx 和 setCssToHead 这两个方法更详细的分析,可以参考这篇 文章 。 此外, checkDeviceWidth 方法顾明思议,用于检测屏幕的宽度,其检测结果将用于 transformRPX 方法中将 rpx 单位转换为 px 像素。 rpx 的全称是 responsive pixel ,它是小程序自己定义的一个尺寸单位,可以根据当前设备屏幕宽度进行自适应。小程序中规定,所有的设备屏幕宽度都为 750rpx ,根据设备屏幕实际宽度的不同, 1rpx 所代表的实际像素值也不一样。 *.html上面提到,每个页面的 page.wxss 样式文件,“编译”后将分别在各自的所在路径下生成一个 page.html 文件,每个 page.html 的结构如下: <style></style> <page></page> <script> var __setCssStartTime__ = Date.now(); setCssToHead([...])() // 设置 search.wxss 的内容 var __setCssEndTime__ = Date.now(); document.dispatchEvent(new CustomEvent("generateFuncReady", { detail: { generateFunc: $gwx('./pages/search/search.wxml') } })) </script> 在该文件中通过调用 setCssToHead 方法将 .wxss 样式内容设置到 Head 中,所以同样地,我们可以根据 setCssToHead 的调用参数提取每个页面的 page.wxss 。 资源文件小程序工程中的图片、音频等资源文件在“编译”后将直接被拷贝到 .wxapkg 包中,其原始的路径也保留不变,因此我们可以直接使用。 “反编译”在上一节,我们完成了 .wxapkg 包几乎所有文件内容的简要分析。现在我们介绍一下如何通过 node.js 脚本帮我们还原出小程序的源码。 在这里需要再次感谢 wxappUnpacker 作者提供的还原工具,让我们可以“站在巨人的肩膀上”轻松地去完成“反编译”。它的使用如下:
同时,作者还提供了 一键 解包并还原的脚本,你只需要提供一个小程序的 .wxapkg 文件,然后执行如下命令: node wuWxapkg.js [-d] <path/to/.wxapkg> 此脚本就会自动将 .wxapkg 文件解包,并将包中相关的已被“编译/混淆”的文件自动地恢复原状(包括目录结构)。 PS: 此工具依赖 uglify-es , vm2 , esprima , cssbeautify , css-tree 等 node.js 包,所以你可能需要 npm install xxx 安装这些依赖包才能正确执行。 更详细的用法及相关问题请查阅该开源项目的 GitHub repo。 最后,我们在 微信开发者工具 中新建一个空小程序工程,并将上述还原后的相关目录文件导入工程,即可编译运行起来,如下图为“知识小集”小程序 .wxapkg 还原后的代码工程: 以上,大功告成! 总结本文详细分析了 .wxapkg 解包后的各文件结构,并介绍了如何通过脚本“一键还原”得到任意小程序的源码。 对于一些简单的,且使用微信官方介绍的原生开发方式开发的小程序,用上述工具基本可以直接还原得到可运行的源码,但是对于一些逻辑复杂,或者使用 WePY 、 Vue 等一些框架开发的小程序,还原后的源码可能会有一些小问题,需要我们人肉去分析解决。 后续本文对小程序源码“编译”后的各文件内容结构及用途的分析相对比较零散,而且没有对各文件的依赖关系及加载逻辑进行研究,后续我们再写一些文章讲解微信客户端是如何解析加载小程序 .wxapkg 包并运行起来。 |