Содержание статьи
В 2014-м JavaScript удостоился внимания серьезных ребят из Apple. На конференции WWDC 2014 была анонсирована новая технология JavaScript Automation, позволяющая создавать приложения для OS X на этом хитовом языке программирования. Попробуем познакомиться с новинкой поближе и на реальных примерах понять: а стоит ли игра свеч?
Новое — хорошо забытое старое
В любой нормальной операционной системе есть средства для автоматизации. Чаще всего они представляют собой хардкорные инструменты. Взять, к примеру, nix’ы. Здесь главенствует мощный bash, способный решать самые изощренные задачи. Проблема лишь в его сложности. Не всякий юзер решится на творческий союз с этим швейцарским ножом. Получается, что инструмент есть, а пользуются им только продвинутые единицы.
Аналогичная ситуация с Windows, у которой есть свой интерпретатор в виде CMD. Более простой, но и менее функциональный — тягаться с bash ему не под силу. Результат тот же — инструмент есть, а работать с ним желания нет.
Специально для таких пользователей были придуманы всевозможные прослойки и альтернативы. Например, в Windows все желающие могут писать на том же JavaScript (причем очень давно) или VBS. Позже добавился мощный инструмент в виде PowerShell, дающий возможность более доступной автоматизации.
Что почитать по теме?
Обычно я привожу кучу полезных ссылок к статье, но сегодня набор будет более скудным. JXA слишком молодая технология, и большого объема литературы по ней пока нет. Есть отдельные обзоры зарубежных коллег, немного вопросов с ответами (причем вопросов без ответов больше) на Stack Overflow и скудноватая официальная документация. Поэтому если ты всерьез решил заняться JXA, то приготовься к самостоятельному ресечингу.
- goo.gl/M3Bqx3 — официальная документация в Mac Developer Library. До нормальной документации она не дотягивает (по правде говоря, это технический пресс‑релиз), поэтому особо обольщаться не стоит. Однако прочитать ее на разок нужно: по ней раскидана куча полезных сниппетов.
- goo.gl/m5AO3h — Batch File Rename Script. Пример полноценного скрипта с использованием JavaScript for Automation. Как видно из названия, сценарий позволяет переименовывать файлы, выделенные в Finder. Вторая интересная особенность примера — готовность к использованию с Automator.
- goo.gl/veMVWn — репозиторий с примерами, демонстрирующими использование моста к Objective-C. Примеры простейшие и призваны помочь разобраться с применением стандартных элементов управления в JavaScript.
- goo.gl/87SnZb — статья Building OS X Apps with JavaScript. Добротный материал с примером разработки небольшого приложения для OS X, с использованием фреймворка Foundation (не путать с одноименным CSS-продуктом).
- goo.gl/2egSwH — официальная документация по Foundation Framework.
- goo.gl/NrftJs — JavaScript for Automation Cookbook. Репозиторий с полезной информацией о применении JXA. Информации не так много, как хотелось бы, но упускать из виду ее нельзя.]
Мир 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 теоретически можно уже больше десяти лет, то в чем же тогда фишка столь обсуждаемого анонса? Неужто Apple созрела для запоздавшей маркетинговой кампании?
В последней версии OS X (Yosimite) была проделана большая работа, давшая возможность более тесного взаимодействия с системой. Тут речь даже не о JavaScript, а о появлении целого комплекса API, библиотек, позволяющих в перспективе применять для автоматизации не только JavaScript, но и другие языки программирования. Если не упираться в технические детали, то это блюдо можно уподобить .NET (уж прости за грубое сравнение). То есть в нашем распоряжении оказывается одно программное ядро, одинаково хорошо работающее с другими языками программирования.
Возникает резонный вопрос: а почему первопроходцем выбрали JavaScript? Вряд ли кто ответит на него точно — официальная информация отсутствует. Мне кажется, что не последним за в этом вопросе стала его популярность. Сегодня этот язык переживает бум, и армия его разработчиков уверенно растет. Грех не воспользоваться столь удачным стечением обстоятельств.
Ну а теперь — немного технических деталей.
Тесная интеграция с системой
Речь уже не идет о банальной автоматизации в стиле «открыл программу → кликнул кнопку». Продвинутые пользователи получают возможность взаимодействовать с нативными фреймворками и библиотеками. Раньше фича была доступна знатокам AppleScript, а сегодня ее расширили и отдали в лапы ушлых JavaScript’еров. Благодаря доступу к Cocoa API разработчики могут создавать приложения с нативным интерфейсом прямо на JS. Причем в большинстве случаев не будет никаких существенных провисаний в скорости по сравнению с применением Objective-C.
Простой диалог с приложениями
Взаимодействие с приложениями сводится к заполнению свойств и выполнению методов соответствующих объектов. Никаких хитроумных и монструозных названий! Все сделано в расчете на, скажем так, программистов средней продвинутости.
Дружба с Automator
Automator (визуальное средство для автоматизации) не осталось в стороне, и его сразу подружили с JavaScript. Теперь, помимо визуальных «кубиков» с логикой AppleScript, реально использовать трушный код на JS.
Документация
Презентация говорит о хорошей документации, но, на мой взгляд, здесь все не так идеально. Да, библиотека с описанием свойств/методов стоковых приложений сделана хорошо. Приведено описание всех встроенных приложений, и попробовать себя в роли гуру OS X автоматизации становится возможным минут через 15 (само собой, при наличии опыта программирования). А вот в вопросах более тесного взаимодействия с системой возникают некоторые пробелы. Впрочем, уверен, что это вопрос времени.
Готовим инструменты
Начнем с редактора кода. В принципе, код можно писать в чем угодно. Для меня в последнее время стал стандартом свободный редактор Brackets. Правда, для первого знакомства с JavaScript Automation все же лучше воспользоваться стандартным редактором скриптов. Он находится в «Программы → Утилиты».
На мой взгляд, стандартный редактор выглядит ущербно — отсутствуют привычные программерскому глазу необходимые инструменты. Нет даже элементарной нумерации строк. Главный его плюс — простота запуска написанного кода. Закодили несколько строчек кода и одной кнопкой выполнили тестовый запуск.
Аналогичного поведения можно добиться с любым другим редактором, но тогда придется потратить время на настройку. Я этим вопросом пока не заморачивался, но думаю, что особых сложностей возникнуть не должно. Во всяком случае, утилита osascript (о ней немного позже) покрывает все потребности по запуску сценариев из консоли.
Во время написания кода будет крайне полезен встроенный в редактор скриптов журнал событий (Окно → Журнал событий). Из него JXA-девелопер черпает информацию, необходимую для отладки. На первых порах туда заглядывать придется часто, так как даже наличие опыта в разработке на JavaScript не спасет от некоторых неожиданностей, присущих JXA.
Сразу взглянем на еще один инструмент, без которого вряд ли удастся написать серьезный сценарий, — «Библиотеку». Библиотека хранит информацию о методах и свойствах стандартных приложений. Как задумал что‑то автоматизировать — сразу загляни в нее (Окно → Библиотека).
Теперь попробуем проверить все это на практике и сотворить простейший скрипт. Пусть это будет традиционный Hello, World, но только свое приветствие миру мы скажем голосом. Сначала в окне редактора скриптов сменим язык программирования на 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() + "\n";}textEdit.documents["pagesFromBrowser.txt"].text = content;}
Этот сценарий сохранит ссылки на вкладки во всех открытых окнах браузера. Для наглядности адрес сайта отделяется от заголовка страницы символом табуляции. Теперь немного заострим внимание на коде.
Первым делом необходимо установить связь с желаемым приложением. В моем случае это Google Chrome и TextEdit. Нам требуется создать экземпляр объекта для дальнейшего взаимодействия. Для этого мы берем и выполняем глобальный метод Application. В качестве параметра ему необходимо передать имя приложения (ID процесса, путь к приложению). Если все прошло нормально, можно приступать к работе.
После получения экземпляра объекта следует сразу открыть библиотеку и посмотреть доступные свойства/методы у выбранного приложения. Я специально выбрал 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. Попробуем подключиться к этому приложению и сформировать новое письмо. Этот пример любят приводить все блогеры, но они ограничиваются созданием нового письма с заполненной темой и текстом. Вроде бы ничто не мешает расширить пример, но тут обязательно (Stack Overflow тому подтверждение) возникают трудности. Во втором листинге я привел полноценный код скрипта, позволяющий создать новое письмо, добавить несколько получателей и приаттачить произвольный файл.
Листинг 2. Работаем с mail
myMailApp = Application("Mail");bodyTxt = "Привет, мен! Это пример отправки письма с помощью JS из OS X.\n\n" + "Все предельно просто и понятно."; 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 -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. Чем глубже ты будешь погружаться в эту тему, тем больше заметишь ограничений.
В поисках alert’a
Первое, что бросается в глаза начинающим JXA разработчикам, — отсутствие стандартных функций вроде Alert или Promt. В этом нет никакой ошибки, так как все перечисленные функции имеются только в браузерах. Для отображения диалоговых окон в JXA правильнее пользоваться плагином (includeStandartAdditions) или нативными API из библиотеки Cocoa. Вот два полезных сниппета (с использованием плагина и Cocoa соответственно):
// Alert средствами плагинаfunction alertPlugin(text) { App = Application.currentApplication(); App.includeStandardAdditions = true; App.displayAlert(text);}// Cocoa alertObjC.import('Cocoa')function alertCocoa(text) { var alert = $.NSAlert.alloc.init var window = alert.window window.level = $.NSStatusWindowLevel alert.messageText = text var result = alert.runModal}
Не стоит также забывать, что 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, безусловно, интересное решение, но пользоваться им стоит осторожно. С точки зрения возможности банальной автоматизации (взаимодействие со стоковыми приложениями) все просто шикарно. JavaScript-разработчикам развязали руки, и теперь они могут творить мелкие полезняшки в своем любимом формате. Что касается пресловутого моста к нативным библиотекам, то нет проблем, пользуйся, но не забывай об описанных мною чуть выше ограничениях. Планируешь серьезный проект — делай его с помощью нативного инструментария.