前言
在移动端运行JS脚本调试方案-单元测试中,主要利用Jest进行单元测试来解决JS脚本的调试问题,保证逻辑正确。针对脚本的测试还有环境适配的问题,这类问题在笔者实践过程中基本没有遇到,但也不排除会有此类问题。如果直接将脚本接入到既有的流程,受限于流程链路的缘故不能很方便的测试。故本文着重记录关于在Andriod、iOS编写一个模块进行模拟测试。本文会参杂一些Android、iOS原生的代码,可选择性阅读。文末也会贴出笔者的Demo,也可对照阅读。
设计
设计一个测试JS脚本的原生模块,笔者希望它有以下几点功能:
- 集成原生JS引擎(Android为
QuickJs,iOS为JavascriptCore),JS脚本直接通过引擎执行。 - 支持动态下发测试脚本。
- 支持执行过程中的异常捕获,错误日志打印。
- 支持多个脚本线性调用。
落实到实现上就是:
-
实现的模块依赖此引擎,并模拟原生调用流程:
- 原生对象注入
- 注入脚本
- 脚本内容执行
-
动态下发测试脚本,有两个选择
- 拉取接口
- 推送
这里为了降低实现成本选择了拉取的方式,但它俩都需要有一个服务作为支持,所以这里选择采用
Node服务。
-
异常捕获的话比较好办,只需要将相应的错误打印到控制台即可。
ps:由于
QuickJs没有debug能力,所以并没有往断点调试的方向考虑,iOS的JavascriptCore是可以利用Safari调试的。这个后面代码会考虑到。 -
测试时,可能会遇到多个脚本有前后依赖关系,或者就单纯想测试多个脚本。这是就需要有配置参与了,还有就是由于原生的测试模块是抽象的,所以需要配置来设置需要注入的原生对象。所以
Node服务就多了一个拉取配置的功能。
ps:补充一点,这里其实还考虑测试结果回传的,但是目前能回传的内容不多且测试还是针对一对一的层面,结合原生开发工具去调试可能比较合理。
最终的时序图为:
Node服务
Node服务比较简单,利用Express搭建。具体代码如下:
const express = require('express')
const path = require('path')
const app = express()
app.listen(8082, () => {
console.log('running at http://127.0.0.1');
})
app.get('/config', (req, res) => {
res.sendFile(path.resolve(__dirname, './Config.json'))
})
app.get('/script/*', (req, res) => {
let url = req.originalUrl
url = url.replace('/script', path.resolve(__dirname, '../dist/'))
res.sendFile(url)
})
Node服务提供了两个接口:
/config用于上述提到的拉取配置。/script/*用于上述提到的拉取脚本,这里的脚本是前面文章中提到的采用Typescript编写并编译后的产物。 ps:这里采用了类似重定向的思路,将get请求的地址转换成文件真实地址。
测试配置
[
{
"script": "test.js",
"exports": [
"deviceInfo",
"database"
],
"eval": "test.testFetchOne()"
}
]
还记得上述提到的原生调用流程,配置主要描述的是要执行哪个脚本、注入哪些原生对象以及执行什么脚本内容的问题。
这里配置的是之前笔者编写的Demo脚本。需要执行一个名为test.js的脚本;需要注入deviceInfo作为平台信息,需要注入database作为数据库;需要执行test.testFetchOne()测试脚本。
原生模块
测试入口-JsEngineDebug
两端都会定义一个JsEngineDebug作为测试的入口,以及上述提到的流程逻辑也都在该类中实现。
Android:
public class JsEngineDebug {
private static final String TAG = JsEngineDebug.class.getSimpleName();
private final OkHttpClient okHttpClient;
private final JsEngineDebugCallback callback;
private final String host;
private QuickJs quickJs;
iOS:
public class JsEngineDebug: NSObject {
private static let TAG = "JsEngineDebug"
private let host: String
private let onExport: ((_ exports: [String], _ ctx: JSContext) -> Void)
private let onDispose: (() -> Void)
private let disposeBag = DisposeBag.init()
private var jsContext: JSContext? = nil
相关回调-Callback
涉及需要注入原生对象,以及执行过后的内存释放。这里定义了onExport和onDispose作为回调。
Android为接口JsEngineDebugCallback
public interface JsEngineDebugCallback {
void onExport(String[] exports, QuickJs js);
void onDispose();
}
iOS为两个闭包:
private let onExport: ((_ exports: [String], _ ctx: JSContext) -> Void)
private let onDispose: (() -> Void)
尾巴
值得注意的是,这里会将JS引擎的上下文(QuickJs/JSContext)作为全局变量,在每次脚本成功执行完一个流程后才会释放,但在执行异常时不会释放。目的是为了可以在出错时对上下文进行调试。(ps:当然,目前此操作仅对iOS有效,可用Safari进行调试)。
其他内容都是些常规的业务代码,且由于是debug库所以写得比较随意,就不一一介绍了。模块目前还不够干净,譬如为了做流程整合和线程切换引入了Rx;Android为了方便进行网络请求引入了OkHttp等。
为了适配debug的情况,所以在Android中采用了no_op的形式,release环境下可依赖no_op
debugImplementation project(':quick_js_debug')
releaseImplementation project(':quick_js_debug_no_op')
iOS由于采用Pod形式,也可配置只在Debug模式下引入,并在代码中编写好DEBUG的条件编译。
最后
以上就是关于脚本在移动端模拟测试的实现方案。动态的脚本下发,可以更方便地在移动端环境上测试JS脚本。
最后再贴一下Demo地址:xcyoung/mobile-js-engine-exsample (github.com)