JavaScript for Automation(JXA) 入门

从 macOS 10.10 开始(当时还叫 OS X),苹果提供了一种新的使用 JavaScript 语言编写自动化脚本的方式,和已有的 AppleScript 一样,能够方便我们操作其他应用,自动化地处理一些事务。然而使用 JavaScript 编写代码要比 AppleScript 容易得多。

0. 学习一个新工具的时候,我们到底在学什么?

在了解 JXA 之前,先考虑一个问题:当决定要学习使用 JXA 的时候,我们到底需要学习什么?不仅是本文档介绍的 JXA,学习任何一个新工具,都逃不过这个问题:

  • 了解基本概念
  • 安装开发工具
  • 配置开发环境
  • 学习编程语言
  • 了解基本框架、工具的使用

1. JXA 基本概念

在日常工作中,我们通常需要处理很多复杂、重复且耗时的任务。通过编写脚本,我们可以自动化地处理一些与应用、进程和操作系统交互的事务,从而提高效率,减少错误,节约时间。

脚本是如何工作的

开放脚本架构(Open Scripting Architecture, OSA)为 OS X 提供了夸应用通信的标准和扩展机制,这种通信发生于应用(Applecations)之间交换苹果事件(Apple Events)。Apple Events 即一种封装了命令和数据的进程间通信(interprocess message)

一个脚本化应用(scriptable application)通过执行操作或提供数据来响应 Apple events。每个脚本化应用都实现了自己的脚本功能,并通过脚本字典(scripting dictionary)公开自己独特的术语。

在 OS X 中,OSA 提供以下能力:

  • 开发者可以创建脚本化应用并公开脚本术语
  • 用户可以通过脚本语言编写脚本
  • 使用 Apple events 可以在同一台计算机的不同应用或不同计算机之间进行通信

开放脚本框架(Open Scripting framework)中定义了用于创建脚本组件(scripting components)的标准数据结构、程序和资源。该框架同时提供API,用于编译、执行、加载和存储脚本。
苹果事件管理器(Apple Event Manager)为创建脚本化应用提供基础支持,由 CoreServices 框架内的 AE 框架实现。开发者可以通过 Foundation 框架中的 Apple Event API 与 Apple Event Manager 进行交互。

下图展示了上述组件如何在操作系统中协同工作[1]

The Open Scripting Architecture workflow

脚本的类型有哪些

  • Applets: 已保存为app的脚本,行为与其他app类似,双击可以启动并运行。运行一个 Applet 时,脚本run函数中的代码会执行。如果脚本没有run函数,则视脚本的顶级域为隐式的run函数。
  • Droplets: 可向图标拖放文件和文件夹的脚本。双击可以启动并运行脚本的run函数,或将文件、文件夹拖放到图标上进行处理。在一个 Droplet 脚本中,拖放进来的文件或文件夹将会传入openDocuments函数处理。
  • Scripts: 脚本文档。双击可以打开并编辑。一些应用和进程可以加载并运行 Script。例如,邮件规则可以执行脚本来处理符合特定条件的邮件。脚本有时被称为编译脚本(compiled scripts)
  • Script bundles: 以 bundle 形式保存的脚本文档。Bundle 是一个具有标准化分层结构的目录,它包含可执行代码和该代码使用的资源。
  • Stay-open scripts: 默认情况下,applets 和 droplets 在启动后运行并退出。但是,当配置为保持打开状态时,它们将保持打开状态,直到明确命令退出为止。通常,保持打开的脚本包含idle函数,它启动定期操作。

在 Mac 上,编写自动化脚本主要使用 AppleScript 和 JavaScript。本文档将从零开始介绍如何使用 JavaScript 编写自动化脚本。

2. 安装工具

从 macOS 10.10 开始,苹果为 OSA 加入了 JavaScript 的支持,在已有能够运行 AppleScript 脚本的地方,都可以运行 JavaScript 编写的脚本。因此我们不需要安装额外的任何软件,使用Script Editor应用或命令行使用osascript命令即可运行 JavaScript 脚本。

3. 开发环境

Script Editor

打开脚本编辑器应用/Applications/Utilities/Script Editor.app

Hello world

新建一个脚本,输入

console.log('hello world')

点击运行:


image

寻求帮助

如果我们需要查找某个应用的脚本字典,在状态栏点击文件->打开词典...,在弹出的对话框中选择应用。

脚本字典

REPL

执行命令osascript -il JavaScript。其中-i表示交互模式,-l JavaScript表示使用 JavaScript ,不加该标签则默认使用 AppleScript。

➜  ~ osascript -il JavaScript
>> console.log('hello world.')
hello world.
=> undefined
>> 

Shell Script

执行命令osascript -l JavaScript -e '$.NSLog("hello ObjC")':

➜  ~ osascript -l JavaScript -e '$.NSLog("hello ObjC")'
2018-09-18 16:43:31.327 osascript[35632:34312370] hello ObjC

Shebang Script

创建文件并保存到./test.js

#!/usr/bin/env osascript -l JavaScript

function run(argv) {
  console.log(JSON.stringify(argv))
}

执行命令./test.js -a -b -c -d lll

➜  ~ ./test.js -a -b -c -d lll
["-a","-b","-c","-d","lll"]

osascript -l JavaScript ./test.js -a aaa -b bbb

➜  ~ osascript -l JavaScript ./test.js -a aaa -b bbb
["-a","aaa","-b","bbb"]

4. 编程语言

JavaScript 本身无需多言。值得关注的是,OSA 对 JavaScript 功能的支持取决于 macOS 的版本。JXA 使用的 JavaScriptCore 引擎版本似乎与 macOS 捆绑的 Safari 版本相对应[2]。参阅 kangax's compatibility table 并找到 SF 列查看不同 Safari 版本的支持情况。

5. 框架,全局对象,第三方库以及其他……

脚本中的事件处理函数(Event Handlers)

脚本中实现特定的名字的函数,可以接收特定的事件:

  • run:脚本运行时调用该函数。默认情况下,脚本顶级域中的所有代码都被视为包含在隐式的run函数中。

    function run() {
        var app = Application.currentApplication()
        app.includeStandardAdditions = true
        app.displayDialog("The script is running.")
    }
    
  • quit:当脚本退出时调用。这个函数是可选的,如有必要,可以在该函数中执行数据清理操作。

    var app = Application.currentApplication()
    app.includeStandardAdditions = true
     
    function quit() {
        app.displayDialog("The script is quitting.")
    }
    
  • openDocuments:实现该函数的脚本将自动处理拖放操作。当以这种方式启动时,openDocuments函数会传入一个参数,存有被拖放文件或文件夹的列表。

    function openDocuments(droppedItems) {
        // Process the dropped items here
    }
    
  • idle:保存脚本时,文件格式选择应用程序,同时勾选运行处理程序后保持打开。在保持打开的脚本 app 中,脚本在run函数完成后保持打开状态,并且每30秒调用一次idle函数。可以使用该函数执行周期任务,例如监视文件夹以查找要处理的新文件。要更改idle函数的调用间隔,需要函数返回一个新的间隔,单位为秒。

    保存保持打开的脚本
    var app = Application.currentApplication()
    app.includeStandardAdditions = true
     
    function idle() {
        app.displayDialog("The script is idling.")
        return 5
    }
    

全局 JS 对象

作为 JavaScript 的执行环境,JXA 包含了以下全局变量[3]

  • Automation
  • Application
  • Library
  • Path
  • Progress
  • ObjectSpecifier
  • delay
  • console.log
  • ObjC
  • Ref
  • $

JXA 与 ObjC

这是最激动人心的部分。

加载 OC 框架到 JS 上下文

JXA通过内置的 Objective-C Bridge 调用 ObjC 接口。上一节提到的ObjC$是用来调用 Objective-C Bridge 全局对象。
其中ObjC对象提供接口用于 JS 引擎访问/操作 OC 对象,而$则是 OC 对象在 JS 上下文中的映射。看不懂这句话也没关系,举个例子,使用ObjC.import()函数加载外部框架:

ObjC.import('Cocoa')
$.NSBeep()

如果直接执行$.NSBeep(),会收到错误:

>> $.NSBeep()
!! Error on line 1: TypeError: $.NSBeep is not a function. (In '$.NSBeep()', '$.NSBeep' is undefined)

这是因为NSBeep()是 Cocoa 框架的一部分,我们在使用该函数前需要调用ObjC.import('Cocoa')将函数加载到 JS 上下文中。

调用 OC 方法的 JS 语法

举个例子,下面的 OC 代码:

NSRect frame = NSMakeRect(0, 0, 200, 200);
NSWindow* window  = [[NSWindow alloc] initWithContentRect:frame
                    styleMask:NSBorderlessWindowMask
                    backing:NSBackingStoreBuffered
                    defer:NO]

在 JXA 中这样实现:

var styleMask = $.NSTitledWindowMask | $.NSClosableWindowMask | $.NSMiniaturizableWindowMask;
var frame = $.NSMakeRect(0, 0, windowWidth, windowHeight);
var window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
  frame,
  styleMask,
  $.NSBackingStoreBuffered,
  false
);

引用调用传参

NSFileManager类的方法- (BOOL)fileExistsAtPath:(NSString *)path isDirectory:(BOOL *)isDirectory;,第二个参数的类型为BOOL *,在 OC 中我们通常使用引用调用来传递这个参数:

BOOL isDir;
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager fileExistsAtPath:fontPath isDirectory:&isDir];

在 JXA 中可以使用Ref()函数获取一个变量,该变量可以传给需要引用调用的参数。调用完成后,变量数据通过isDirRef[0]返回:

var isDirRef = Ref()  //set up a variable which can be passed by reference to ObjC functions.
$.NSFileManager.alloc.init.fileExistsAtPathIsDirectory("/Users/Sancarn/Desktop/D.png", isDir)
var isDir = isDirRef[0]     //get the data from the variable

参考资料

我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2tynnwdd44ysg


  1. Mac Automation Scripting Guide

  2. JavaScript for Automation Cookbook

  3. JavaScript for Automation Release Notes

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容