【技术】【小程序】wxUnpacker 工具“SyntaxError: Illegal return statement” 错误

前言

之前提到的微信小程序解包工具,在部分小程序上无法正常反编译,表现在输出结果上为得不到页面 wxml 及 wxss 文件。

Error 解决

描述

脚本解包过程中断,控制台输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vm.js:519

return function(env,dd,global){$gwxc=0;var root={"tag":"wx-page"};root.children=[]

^^^^^^

SyntaxError: Illegal return statement
at VMScript.compile (/home/wq57885/wxappUnpacker/node_modules/vm2/lib/main.js:80:20)
at VM.run (/home/wq57885/wxappUnpacker/node_modules/vm2/lib/main.js:215:10)
at z (/home/wq57885/wxappUnpacker/wuWxml.js:366:7)
at z (/home/wq57885/wxappUnpacker/wuRestoreZ.js:244:17)
at catchZGroup (/home/wq57885/wxappUnpacker/wuRestoreZ.js:15:2)
at catchZ (/home/wq57885/wxappUnpacker/wuRestoreZ.js:19:29)
at getZ (/home/wq57885/wxappUnpacker/wuRestoreZ.js:244:2)
at wu.get.code (/home/wq57885/wxappUnpacker/wuWxml.js:354:3)
at ioLimit.runWithCb (/home/wq57885/wxappUnpacker/wuLib.js:80:8)
at agent (/home/wq57885/wxappUnpacker/wuLib.js:54:14)

解决方案

总的来说这个问题是由于小程序包结构随版本更新有变化,导致前一位大佬的 wxUnpacker 没法正确解析 wxml 及 wxss 文件,为此 19 年另一位大佬提供了解决工具larack8

原因分析

拜读了大佬解决问题的逻辑,主要如下

原工具解析思路

所有在 wxapkg 包中的 html 文件都调用了setCssToHead函数,其代码如下

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
var setCssToHead = function(file, _xcInvalid) {
var Ca = {};
var _C = [...arrays...];
function makeup(file, suffix) {
var _n = typeof file === "number";
if (_n && Ca.hasOwnProperty(file)) return "";
if (_n) Ca[file] = 1;
var ex = _n ? _C[file] : file;
var res = "";
for (var i = ex.length - 1; i >= 0; i--) {
var content = ex[i];
if (typeof content === "object") {
var op = content[0];
if (op == 0) res = transformRPX(content[1]) + "px" + res; else if (op == 1) res = suffix + res; else if (op == 2) res = makeup(content[1], suffix) + res;
} else res = content + res;
}
return res;
}
return function(suffix, opt) {
if (typeof suffix === "undefined") suffix = "";
if (opt && opt.allowIllegalSelector != undefined && _xcInvalid != undefined) {
if (opt.allowIllegalSelector) console.warn("For developer:" + _xcInvalid); else {
console.error(_xcInvalid + "This wxss file is ignored.");
return;
}
}
Ca = {};
css = makeup(file, suffix);
var style = document.createElement("style");
var head = document.head || document.getElementsByTagName("head")[0];
style.type = "text/css";
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
head.appendChild(style);
};
};

阅读这段代码可知,它把 wxss 代码拆分成几段数组,数组中的内容可以是一段将要作为 css 文件的字符串,也可以是一个表示 这里要添加一个公共后缀 或 这里要包含另一段代码 或 要将以 wxss 专供的 rpx 单位表达的数字换算成能由浏览器渲染的 px 单位所对应的数字 的数组。

同时,它还将所有被@import引用的 wxss 文件所对应的数组内嵌在该函数中的 _C 变量中。

我们可以修改setCssToHead,然后执行所有的setCssToHead,第一遍先判断出 _C 变量中所有的内容是哪个要被引用的 wxss 提供的,第二遍还原所有的 wxss。值得注意的是,可能出于兼容性原因,微信为很多属性自动补上含有-webkit-开头的版本,另外几乎所有的 tag 都加上了wx-前缀,并将page变成了body。通过一些 CSS 的 AST ,例如 CSSTree,我们可以去掉这些东西。

wxs
在 page-frame.html ( 或 app-wxss.js ) 中,我们找到了这样的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
f_['a/comm.wxs'] = nv_require("p_a/comm.wxs");
function np_0(){var nv_module={nv_exports:{}};nv_module.nv_exports = ({nv_bar:nv_some_msg,});return nv_module.nv_exports;}

f_['b/comm.wxs'] = nv_require("p_b/comm.wxs");
function np_1(){var nv_module={nv_exports:{}};nv_module.nv_exports = ({nv_bar:nv_some_msg,});return nv_module.nv_exports;}

f_['b/index.wxml']={};
f_['b/index.wxml']['foo'] =nv_require("m_b/index.wxml:foo");
function np_2(){var nv_module={nv_exports:{}};var nv_some_msg = "hello world";nv_module.nv_exports = ({nv_msg:nv_some_msg,});return nv_module.nv_exports;}
f_['b/index.wxml']['some_comms'] =f_['b/comm.wxs'] || nv_require("p_b/comm.wxs");
f_['b/index.wxml']['some_comms']();
f_['b/index.wxml']['some_commsb'] =f_['a/comm.wxs'] || nv_require("p_a/comm.wxs");
f_['b/index.wxml']['some_commsb']();

可以看出微信将内嵌和外置的 wxs 都转译成np_%d函数,并由f_数组来描述他们。转译的主要变换是调用的函数名称都加上了nv_前缀。在不严谨的场合,我们可以直接通过文本替换去除这些前缀。

wxml
相比其他内容,这一段比较复杂,因为微信将原本 类 xml 格式的 wxml 文件直接编译成了 js 代码放入 page-frame.html ( 或 app-wxss.js ) 中,之后通过调用这些代码来构造 virtual-dom,进而渲染网页。 首先,微信将所有要动态计算的变量放在了一个由函数构造的z数组中,构造部分代码如下:

1
2
3
4
(function(z){var a=11;function Z(ops){z.push(ops)}
Z([3,'index']);
Z([[8],'text',[[4],[[5],[[5],[[5],[1,1]],[1,2]],[1,3]]]]);
})(z);

其实可以将[[id],xxx,yyy]看作由指令与操作数的组合。注意每个这样的数组作为指令所产生的结果会作为外层数组中的操作数,这样可以构成一个树形结构。通过将递归计算的过程改成拼接源代码字符串的过程,我们可以还原出每个数组所对应的实际内容(值得注意的是,由于微信的Token解析程序采用了贪心算法,我们必须将连续的}翻译为} }而非}},否则会被误认为是Mustache的结束符)。下文中,将这个数组中记为z。

然后,对于 wxml 文件的结构,可以将每种可能的 js 语句拆分成 指令 来分析,这里可以用到 Esprima 这样的 js 的 AST 来简化识别操作,可以很容易分析出以下内容,例如:

  • var {name}=_n(‘{tag}’) 创建名称为{name}, tag 为{tag}的节点。
  • _r({name},’{attrName}’,{id},e,s,gg) 将{name}的{attrName}属性修改为z[{id}]的值。
  • _({parName},{name}) 将{name}作为{parName}的子节点。
  • var {name}=_o({id},..,..,..) 创建名称为{name},内容为z[{id}]的文本节点。
  • var {name}=_v() 创建名称为{name}的虚节点( wxml 里恰好提供了功能相当的虚结点block, 这句话相当于var {name}=_n(‘block’))。
  • var {name}=_m(‘{tag}’,[‘{attrName1}’,{id1},’{attrName2}’,{id2},…],[],..,..,..) 创建名称为{name}, tag 为{tag}的节点,同时将{attrNameX}属性修改为z[f({idX})]的值(f定义为{idX}与{base}的和;{base}初始为0,f返回的第一个正值后{base}即改为该返回值;若返回负值,表示该属性无值)。
  • return {name} 名称为{name}的节点设为主节点。
  • cs.*** 调试用语句,无视之。

此外wx:if结构和wx:for可做递归处理。例如,对于如下wx:if结构:

1
2
3
4
5
6
7
8
9
10
11
var {name}=_v()
_({parName},{name})
if(_o({id1},e,s,gg)){oD.wxVkey=1
//content1
}
else if(_o({id2},e,s,gg)){oD.wxVkey=2
//content2
}
else{oD.wxVkey=3
//content3
}

相当于将以下节点放入{parName}节点下(z[{id1}]应替换为对应的z数组中的值):

1
2
3
4
5
6
7
8
9
<block wx:if="z[{id1}]">
<!--content1-->
</block>
<block wx:elif="z[{id2}]">
<!--content2-->
</block>
<block wx:else>
<!--content3-->
</block>

具体实现中可以将递归时创建好多个block,调用子函数时指明将放入{name}下(_({name},{son}))识别为放入对应{block}下。wx:for也可类似处理,例如:

1
具体实现中可以将递归时创建好多个block,调用子函数时指明将放入{name}下(_({name},{son}))识别为放入对应{block}下。wx:for也可类似处理,例如:

对应(z[{id1}]应替换为对应的z数组中的值):

1
2
3
<view wx:for="{z[{id}]}" wx:for-item="{item}" wx:for-index="{index}" wx:key="{key}">
<!--content-->
</view>

调用子函数时指明将放入{fakeRoot}下(_({fakeRoot},{son}))识别为放入{name}下。除此之外,有时我们还要将一组代码标记为一个指令,例如下面:

1
2
3
4
5
6
7
8
9
10
11
12
var lK=_v()
_({parName},lK)
var aL=_o({isId},e,s,gg)
var tM=_gd(x[0],aL,e_,d_)
if(tM){
var eN=_1({dataId},e,s,gg) || {}
var cur_globalf=gg.f
lK.wxXCkey=3
tM(eN,eN,lK,gg)
gg.f=cur_globalf
}
else _w(aL,x[0],11,26)

对应于{parName}下添加如下节点:

1
<template is="z[{isId}]" data="z[{dataId}]"></template>

还有import和include的代码比较分散,但其实只要抓住重点的一句话就可以了,例如:

1
2
3
4
5
var {name}=e_[x[{to}]].i
//Other code
_ai({name},x[{from}],e_,x[{to}],..,..)
//Other code
{name}.pop()

对应与(其中的x是直接定义在 page-frame.html ( 或 app-wxss.js ) 中的字符串数组):

1
<import src="x[{from}]" />

而include类似:

1
2
3
4
5
var {name}=e_[x[0]].j
//Other code
_ic(x[{from}],e_,x[{to}],..,..,..,..);
//Other code
{name}.pop()

对应与:

1
<include src="x[{from}]" />

可以看到我们可以在处理时忽略前后两句话,把中间的_ic和_ai处理好就行了。通过解析 js 把 wxml 大概结构还原后,可能相比编译前的 wxml 显得臃肿,可以考虑自动简化,例如:

1
2
3
4
5
<block wx:if="xxx">
<view>
<!--content-->
</view>
</block>

可简化为:

1
2
3
<view wx:if="xxx">
<!--content-->
</view>

更新后

wcc-v0.5vv_20180626_syb_zp后通过只加载z数组中需要的部分来提高小程序运行速度,这也会导致仅考虑到上述内容的解包程序解包失败,这一更新的主要内容如下:

  • 增加z数组的函数:_rz _2z _mz _1z _oz
  • 在每个函数头部增加了var z=gz$gwx_{$id}(),来标识使用的z数组id
  • 原有的z数组不再存在
  • z数组已以下固定格式出现:
1
2
3
4
5
6
7
8
9
function gz$gwx_{$id}(){
if( __WXML_GLOBAL__.ops_cached.$gwx_{$id})return __WXML_GLOBAL__.ops_cached.$gwx_{$id}
__WXML_GLOBAL__.ops_cached.$gwx_{$id}=[];
(function(z){var a=11;function Z(ops){z.push(ops)}

//... (Z({$content}))

})(__WXML_GLOBAL__.ops_cached.$gwx_{$id});return __WXML_GLOBAL__.ops_cached.$gwx_{$id}
}

对于上述变更,将获取z数组处修改并添加对_rz _2z _mz _1z _oz的支持即可。需要注意的是开发版的z数组转为如下结构:

1
2
3
(function(z){var a=11;function Z(ops,debugLine){z.push(['11182016',ops,debugLine])}
//...
})//...

探测到为开发版后应将获取到的z数组仅保留数组中的第二项。以及含分包的子包采用 gz$gwx{$subPackageId}_{$id} 命名,其中{$subPackageId}是一个数字。另外还需要注意,template的 var z=gz$gwx_{$id} 在try块外。

其他

其余一些博客提到利用 larack8 大佬的工具解决问题后,还存在找不到 node 模块的问题,他们给的解决方案相同,

1
2
3
npm install -g n
n latest
npm update -g

然后再把所有需要的包 install 一遍。

这边用这个方法并没有解决问题,其次私以为该错误的根本问题还是 node 环境本身,问题在nodejs怎么查找模块上。

  • 首先,要知道 npm 全局安装到底把模块安装到了哪个目录下面。在终端运行npm prefix -g命令会打印出安装路径。而nodejs查找模块是在module.paths目录列表下面查找的。
  • 所以,一种解决方案是在程序中将npm全局安装路径添加到module.paths中。module.paths.push(‘全局安装路径’)。然后再测试可行。这种方案只对当前js有效。
  • 另一种是添加环境变量NODE_PATH,值就设置成全局安装路径。如图中所示,添加后测试可行。
  • 其实,添加环境变量NODE_PATH后,我们再去查看module.paths时会发现环境变量中的路径也已经在module.paths中了。所以,最方便的解决办法就是:npm prefix -g 找到全局安装的路径,然后添加到环境变量NODE_PATH中。

解决 require 问题之后再统一安装 wxUnpacker-larack8 ver 需要的模块即可

1
2
3
4
5
6
7
8
9
npm install uglify-es --save
npm install esprima --save
npm install css-tree --save
npm install cssbeautify --save
npm install vm2 --save
npm install uglify-es --save
npm install js-beautify --save
npm install escodegen --save
npm install cheerio --save

参考资料
https://github.com/larack8/wxappUnpacker
https://blog.csdn.net/wq57885/article/details/101113017
https://jingyan.baidu.com/article/2d5afd6937ad7785a2e28e98.html