在冰狐平台上写自动化脚本,写到一定规模后,迟早会遇到一个问题:主流程里堆了几百行,抢购归抢购、登录归登录、数据上报归数据上报,改一处逻辑要在文件里来回翻。把功能拆到独立脚本里,主脚本只负责编排,是更可持续的做法。
冰狐提供了几种脚本间调用的途径,但它们的线程行为和入口规则完全不同,用错场景会出问题。下面从实际的调用需求出发,把 callScript、runTask 和 import 这三种机制拆开来说清楚。
先理解 callScript:它到底做了什么
callScript 是冰狐脚本间动态调用最直接的入口。它的行为可以概括为一句话:在当前线程中执行目标脚本的 main 函数,并把 main 的返回值原样返回给调用方。
这意味着两件事。第一,callScript 是同步 的:主脚本执行到 callScript 这一行时会停住,等目标脚本的 main 跑完才继续往下走。第二,它不创建新线程。目标脚本的代码运行在主脚本当前的线程上,因此如果目标脚本里有耗时操作------等待界面加载、循环轮询、网络请求------主线程就会被占住。
这个特性直接决定了 callScript 的使用边界:适合在非 UI 脚本中做确定性的逻辑调用,不适合在 UI 代码中执行可能阻塞的脚本 。冰狐的文档对此说得很明确:ui 中应该使用 runTask,而不是 callScript。
脚本路径的写法
callScript 的第一个参数是 scriptName,接受三种形式的脚本来源:
在线脚本 :直接写脚本名,比如 'test',平台会从云端拉取。
手机本地脚本 :用绝对路径或相对路径。绝对路径如 '/sdcard/scripts/login.js',相对路径直接写 'login.js'。
APK 资源脚本 :打包 App 时嵌入的 JS 文件,用 asset: 前缀,例如 'asset:login.js'。
三种来源在调用语法上没有区别,唯一的差异是路径字符串的写法。
参数传递与返回值
callScript 在脚本名之后可以接任意多个参数,这些参数会直接透传 给目标脚本的 main 函数,位置一一对应。
假设有一个独立的计算脚本 asset:calc.js:
javascript
// calc.js ------ 被调用的子脚本
function main(a, b, verbose) {
var result = a + b;
if (verbose) {
console.log('calc: ' + a + ' + ' + b + ' = ' + result);
}
return result;
}
主脚本中这样调用:
javascript
// 主脚本
function main() {
// 同步调用,12 和 30 分别对应 calc.js 的 a 和 b
var sum = callScript('asset:calc.js', 12, 30, true);
console.log('sum = ' + sum); // 输出 sum = 42
// 也可以传对象,只要子脚本的 main 能接住
var detail = callScript('asset:calc.js', { x: 5, y: 8 }, 0, false);
}
返回值就是子脚本 main 中 return 出来的值。如果子脚本没有显式 return,调用方拿到的是 undefined。这一点值得注意:callScript 的返回值完全由子脚本的 main 决定,调用方不需要做任何额外包装。
什么时候必须用 runTask
runTask 在调用语法上和 callScript 几乎一样:接受同样的脚本路径形式,同样把可变参数透传给 main。但行为有一个根本差异:runTask 会新开一个线程来执行目标脚本。
这个差异的唯一但重要的使用场景,就是 UI 脚本中调用耗时任务。
UI 脚本的 main 跑在界面线程上,如果在里面直接 callScript 一个带有等待或循环的脚本,界面会卡死。冰狐的 UI 编程规范要求耗时操作必须通过 runTask 放到新线程中执行。
一个典型的按钮点击场景:
javascript
// UI 脚本
function main() {
setupUI();
}
function onStartClick(event) {
// 不能在 UI 线程里 callScript 一个包含等待的抢购脚本
// 用 runTask 开新线程执行,UI 保持响应
runTask('asset:rush.js', 'sku_8848', 3);
toast('抢购任务已提交');
子脚本 rush.js:
javascript
// rush.js
function main(sku, retry) {
for (var i = 0; i < retry; i++) {
var ok = trySubmit(sku);
if (ok) {
console.log('抢购成功: ' + sku);
return true;
}
sleep(500); // 这个等待在新线程里,不影响 UI
}
return false;
}
需要接受的现实是:runTask 是异步的,主脚本无法直接拿到它的返回值 。runTask 调用返回时,子脚本可能还没执行完。如果主脚本确实依赖子脚本的结果,就需要通过冰狐提供的跨线程通信机制(如持久变量、全局变量、文件等)来中转,而不是指望 runTask 的返回值。
所以选型逻辑很清晰:
-
不需要 UI 保持响应、需要拿返回值 →
callScript -
在 UI 代码中调用耗时脚本、不需要立即拿返回值 →
runTask
import 和 callScript 不是一回事
import 也出现在脚本复用的讨论里,但它和 callScript 的定位不同。import 是静态导入 :在脚本加载阶段把目标脚本的代码合并进来,目标脚本里的函数和变量可以直接在主脚本中按名字调用,不需要经过 main 入口。
javascript
// 导入工具脚本
import 'asset:utils.js';
function main() {
// 直接调用 utils.js 中定义的函数,不走 main
var deviceId = getDeviceId();
var hash = md5(deviceId + '_salt');
console.log(hash);
}
utils.js 中不需要写 main,只需要把要暴露的函数声明出来:
javascript
// utils.js
function getDeviceId() {
return device.getSerial();
}
function md5(s) {
// 冰狐 JS 环境中的实现
return hash.md5(s);
}
import 适合通用函数库、常量定义、加密工具 这类需要在主脚本中反复直接调用的代码。它的代价是导入的代码会成为主脚本上下文的一部分,作用域是合并的,不如 callScript 那样有清晰的脚本边界。冰狐官方文档在讨论模块化时也把 import 作为静态复用的首选,而把 callScript 定位为"动态调用"。
一个完整的协作场景
把三种机制放在同一个流程里,能更清楚地看出它们各自的位置。
假设一个自动化任务分三层:一个入口脚本 负责任务调度,一个UI 脚本 提供手动触发按钮,一个本地核心脚本处理不想上传到云端的业务逻辑。
入口脚本(在线) :
javascript
// entry.js
function main() {
// 调用本地核心脚本,同步拿结果
var report = callScript('/sdcard/icefox/core.js', 'daily', 3);
if (report && report.success) {
console.log('核心任务完成,记录数: ' + report.count);
} else {
console.error('核心任务异常: ' + JSON.stringify(report));
本地核心脚本 /sdcard/icefox/core.js:
javascript
function main(mode, retries) {
var count = 0;
for (var i = 0; i < retries; i++) {
var ok = runOnce(mode);
if (ok) {
count++;
break;
}
sleep(1000);
}
return { success: count > 0, count: count };
}
function runOnce(mode) {
// 具体的业务操作
return true;
}
UI 脚本 中如果也要触发同一套逻辑,就不能 callScript,而应 runTask:
javascript
// ui.js
function onRunClick(event) {
这里 core.js 被两种方式调用,但路径字符串完全相同,差别只在于调用方处于什么线程环境、需不需要返回值。几个容易踩的坑
坑一:在 UI 脚本里用 callScript。 这是最常见的错误。callScript 不会报错说你"不能在 UI 中用",它只会阻塞 ,表现为界面卡住、按钮无响应。冰狐文档在 callScript 和 runTask 两处都反复提醒了这一点,但实际写代码时很容易顺手写错。
坑二:指望 runTask 的返回值。 runTask 调用本身返回的并不是子脚本 main 的返回值。它是一个"提交任务"的动作,任务提交后就返回了。需要结果,就得让子脚本把结果写到双方都能访问的地方,主脚本再读。
坑三:路径拼写错误导致的静默失败。 asset: 前缀漏写、/sdcard/ 大小写不对、在线脚本名拼错,都可能让 callScript 静默返回 undefined 而不是抛出明显的异常。在子脚本入口加一行 console.log('entered: ' + scriptName) 之类的标记,比事后排查路径高效得多。
坑四:把 import 和 callScript 混着用于同一目的。 两者解决的是不同层面的问题。import 是"把代码引进来直接用",callScript 是"把另一个脚本当作一个函数来调"。一个工具函数库用 import 合理,一个完整的业务流程脚本用 callScript 合理,反过来用就会别扭。
脚本间调用的核心不在于 API 本身有多复杂,而在于线程边界 和数据流向 这两件事。callScript 给你同步的确定性和干净的返回值,代价是不能在 UI 线程里用;runTask 给你线程安全,代价是失去直接的返回值;import 给你最轻量的复用方式,代价是作用域合并。把调用方的线程环境作为第一判断条件,选型就不会偏。冰狐的这三种机制各自覆盖了清晰的一段场景,用对了地方,脚本拆得再细也不会乱。