跳至主要內容

阅读须知

约 2953 字大约 10 分钟道无涯

阅读须知

注意

本文档为Pro 9新增的基于Node.js的第二代API的文档(第一代API仍然保留可用)。如果你想查看的是旧的第一代API的文档,请在菜单栏切换。

本文档完善中;加入内测请加QQ群569938976。内测版本有Bug为正常情况,请积极反馈和包容。内测群禁止讨论敏感、违法话题,否则一律永久拉黑。

Auto.js Pro 9是Auto.js Pro的全新版本,除了编辑器、打包等新功能外,最重要的是带来了基于Node.js的引擎和全新的第二代API(第一代API仍然保留可用),伴随着庞大的npm生态(接近200万个npm包),并仍然支持和Android/Java交互(也即可在Node.js中使用Android/Java API)。

快速开始

用Node.js引擎运行代码

为了向前兼容,Pro 9中的代码仍然默认为旧的Rhino引擎运行,要使用新的Node.js引擎可以用以下任一方式:

  1. 文件头带上"nodejs";,例如:
"nodejs";

// 打印nodejs的版本
console.log(`Node.js版本: ${process.version}`);
  1. 文件以.node.js.mjs结尾。.mjs结尾则会同时启用ES Module功能,参见https://nodejs.org/api/esm.htmlopen in new window

使用Node.js内置模块

在Node.js中,你可以使用其自带的几十个模块,比如:

  • fs: 文件系统,用于读写文件(类似Pro 8中的files模块)
  • http, https: http(s)请求与服务,用于发送http(s)请求或者搭建http服务器
  • worker_threads: 工作线程,用于并行执行任务(类似于Pro 8中的threads模块)
  • ...

全部的Node.js内置模块及其文档参见Node.js 16.x文档open in new window

以下是一个使用内置的fs模块读取文本文件的例子:

"nodejs";

const fs = require("fs");

// 使用readFile读取文件,参见https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsreadfilepath-options-callback
fs.readFile('/sdcard/脚本/test.txt', {'encoding': 'utf-8'}, (err, data) => {
    if (err) {
        console.error("读取文件失败:", err);
    } else {
        console.log("读取文件成功:", data);
    }
});

使用Pro 9内置模块

作为对Node.js内置模块的补充,Pro 9将部分Pro 8的模块迁移到了Pro 9的API中,比如:

  • app: 用于启动其他应用、获取其他应用信息,发送广播、邮件等
  • ui: 用于显示自定义界面、Web界面
  • accessibility: 用于使用无障碍API完成自动化任务
  • ...

在Pro 9中,所有模块均需要使用require()来导入才能使用,而不能像Pro 8一样直接使用全局变量。比如不能直接用app$app变量,而需要用const app = require('app')来导入模块。

所有模块的列表可在本文档右侧或右上角菜单中查看。

每个模块的API可能与Pro 8有所不同,大部分API被设计为异步而非同步阻塞,一些全局函数、变量被设计为模块内函数。比如,requestScreenCapture函数在Pro 8版本中是一个同步函数,请求截图权限,在用户操作前会一直阻塞,因此无法在UI线程中执行,在Pro 9中则是一个异步函数,返回一个Promise,需要用awaitthen来得到结果;setClip(), getClip()在Pro 8中是全局函数,在Pro 9中则属于模块]clip_manager的函数。

以下是一个使用media_projection模块请求截图权限,使用image模块寻找图片的例子:

// 使用解构语法导入模块的部分函数和变量
// 当然也可以用const image = require("image")再使用image.readImage(),但相对繁琐
const {readImage, findImage} = require("image");
const {requestScreenCapture} = require('media_projection');

// 主函数,用async标记以便使用await来等待结果
async function main() {
    // 请求截图权限
    const capturer = await requestScreenCapture();
    // 读取要寻找的图片
    const template = await readImage("./template.png");
    // 获取下一帧截图图片
    const capture = await capturer.nextImage();
    // 在截图中模板匹配template
    const result = await findImage(capture, template);
    // 打印结果
    console.log('findImage: ', result);
    // 停止截图
    capturer.stop();
    // 回收图片
    template.recycle();
}
// 执行主函数
main();

使用npm安装第三方模块

在npm上有大量的第三方模块,这些模块绝大部分都能在Pro 9中使用。在使用它们之前需要用npm命令来安装模块。

  1. npm包需要项目才能安装。在Pro 9文件管理中点击右下角菜单,选择项目,在模板中选择Node.js项目。
  2. 在新建项目页面,填好应用名称、包名(包名需要包含英文".",比如com.example),点击确定
  3. 在项目文件夹中,点击工具栏的项目图标,点击终端
  4. 输入"npm i --no-bin-links 模块名称"来安装npm包,安装后就可以在项目的代码中使用该模块

以用于生成UUID的uuid模块为例,整个过程如下:

npm-install
npm-install

之前就可以参考uuid模块的文档open in new window在main.js中使用该模块:

"nodejs";
const uuid = require("uuid");
console.log("uuid:", uuid.v4());

后续需要安装其他模块,也都在终端中,通过cd命令进入相应的项目目录。

要搜索模块,请在npm官网open in new window中搜索。

之所以要用--no-bin-links选项,是因为很多npm模块会在安装时链接一些可执行脚本到node_modules/.bin目录,但是Android的内部存储分区(sdcard)的文件系统不支持符号链接,因此我们需要使用该选项来禁用它。但是与此同时,我们常常会在npm脚本中用到这些可执行文件,例如安装webpack后运行webpack命令,安装react后运行react-scripts命令,此时只能执行具体路径的js文件来代替,比如用node node_modules/webpack/bin/webpack.js来代替。另外一个方案是将默认脚本文件夹迁移到app私有目录,这里的文件系统是支持符合链接的,你可以在设置中修改默认脚本文件夹为"~",但是需要注意,私有目录会在卸载APP后被删除,因此,

安装npm全局模块

Pro 9内置的npm也可安装全局模块,比如typescript编译ts文件,webpack-cli打包js文件等。

在终端中执行npm i -g typescript即可安装typescript模块,之后就可以在终端中执行tsc命令来编译ts文件。

注意!请勿升级内置的npm版本,否则可能遇到意料之外的问题;另外安装全局模块时不能用--no-bin-links参数,否则将无法找到相应的命令。

调用Java/Android API

Pro 9提供了全局对象$autojs,提供一些特殊的API,比如调用Java API。

例如:

"nodejs";
// 获取$java对象,用于和Java交互
const $java = $autojs.java;
// 加载Java/Android类
const StringBuilder = $java.findClass('java.lang.StringBuilder');
// 创建这个类的对象
const sb = new StringBuilder();
// 调用这个类的方法
sb.append("Hello");
sb.append(2);
console.log(sb.toString());

除了findClass外,java提供了调用Java方法时切换线程等API,参见[java提供了调用Java方法时切换线程等API,参见[java对象的文档](https://pro.autojs.org/docs/v9/interfaces/globals.Java.mdopen in new window)。

除了这种比较原始的方式外,Pro 9提供了rhino模块,用于提供类似Pro 8中rhino引擎类似的和Java交互的方式:

"nodejs";
// 调用install后,可以直接java.*, android.*等来访问Java类
require('rhino').install();

const StringBuilder = java.lang.StringBuilder;
const sb = new StringBuilder();
sb.append(android.util.Base64.decode("YXV0b2pz", 0));
console.log(sb.toString());

暂时还不支持importClass/importPackage等函数;也不支持JavaAdapter。

有关Java/Android交互的更多信息,请等待后续单独章节展开。

线程与线程模型

单线程与多线程

Node.js使用遵循带有事件循环的单线程模型,在Pro 9中也是如此,因此你不能像Pro 8那样使用threads模块启动新线程。

大多数情况你也不必使用线程,一些耗时操作,比如findImage、click等都被封装为异步操作,完全可以并行执行;调用一些Java API时,若这些API是异步操作,也可以指定Java函数执行的线程,比如:

"nodejs";
require('rhino').install();
const path = require('path');

const BitmapFactory = android.graphics.BitmapFactory;
// 当前目录下的test.png文件
const file = path.join(__dirname, "./test.png");

async function main() {
    // 调用 BitmapFactory.decodeFile(file)来解码图片文件为Bitmap
    // 这是一个耗时操作,我们指定在io线程执行
    const bitmap = await BitmapFactory.decodeFile.invoke(null, [file], 'io');
    console.log(bitmap);
    bitmap.recycle();
}
main();

如果以上均不能满足你的需求,你需要纯JavaScript计算逻辑运行于单独的线程,那么需要使用Node.js的worker_threads模块,参见Node.js文档open in new window和网上相关资料。worker_thread不像Pro 8中的threads子线程那样可以和主线程共享所有公共、全局变量等,需要额外的通信,这里就不展开了。

****目前,worker_threads中的子线程无法访问autojs相关的API和模块,比如$autojs, 只能访问Node.js内置的模块和对象。

UI线程

默认情况下,Node.js引擎运行在非UI线程,但是这样无法操作界面相关的内容;因此Pro 9提供了UI线程的选项,通过在文件头用字符串"ui-thread"或"ui"来标识,例如:

"nodejs ui";
const {isUiThread} = require("ui");
console.log(isUiThread());

"ui"和"ui-thread"是有区别的:

  • ui: 用于显示界面(Activity)的情况,比如启动后展示一个Web页面用于用户操作,参见UI模块的文档。
  • ui-thread: 不在启动时显示一个新页面,但是代码运行在UI线程,一般用于无界面的代码中显示和控制悬浮窗,参见悬浮窗模块的文档。

另外,若在非UI线程中,偶尔需要操作UI元素,比如显示和控制対话框,则可以用前述的调用Java API时切换线程的方式来实现。比如view.setText.invoke(view, ["hello"], "ui")

阅读模块文档的指引

模块的文档是通过代码生成的,阅读文档需要一些技巧,否则可能觉得文档晦涩难懂。

以app模块为例,打开app模块文档后,会看到一个列表:

  • 接口: 接口,第一次看文档,直接跳过这部分内容。
  • 变量: 本模块的变量。我们在这个列表中看到packageName,代表app模块有一个叫packageName的变量。可以用以下方式使用它:
"nodejs";
const app = require("app");
console.log(app.packageName);

点击该变量的文档,可以看到前面有常量标记,表示这是一个常量,不能修改他的值,也即app.packageName = "xxx"会报错;类型是string,字符串。结合变量名可以知道,这是当前app的包名。当然后面会有变量的中文注释,只是目前还没写,得通过变量名去猜测。

  • 函数: 本模块的函数。我们在这个列表中看到很多函数,比如editFile, startActivity。这些都是app模块的函数,以startActivity为例,点击其文档可以看到:
startActivity(target: string | IntentOptionsWithRoot): Promise<void>

它有一个参数target,类似是string或IntentOptionsWithRoot。string我们都知道是字符串,也即类似Pro 8中的app.startActivity("console")的用法。那么IntentOptionsWithRoot呢?

点击IntentOptionsWithRoot可以看到IntentOptionsWithRoot的文档。先看Properties这一栏,是描述该接口的属性,有root这个boolean属性,前面有Optional标签,表示是可选属性;再看前面的继承关系,表示IntentOptionsWithRoot继承于IntentOptions,跳转到IntentOptions,可以看到它有很多属性,比如string类型的action。其实也可以不跳转过去,勾选右上角的Inherited,可以看到所有继承过来的属性。

综合起来,我们可以知道IntentOptionsWithRoot是要我们传入一个对象,这个对象可以有root、action等可选属性,因此我们可以这样写:

"nodejs";
const app = require("app");
app.startActivity({
    "root": false,
    "action": "android.intent.action.VIEW",
    "data": "https://pro.autojs.org",
});

路线规划

关于V9版本的路线规划,请参考https://github.com/hyb1996/Auto.js/issues/526open in new window