移动端运行JS脚本调试方案-移动端测试库

775 阅读5分钟

前言

移动端运行JS脚本调试方案-单元测试中,主要利用Jest进行单元测试来解决JS脚本的调试问题,保证逻辑正确。针对脚本的测试还有环境适配的问题,这类问题在笔者实践过程中基本没有遇到,但也不排除会有此类问题。如果直接将脚本接入到既有的流程,受限于流程链路的缘故不能很方便的测试。故本文着重记录关于在Andriod、iOS编写一个模块进行模拟测试。本文会参杂一些Android、iOS原生的代码,可选择性阅读。文末也会贴出笔者的Demo,也可对照阅读。

设计

设计一个测试JS脚本的原生模块,笔者希望它有以下几点功能:

  1. 集成原生JS引擎(Android为QuickJs,iOS为JavascriptCore),JS脚本直接通过引擎执行。
  2. 支持动态下发测试脚本。
  3. 支持执行过程中的异常捕获,错误日志打印
  4. 支持多个脚本线性调用

落实到实现上就是:

  1. 实现的模块依赖此引擎,并模拟原生调用流程:

    • 原生对象注入
    • 注入脚本
    • 脚本内容执行
  2. 动态下发测试脚本,有两个选择

    • 拉取接口
    • 推送 这里为了降低实现成本选择了拉取的方式,但它俩都需要有一个服务作为支持,所以这里选择采用Node服务
  3. 异常捕获的话比较好办,只需要将相应的错误打印到控制台即可。

    ps:由于QuickJs没有debug能力,所以并没有往断点调试的方向考虑,iOS的JavascriptCore是可以利用Safari调试的。这个后面代码会考虑到。

  4. 测试时,可能会遇到多个脚本有前后依赖关系,或者就单纯想测试多个脚本。这是就需要有配置参与了,还有就是由于原生的测试模块是抽象的,所以需要配置来设置需要注入的原生对象。所以Node服务就多了一个拉取配置的功能。

ps:补充一点,这里其实还考虑测试结果回传的,但是目前能回传的内容不多且测试还是针对一对一的层面,结合原生开发工具去调试可能比较合理。

最终的时序图为: image.png

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

涉及需要注入原生对象,以及执行过后的内存释放。这里定义了onExportonDispose作为回调。

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)