阅读须知

2022年10月22日

阅读须知

注意

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

本文档完善中。

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

第二代API和第一代API的区别

Node.js(第二代API)对比Rhino(第一代API)的优势是:

  • Node.js引擎的JavaScript执行性能是Rhino的100倍以上
  • 使用Node.js引擎的代码加密强度高,目前不能被还原
  • Node.js支持ES2021以上语言标准,Rhino仅支持ES5和部分ES6特性
  • Node.js引擎本身的Bug基本很少,而Rhino引擎的模块系统、语言实现本身有不少Bug
  • Node.js对应的第二代API设计较好、更加标准
  • 可以使用第三方npm包
  • Node.js的网络资料较多

Node.js(第二代API)对比Rhino(第一代API)的劣势是:

  • Node.js对应的第二代API上手门槛较高,需要对Promise、异步有一定了解,尤其对新手来说
  • 第二代API的文档阅读较难,并且目前正在完善中
  • Rhino和第一代API的社区的源码、资料、示例较多
  • 第一代API使用上比较方便

刚接触CloudControl Pro时如何选择引擎

如果你是:

  1. 没有编程基础的新手,并且不想深入学习编程
  2. 代码能跑就行,不追求可维护性、可读性
  3. 不追求最新语言标准,能容忍引擎、API设计本身有不符合标准的地方和bug

那么建议你使用Rhino引擎和第一代API,上手较快。你无需特别配置,代码默认都以该引擎执行。

如果你是:

  1. 计算机专业出身或者有一定开发经验
  2. 第一次学习编程,但想学习行业标准和规范,为以后学习深入或学习Android/JavaScript/Web等打基础
  3. 有一定的代码素养和追求
  4. 有较高的软件安全、加密需求
  5. 想用npm包,实现比如连接mysql等需求
  6. 追求更高的JS运行性能
  7. 热爱编程,或者热爱探索,热爱学习

那么建议你使用Node.js引擎和第二代API,对Rhino引擎和第一代API了解即可。

提示

引擎的选择并非绝对,你可以一边使用Rhino引擎一边使用Node.js引擎,或者在学习一段时间后再看另一个引擎/API。

快速开始

本节将介绍如何使用Node.js引擎和第二代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.html

使用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界面
  • ...
在Pro 9中,所有模块均需要使用require()来导入才能使用,而不能像Pro 8一样直接使用全局变量。比如不能直接用app$app变量,而需要用const app = require('app')来导入模块。

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

每个模块的API可能与Pro 8有所不同,大部分API被设计为异步而非同步阻塞,一些全局函数、变量被设计为模块内函数。比如,setClip(), getClip()在Pro 8中是全局函数,在Pro 9中则属于模块]clip_manageropen in new window的函数。

使用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

之前就可以参考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提供了全局对象$autojsopen in new window,提供一些特殊的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对象的文档open 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模块文档open in new window后,会看到一个列表:

  • 接口: 接口,第一次看文档,直接跳过这部分内容。
  • 变量: 本模块的变量。我们在这个列表中看到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呢?

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

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

"nodejs";
const app = require("app");
app.startActivity({
    "root": false,
    "action": "android.intent.action.VIEW",
    "data": "http://smartcloudscript.com",
});
上次编辑于:
贡献者: Bruce