Персональный блог Игоря Антонова aka "spider_net"

Разбираемся с JavaScript Automation для OS X


Рубрика: JavaScript -> Программирование -> Журнал Хакер -> Статьи
Метки: | | | | | | |
Просмотров: 11213
Разбираемся с JavaScript Automation для OS X

Последние годы JavaScript уверенно держится на Олимпе популярных языков программирования. Многочисленные фреймворки, разработка под популярные платформы закрепляют успех и стирают в памяти гадкие клише прошлого. Язык растет, развивается и становится логичнее, что не может не радовать многотысячную армию его фанатов.

В 2014-м JavaScript удостоился внимания серьезных ребят из Apple. На конференции WWDC 2014 была анонсирована новая технология “JavaScript Automation”, позволяющая создавать приложения для OS X на этом хитовом языке программирования. Попробуем познакомиться с новинкой поближе и на реальных примерах понять, а стоит ли игра свеч?

Новое - хорошо забытое старое

В любой нормальной операционной системе есть средства для автоматизации. Чаще всего они представляют собой хардкорные инструменты. Взять, к примеру, nix’ы. Здесь главенствует мощный BASH, способный решать самые изощренные задачи. Проблема лишь в его сложности. Не всякий юзер решится на творческий союз с этим швейцарским ножом. Получается, что инструмент есть, а пользуются им только продвинутые единицы.

Аналогичная ситуация с Windows, у которой есть свой интерпретатор в виде CMD. Более простой, но и менее функциональный - тягаться с BASH ему не под силу. Результат тот же - инструмент есть, а работать с ним желания нет.

Специально для таких пользователей были придуманы всевозможные прослойки и альтернативы. Так, например, в Windows все желающие могут писать на том же JavaScript (причем очень давно) или VBS. Позже еще добавился мощный инструмент в виде PowerShell, делающий возможность более доступной автоматизации.

Мир OS X сталкивался с подобной ситуацией. Как не крути, а в основе этой ОС всегда лежала BSD. Следовательно, средства автоматизации в нем исходно следуют традиционному unix-way: BASH или любой другой язык программирования.

Есть только одно “но”. В отличие от своих предков, OS X старается по максимуму упрощать пользователям жизнь. Их средний пользователь – вовсе не хардкорный гик, ему не нужны 100500 малопонятных фич и великолепные языковые пассажи. Для него априори важна юзабельность и простота - чем проще, тем лучше.

Бородатые пользователи должны помнить, что возможность писать автоматизирующие скрипты (сценарии) на JS появилась еще в далеком 2001 году. Увы, тогда популярность он не завоевал и несколькими годами позже его сменил язык AppleScript, надолго ставший стандартом. Это был не просто «еще один язык программирования», а новый взгляд на программирование для обычных людей. Вместо привычных матерым разработчикам синтаксических конструкций, AppleScript общался языком, похожим на человеческий.

Маркетинговые усилия со стороны Apple и радужные отзывы простых смертных, сделали из AppleScript небольшой культ, который с выходом визуального средства для автоматизации под названием Automator только окреп.

Получается, что JavaScript (JS OSA) в яблочном строю был давно, но по воле рока и ввиду своей юности, несправедливо был заброшен на задворки. И эту ситуацию легко можно понять, если вспомнить, что в начале нулевых JS ассоциировался больше с хулиганским инструментом для издевательств над браузером, нежели с универсальным языком программирования…

JavaScript for Automation

Если программировать на JavaScript для OS X теоретически возможно больше десяти лет, то в чем же тогда фишка столь обсуждаемого анонса? Неужто компания созрела на запоздавшую маркетинговую компанию?

В последней версии OS X (Yosimite) была проделана большая работа, давшая возможность более тесного взаимодействия с системой. Тут речь даже не о JavaScript, а о появлении целого комплекса API, библиотек, позволяющих в перспективе применять для автоматизации не только JavaScript, но и другие языки программирования. Если не упираться в технические детали, то это блюдо можно сравнить с .NET (уж прости за грубое сравнение). То есть, в нашем распоряжении оказывается одно программное ядро, одинаково хорошо работающее с другими языками программирования.

Языки программирования и автоматизация

Возникает резонный вопрос: “А почему первопроходцем выбрали JavaScript?”. Вряд ли кто ответит на этот вопрос точно - официальная информация отсутствует. Мне кажется, что не последним “за” в этом вопросе стала его популярность. Сегодня этот язык переживает бум и армия его разработчиков уверенно растет. Грех не воспользоваться столь удачным стечением обстоятельств.

Ну а теперь – немного технических деталей.

Тесная интеграция с системой

Речь уже не идет о банальной автоматизации в стиле “открыл программу -> кликнул кнопку”. Продвинутые пользователи получают возможность взаимодействовать с нативными фреймворками и библиотеками. Раньше фича была доступна знатокам AppleScript, а сегодня ее расширили и отдали в лапы ушлых JavaScript’еров. Благодаря доступу к Cocoa API, разработчики могут создавать приложения с нативным интерфейсом прямо на JS. Причем, в большинстве случаев не будет никаких существенных провисаний в скорости по сравнению с применением Objective-C.

Простой диалог с приложениями

Взаимодействие с приложениями сводится к заполнению свойств и выполнению методов соответствующих объектов. Никаких хитроумных и монструозных названий! Все сделано в расчете на, скажем так, программистов средней продвинутости.

Дружба с Automator

Automator (визуальное средство для автоматизации) не осталось в стороне и его сразу подружили с JavaScript. Теперь, помимо визуальных “кубиков” с логикой AppleScript, реально использовать трушный код на JS.

Применение Automator вместе с JS

Документация

Презентация говорит о хорошей документации, но на мой взгляд, здесь все не так идеально. Да, библиотека с описанием свойств/методов стоковых приложений сделана хорошо. Приведено описание всех встроенных приложений и попробовать себя в роли гуру OS X автоматизации становится возможным минут через 15 (само собой, при наличии опыта программирования). А вот в вопросах более тесного взаимодействия с системой возникают некоторые пробелы. Впрочем, уверен, что это дело времени.

Список приложений для автоматизации

Готовим инструменты

Начнем с редактора кода. В принципе, код можно писать в чем угодно. Для меня в последнее время стал стандартом свободный редактор Brackets. Правда, для первого знакомства с JavaScript Automation все же лучше воспользоваться стандартным редактором скриптов. Он находится в “Программы” -> “Утилиты”.

Стандартный редактор скриптов

На мой взгляд, стандартный редактор выглядит ущербно - отсутствуют привычные программерскому глазу необходимые инструменты. Нет даже элементарной нумерации строк. Главный его плюс - простота запуска написанного кода. Закодили несколько строчек кода и одной кнопкой выполнили тестовый запуск.

Аналогичного поведения можно добиться с любым другим редактором, но тогда придется потратить время на настройку. Я этим вопросом пока не заморачивался, но думаю, что особых сложностей возникнуть не должно. Во всяком случае, утилита osascript (о ней немного позже) покрывает все потребности по запуску сценариев из консоли.

Свободный редактор Brackets

Во время написания кода будет крайне полезен встроенный в редактор скриптов журнал событий (Окно -> Журнал событий). Из него JXA-девелопер черпает информацию, необходимую для отладки. На первых порах туда заглядывать придется часто, т.к. даже наличие опыта в разработке на JavaScript не спасет от некоторых неожиданностей, присущих JXA.

Сразу взглянем на еще один инструмент, без которого вряд ли удастся написать серьезный сценарий - “Библиотека”. Библиотека хранит информацию о методах и свойствах стандартных приложений. Как задумал что-то автоматизировать, сразу загляни в библиотеку (“Окно” -> “Библиотека”).

Подробное описание объектов

Теперь попробуем проверить все вышесказанное на практике и сотворить простейший скрипт. Пусть это будет традиционный HelloWorld, но только свое приветствие миру мы скажем голосом. Сначала в окне редактора скриптов сменим язык программирования на JavaScript. Затем наберем три строчки и запустим сценарий на выполнение:

App = Application.currentApplication();
    App.includeStandardAdditions = true;
    App.say("Hello, World!");

Если все введено без ошибок, то приятный женский голос (все зависит от системных настроек) произнесет замыленную в программерских кругах фразу.

Рулим браузером

В моей любимой интернет-бродилке постоянно висят десятки открытых вкладок. Увидишь что-нибудь интересное, а читать времени нет. Оставляешь вкладку открытой и даешь себе обещание: “Чуть позже прочитаю”. Вот только это “позже” не наступает никогда и вкладки начинают хаотично копиться. Энтропия нарастает, и в итоге сжирает всю доступную память. Потом разбираться в этом хламе уже не хочется и все открытые вкладки разом закрываются.

С описанной проблемой я начал бороться давно, путем проб и ошибок придя наконец к использованию расширения OneTab. Сейчас я покажу как примерно тоже самое повторить средствами JXA. Заодно на реальном примере мы увидим нюансы взаимодействия с популярными приложениями - Goolge Chrome и TextEdit. Сделаем новый скрипт и напишем в него код из листинга 1 (эх, помню, как Никиту Кислицына дико бесили эти Игоревы «листинги», он даже отдельно запрещал использование слова «листинг» в журнале :) - прим. ред.).

Листинг 1. Грабим ссылки из вкладок

var googleChrome = Application("Google Chrome");
    var textEdit = Application("TextEdit");
    var newDoc = textEdit.Document().make();
    var content = "";
    newDoc.name = "pagesFromBrowser.txt";
   for (j = 0; j <= googleChrome.windows.length-1; j++) {	
	
	var window = googleChrome.windows[j];
	
	for (var i = 0; i <= window.tabs.length-1; i++) {
		content = content + window.tabs[i].url() + "	" + window.tabs[i].name() + "
";
	}
	
	textEdit.documents["pagesFromBrowser.txt"].text = content;
   }

Этот сценарий сохранит ссылки на открытые вкладки во всех открытых окнах браузера. Для наглядности адрес сайта отделяется от заголовка страницы символом табуляции. Теперь немного заострим внимание на коде.

Первым делом необходимо установить связь с желаемым приложением. В моем случае это “Google Chrome” и “TextEdit”. Нам требуется создать экземпляр объекта для дальнейшего взаимодействия. Для этого мы берем и выполняем глобальный метод Application. В качестве параметра ему необходимо передать имя приложения (id процесса, путь к приложению). Если все прошло ok, то можно приступать к работе.

После получения экземпляра объекта следует сразу открыть “Библиотеку” и посмотреть доступные свойства/методы у выбранного приложения. Я специально выбрал Google Chrome, так как его описание в библиотеке отсутствует. Как же быть? Официальные комментарии мне не попались, поэтому я рискнул и списал название методов из раздела про Safari. Код прекрасно заработал.

С TextEdit ситуация аналогичная - устанавливаем связь и создаем новый документ. Описание всех методов и свойств берем из документации.

Поскольку у браузера может быть открыто несколько окон и в каждом из них открыты закладки, необходимо пройтись по каждому из них. Для этого перебираем коллекцию windows, а затем у очередного окна пробегаемся по вкладкам (tabs). Дальше идут стандартные возможности JS, которые в дополнительных комментариях они нуждаются.

Приведенную идею легко развить и дописать код открытия ссылок из файла. А что, получится очень даже недурно! Подобная функция когда-то даже была реализована в павшем смертью храбрых (я про его оригинальный движок) браузере Opera. Ну и само собой сделать поддержку разных браузеров. Сразу рассмотрим пример открытия новой вкладки в Google Chrome:

window = googleChrome.windows[0];
    newTab = googleChrome.Tab ({url: “http://iantonov.me”})
    window.tabs.push(newTab);

Пишем письма под копирку

Теперь взглянем на встроенный почтовый клиент (mail) с точки зрения JXA. Попробуем подключиться к этому приложению и сформировать новое письмо. Этот пример любят приводить все блоггеры, но они ограничиваются созданием нового письма с заполненной темой и текстом. Вроде бы ничего не мешает расширить пример, но тут обязательно (StackOverflow тому подтверждение) возникают трудности. Во втором листинге я привел полноценный код скрипта, позволяющий создать новое письмо, добавить несколько получателей и приаттачить произвольный файл.

Листинг 2. Работаем с mail

myMailApp = Application("Mail");
    bodyTxt = "Привет, мен! Это пример отправки письма с помощью JS из OS X.
"
			+ "Все предельно просто и понятно.";
			
   newMessage = myMailApp.OutgoingMessage().make();
   newMessage.visible = true;
   newMessage.content = bodyTxt;
   newMessage.subject = "Письмо счастья";
   newMessage.visible = true;
   newMessage.toRecipients.push(myMailApp.Recipient({address: "a@iantonov.me", name: "Igor Antonov"}));
   newMessage.toRecipients.push(myMailApp.Recipient({address: "info@iantonov.me", name: "bot"}));
   newMessage.attachments.push(myMailApp.Attachment({ fileName: "/Users/spider_net/Downloads/[rutracker.org].t4878348.torrent"}));
   myMailApp.outgoingMessages.push(newMessage);
   myMailApp.activate();

Здесь мы идем по уже знакомой тропинке - устанавливаем связь с приложением и начинаем заполнять его свойства. Названия используемых свойств и методов описаны в документации. Стоит лишь обратить внимание на стиль заполнения объекта с письмом.

Результат формирования письма

По идее, ничего необычного: инициализируем соответствующий объект и заполняем свойства. Однако, есть один нюанс, с которым я столкнулся при первом знакомстве с JXA. Смотри, по идее мы могли бы записать весь код в традиционном для JS стиле:

myMessage = Mail.OutgoingMessage({
subject: “subject”,
content: “”,
visible: true,
toRecipients: [
myMailApp.Recipient({
address: "a@iantonov.me", name: "Igor Antonov"
}),
]
...
    });

Код выглядит элегантнее, синтаксис абсолютно корректный… но пример правильно не отработает. Новое письмо будет создано, но строка с получателями и список аттачей будут пусты. На это стоит сразу обратить внимание, потому что такая ситуация будет тебя поджидать еще в нескольких ситуациях.

В приведенном примере получатель захардкожен, а в реале его данные наверняка имеются в адресной книге. Научить код работать с приложением “Контакты” - дело нескольких минут:

var contactsApps = Application("Contacts");
   var recipientFromContacts = contactsApps.people[“Igor Antonov”];
   var name = recipientFromContacts.name();
   var email = recipientFromContacts.emails[0].value();

Код опять же довольно логичный, только следует обратить внимание на получение имени и email. Помни, name() - это метод, а не свойство. Следовательно, не забываем про скобки, иначе придется долго ломать голову над вываливающимися ошибками.

Командуем интерактивно

Возможности автоматизации не ограничиваются написанием скриптов в традиционном стиле. JXA позволяет также выполнять код интерактивно, позволяя сразу же видеть результат действия каждой строки. Продемонстрировать все вышесказанное поможет утилита OSASCRIPT. Откроем терминал и запустим osascript:

$osascript -l JavaScript -i

Первым ключом мы выбрали используемый язык программирования (не забываем, все то же самое можно сделать с помощью AppleScript). Второй ключ указывает на желание работать в интерактивном режиме.

Выполнив эту команду, мы получим приглашение для ввода (>>) JavaScript кода. Попробуем для примера запустить браузер Google Chrome и открыть в нем несколько вкладок. Вводим строку и отправляем ее на выполнение нажатием клавиши Enter.

$ osascript -l JavaScript -i
    >> Chrome = Application("Google Chrome")
    => Application("Google Chrome")
    >> window = Chrome.windows[0]
    => Application("Google Chrome").windows.at(0)
    >> newTab = Chrome.Tab({url:"http://xakep.ru"})
    => app.Tab({"url":"http://xakep.ru", "make":[function anonymous]})
    >> window.tabs.push(newTab)
    => 28
    >> newTab = Chrome.Tab({url:"http://iantonov.me"})
    => app.Tab({"url":"http://iantonov.me", "make":[function anonymous]})
    >> window.tabs.push(newTab)

Меняем bash на JS

С помощью все той же утилиты osascript можно писать традиционные консольные скрипты в стиле BASH. А что делает типичный консольный скрипт? Как правило, выполняет какой-то долгий процесс (типа резервного копирования). Для любого серьезного скипта потребуется работа с параметрами от пользователя. Подобное вполне реально реализовать на JXA. Пример простейшей болванки:

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

Для теста запускаем этот сценарий из консоли и передаем ему несколько параметров:

>   $ osascript cli.js -firstArgument -twoArgument
   >> ["-firstArgument","-twoArgument"]

Профит очевиден - добавляем обработку необходимого количества параметров и делаем сценарий максимально гибким.

Кстати, если требуется выполнить небольшой код на JavaScript в консоли немедленно, то необходимости в создании отдельного сценария нет никакой:

> osascript -l JavaScript -e 'Application("Safari").windows[0].name()'
    >> JavaScript для OS X - Google Документы

JavaScript vs. Objective-C

После прочтения пестрящей примерами первой части статьи, может сложиться впечатление о нескончаемой крутости JavaScript. Отчасти это действительно так - работа со стоковыми приложениями проста, но ведь на этом JXA не заканчивается.

Помнишь, я говорил о возможности использования нативных фреймворков? Так вот, это поистине мощная фича! “Теперь-то можно не забивать голову неподатливым Objective-C и писать полноценные приложения на любимом языке” – вот мысль истинного фана JS...стоп, я тоже фанат, но ты не обольщайся ;). Возможность создавать приложения на JS с использованием нативных библиотек - фича, а не полноценная замена ObjC. Чем глубже ты будешь погружаться в эту тему, тем больше заметишь ограничений.

Не стоит также забывать, что Apple совсем недавно представила новый язык программирования Swift. В ближайшие годы он будет идти по пятам Objective-C и если эксперимент удастся, потеснит или вовсе вытеснит его. На фоне этого факта перспективы JavaScript выглядят туманно.

Все вышесказанное - не официальная информация, а сугубо личные рассуждения. За годы практики работы разработчиком мне удалось усвоить одно простое правило: даже самые крутые и впечатляющие надстройки не смогут полноценно конкурировать с нативными средствами разработки.

Мы знаем, что есть крутой HTML5 и орда фреймворков/технологий, позволяющих упаковать web-технологию в мобильное приложение, но по возможностям они всегда будут уступать нативным инструментам. Именно поэтому (любая статистика подтвердит) что 99% хитовых приложений были созданы на Objective-C, а не с помощью волшебных надстроек.

К фиче разработки под OS X на JavaScript, на мой взгляд, стоит относиться точно также. Это прекрасный повод упростить свою работу, не заморачиваясь с изучением экзотичного AppleScript, но ни в коем случае не серебряная пуля, избавляющая от использования других технологий.

Несмотря на все мое ворчание, Objective-C Bridge существует, а значит грех им не воспользоваться. Код полноценного приложения привести не смогу - я не эксперт в разработке под OS X, поэтому ограничусь созданием типичного окна с нативными элементами управления (см. листинг 3).

# Листинг 3. По мосту к Objective-C

ObjC.import("Cocoa");  
 
    var styleMask = $.NSTitledWindowMask | $.NSClosableWindowMask | $.NSMiniaturizableWindowMask; 
    var windowHeight = 85; 
    var windowWidth = 400;  
    var window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer( $.NSMakeRect(0, 0, windowWidth, windowHeight), styleMask, $.NSBackingStoreBuffered, false );  
    var newLabel = $.NSTextField.alloc.initWithFrame($.NSMakeRect(25, (windowHeight - 40), 200, 24));
    newLabel.stringValue = "Label"; 
    newLabel.drawsBackground = false; 
    newLabel.editable = false; 
    newLabel.bezeled = false; 
    newLabel.selectable = true; 
   var newEdit = $.NSTextField.alloc.initWithFrame($.NSMakeRect(25, (windowHeight - 60), 205, 24));
   newEdit.editable = false; 
   var button = $.NSButton.alloc.initWithFrame($.NSMakeRect(230, (windowHeight - 62), 150, 25)); 
   button.title = "Пимпа"; 
   button.bezelStyle = $.NSRoundedBezelStyle; 
    button.buttonType = $.NSMomentaryLightButton; 
    window.contentView.addSubview(newLabel); 
    window.contentView.addSubview(newEdit); 
    window.contentView.addSubview(button); 
 
   window.title = "Заголовок окно"; 
   window.center;
   window.makeKeyAndOrderFront(window);

Что почитать по теме?

Обычно я привожу кучу полезных ссылок к статье, но сегодня набор будет более скудным. JXA слишком “молодая” технология и большего объема литературы по ней пока нет. Есть отдельные небольшие обзоры зарубежных коллег, немного вопросов с ответами (причем, вопросов без ответов больше) на StackOverflow и скудноватая официальная документация. Поэтому, если ты всерьез решил заняться JXA, то приготовься к самостоятельному ресеченгу.

  • http://goo.gl/M3Bqx3 - официальная документация в Mac Developer Library. До нормальной документации она не дотягивает (по правде говоря, это технический пресс-релиз), поэтому особо обольщаться не стоит. Однако, прочитать ее на разок нужно, т.к. по ней раскидано куча полезных сниппетов.
  • http://goo.gl/m5AO3h - Bath File Rename Script. Пример полноценного скрипта с использованием JavaScript for Automation. Как видно из названия, сценарий позволяет переименовывать файлы, выделенные в Finder. Вторая интересная особенность примера - готовность к использованию с Automator.
  • http://goo.gl/veMVWn - Репозиторий с примерами, демонстрирующими использование моста к Objective-C. Примеры простейшие и призваны помочь разобраться с применением стандартных элементов управления в JavaScript.
  • http://goo.gl/87SnZb - статья Building OS X Apps with JavaScript. Добротный материал с примером разработки небольшого приложения для OS X, с использованием фреймворка Foundation (не путать с одноименным CSS продуктом).
  • http://goo.gl/2egSwH - официальная документация по Foundation Framework.
  • http://goo.gl/NrftJs - JavaScript for Automation CookBook. Репозиторий с полезной информацией о применении JXA. Информации не так много, как хотелось бы, но упускать из виду ее нельзя.

В поисках alert’a

Первое, что бросается в глаза начинающим JXA разработчикам - отсутствие стандартных функций вроде Alert, Promt и т.д. В этом нет никакой ошибки, т.к. все перечисленные функции имеются только в браузерах. Для отображения диалоговых окон в JXA правильнее пользоваться плагином (includeStandartAdditions) или нативными API из библиотеки Cocoa. Вот два полезных сниппета (с использованием плагина и Cocoa соответственно):

//Alert средствами плагина
function alertPlugin(text) {
	App = Application.currentApplication();
	App.includeStandardAdditions = true;
	App.displayAlert(text);
}
//Cocoa alert
ObjC.import('Cocoa')
 
function alertCocoa(text) {
  var alert = $.NSAlert.alloc.init
  var window = alert.window
  window.level = $.NSStatusWindowLevel
  alert.messageText = text
  var result = alert.runModal
}

Не все так просто

JXA, безусловно, интересное решение, но пользоваться им стоит осторожно. С точки зрения возможности банальной автоматизации (взаимодействие со стоковыми приложениями) все просто шикарно. JavaScript разработчикам развязали руки и теперь они могут творить мелкие полезняшки в своем любимом формате. Что касается пресловутого моста к нативным библиотекам, то, как говорится, проблем нет, пользуйся, но не забывай об описанных мною чуть выше ограничений. Планируешь серьезный проект – делай его с помощью нативного инструментария.

Исходники примеров

Статья опубликована в журнале "Хакер" (http://xakep.ru). Февраль 2015 г.

Ссылка на журнал: https://goo.gl/D22kkM

Оставьте комментарий!
comments powered by HyperComments