вторник, 30 августа 2011 г.

Заметки по поводу написания расширений для хрома

Вчера закончился хакатон-уикенд по расширениям для браузера Google Chrome. В связи с чем, хотелось бы сделать несколько заметок по поводу разработки расширений для этого замечательного браузера.

Я человек достаточно прямолинейный и привык, что можно начинать что-то кодить, прочитав документацию по инструменту разработки. Это работает, к примеру, с Actionscript 3, это работает с Python, да и с большинством фреймворков, написанных на этих языках. В случае с разработкой расширений для хрома все чуть иначе. Документация присутствует и выглядит даже вполне хорошо написанной. Но как только начинаешь писать что-то, довольно быстро понимаешь что документация совершенно никакая: куча нюансов не объяснена и приходится до чего-то догадываться самому или гуглить, чтобы ускорить процесс.

По сути, в расширениях для хрома есть три основных ммм... скажем так, "сущности".

Первой является popup-страница, которая появляется при клике на иконку расширения рядом со строкой браузера (или в ней, в зависимости от контекста). Страница эта представляет из себя обычный html-файл.

Вторая сущность - страница background.html, которая запущена фоном во время всей жизни расширения. Сначала может показаться неочевидным для чего она нужна - ведь можно было просто сделать возможность просто запускать фоном отдельный js-скрипт, но причина есть и она несколько нетривиальна: страница сделана только для того, чтобы можно было запустить отдельный инспектор и наблюдать в консоли за ошибками и выводом скриптов в консоль (да, это действительно выглядит как костыль, причем ощущение усиливается, когда видишь, как этот инспектор открывается). Кстати, просматривать код background-страницы можно не только у своих нераспакованных расширений, но и у установленных чужих, что позволяет спокойно красть скрипты/изображения/css - в общем, все, что вы там найдете и что душа пожелает.

Третья сущность - контент-скрипты. Обычные js-скрипты, которые запускаются в контексте открытой страницы браузера, как, к примеру, это делали скрипты юзерскипты greasemonkey, если вы такие помните, конечно.

Проблемы начинаются при попытки передать данные между этими сущностями. В api есть замечательно выглядящий (на бумаге и в документации) messaging. И вот тут начинаются неочевидные вещи. Попробуйте передать сообщения в popup-окно и вы поймете о чем я. Сначала вы скорее всего получите ошибку, сообщающую вам, что приемной стороны для сообщения нет. Через какое-то время вы догадаетесь (заметьте, не прочитаете в документации - потому что там этого банально нет - а догадаетесь), что popup-страница существует только в тот момент, когда popup-окно вызвано кликом по иконке. Т.е., когда оно скрыто, страница не прячется - ее просто не существует. Каждый раз при открытии popup'а она собирается заново подобно вновь открываемой вкладке браузера. Тогда у вас, естественно, встанет вопрос о том, как же передать popup-странице какие-либо параметры пока она закрыта, чтобы при ее открытии их обработать. Первой безумной идеей, подсмотренной где-нибудь на stackoverflow будет вызов страницы с параметрами в GET-строке (вроде popup.html?foo=bar). Судя по комментариям, это даже когда-то  работало, но по каким-то причинам в текущих версиях хрома этого делать нельзя. Тогда вы можете решить, что выйти из сложившейся ситуации можно, подписавшись на событие клика по кнопке, но тут вас тоже будет ждать сюрприз - это событие рассылается только если вы не используете (sic!) popup-окна в вашем расширении.  В общем, единственный способ получить данные из background-страницы является вызов из popup'а метода API chrome.extension.getBackgoundPage - параметры, естественно, придется сохранить в глобальной области видимости в background.html. Говорить об идиотизме такого подхода не приходится.

Окрыленные успехом, вы начинаете разрабатывать контент-скрипт и снова перед вами встает задача получить какие-то параметры из background-страницы. Что вы первым делом сделаете? Ну, конечно же, вы попробуете chrome.extension.getBackgoundPage, после неудачного опыта с messaging'ом. И увидите в консоли замечательное сообщение, гласящее, что контент-скриптом нельзя вызывать методы их пакета chrome.extension.* Не страдая от избытка тактичности, сообщение об ошибке также посоветует вам взглянуть на документацию по контент-скриптам, в которой не написано что нельзя вызывать этот метод (upd: исправили, уже написано, но размыто - сказано, что нельзя вызывать некоторые методы - не сказано какие). Почему нельзя было в документации по chrome.extension написать, что эти методы нельзя вызвать из контент-скрипта?

Что дальше? Вы попробуете передать из background-страницы сообщение контент-скрипту с помощью messaging'а. Но, скорее всего, потерпите неудачу. Есть два метода отправки сообщений - chrome.extension.sendRequest и chrome.tabs.sendRequest. По идее, первый должен рассылать сообщения из страницы background в страницу popup и по всем запущенным контент-скриптам. Но он этого не делает. Может быть он и не должен, но неужели было так сложно было написать об этом в описании метода в документации? Единственный способ передать сообщение в контент-скрипт - это либо отправить сообщение из контент-скрипта и потом отправить ему ответ (в качестве аргумента принимающей функции приходит функция для ответа на сообщение), либо, если по каким-то архитектурным причинам невозможно так сделать (например, ответ нужно послать не сразу), то нужно из контекстного скрипта узнать id вкладки, на которой он запущен (chrome.tabs.getCurrent или chrome.tabs.getSelected - в зависимости от того, что нужно) и отправить его сообщением странице background'а, где этот id сохранить и позже воспользоваться chrome.tabs.sendRequest.

Кстати, отдельно о методах chrome.tabs.getCurrent и ему подобных. Это, в принципе, покрыто в документации (например тут), но мне кажется не совсем верным: метод не возвращает данные, а передает их в качестве аргумента указанному коллбеку. Зачем это сделано для меня, признаться, загадка. Зачем этому методу неблокируемость, если ответ всегда приходит моментально? Зато доставляет определенные неудобства.

Еще одна особенность messaging'а: в сообщениях между сущностями нельзя передавать js-объекты. Вернее можно, но они сначала проходят через stringify. Зачем это делать я не знаю - мы же на локальной машине работаем в пределах браузера, а не передаем данные с клиента на сервер. Чем это плохо, кроме теоретически чуть меньшей производительности? Тем, что не получится передать, например, последовательности jQuery: stringify выпадет с ошибкой.

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