Создание кроссбраузерной оболочки для пользовательских скриптов

в 20:04, , рубрики: greasemonkey, javascript, userscript, userscripts, Веб-разработка, метки: , , ,

Здравствуйте, уважаемые читатели. Постов про пользовательские скрипты (userscripts) было на хабре немало, тем не менее, они только показывали, как ими пользоваться. А в работе юзерскриптов достаточно много кроссбраузерных несовместимостей (как и в любой области браузерного js). Естественно, можно установить различные дополнения для разных браузеров, однако, в случае написание скрипта для конечного пользователя, придётся сопровождать его огромным readme по установке компонент для обеспечения нормальной его работы. Что лично меня, да и вас, полагаю, тоже, не очень-то устраивает.

В данной статье речь будет вестись о трёх браузерах: Mozilla Firefox (с установленным GreaseMonkey), Google Chrome, Opera. Целью статьи является «заготовка», которая позволит пользовательскому скрипту работать одинаковым образом во всех перечисленных браузерах. Реализация GM API рассматриваться не будет, т.к. таковых уже сотни. Предполагается, что читатель уже знаком с общими правилами написания юзерскриптов (в случае, если нет, рекомендую сначала прочитать другую статью).

И непосредственно к теме. Начнём с директив. Несовместимостей здесь несколько: во-первых, в опере поддерживается только @include, а в хроме — только @match. Firefox поддерживает и то, и другое. Соответственно, нужно указать обе директивы (внезапно). Во-вторых, для поддержки unsafeWindow в firefox нужна директива @unwrap. Не буду уточнять, что перед использованием unsafeWindow нужно сильно подумать, правда ли это так необходимо. Итак, начальная часть нашей заготовки выглядит примерно так:

// ==UserScript==
// @name script_name
// @author author's name
// @version 1.0
// @description example
// @unwrap
// @run-at document-end
// @include http://example.com/*
// @match http://example.com/*
// ==/UserScript==

Плюс в Chrome директива @match имеет св-во отваливаться, поэтому нелишним будет дописать в начало строчку типа такой:

if (location.hostname !== "example.com")
 return;

Теперь поговорим об области видимости. В Google Chrome пользовательские скрипты всегда выполняются в отдельной области видимости — в то время как в опере, наоборот, они внаглую выполняются прямо посреди страницы, грозя уничтожением всем глобальным переменным. Поэтому для безопасности неплохо бы добавить дополнительное замыкание:

// ==UserScript==
// @name script_name
// @author author's name
// @version 1.0
// @description example
// @unwrap
// @run-at document-end
// @include http://example.com/*
// @match http://example.com/*
// ==/UserScript==

(function(){
 if (location.hostname !== "example.com")
  return;

})();

И, наконец, такая немаловажная вещь, как доступ к глобальным объектам. В Mozilla Firefox доступ к ним осуществляется через переменную unsafeWindow, в Opera скрипт выполняется непосредственно в области видимости страницы, следовательно, глобальные переменные доступны через window, в Google Chrome доступа к глобальным объектам нет вовсе (печаль). Однако unsafeWindow там можно сэмулировать посредством добавления на страницу объекта, onclick которого нам возвращает window. Собственно, код с комментариями:

// если переменной unsafeWindow у нас нету - мы в опере, и её надо объявить,
// чтобы она не выпала в window
var unsafeWindow= this.unsafeWindow;
// чтобы переменные, используемые для эмуляции unsafeWindow, не мешали другим частям скрипта,
// добавляем дополнительное замыкание
(function(){
    // т.к., несмотря на то, что он не несёт никакого функционала,
    // объект unsafeWindow в chrome, тем не менее, присутствует, проверка будет изощрённой.
    var test_scr= document.createElement("script");
    // создаём новую переменную с уникальным именем
    var tid= ("t" + Math.random() + +(new Date())).replace(/./g, "");
    // и бросаем её в глобальную область видимости.
    test_scr.text= "window."+tid+"=true";
    document.querySelector("body").appendChild(test_scr);
    // если у нас нет объекта unsafeWindow,
    // или если он не несёт никакой функциональной нагрузки
    if (typeof(unsafeWindow) == "undefined" || !unsafeWindow[tid]) {
        if (window[tid]) {
            // т.к. в опере window - и есть оригинальный window,
            // просто добавляем синоним
            unsafeWindow= window;
        } else {
            // а это - для гугл хрома
            var scr= document.createElement("script");
            scr.text= "(" +
            (function() {
                var el= document.createElement('unsafeWindow');
                el.style.display= 'none';
                el.onclick=function(){return window};
                document.body.appendChild(el);
            }).toString() + ")()"; // копируем текст ф-ции в файл script и добавляем его на страницу
            document.querySelector("body").appendChild(scr);
            this.unsafeWindow= document.querySelector("unsafeWindow").onclick();
            // экспериментальным путём установлено, что если присвоить результат
            // onclick переменной unsafeWindow сразу, то работать оно не будет
            unsafeWindow= this.unsafeWindow;
        };
    }
})(); // профит

Итак, в конечном итоге наша заготовка будет выглядеть так:

// ==UserScript==
// @name script_name
// @author author's name
// @version 1.0
// @description example
// @unwrap
// @run-at document-end
// @include http://example.com/*
// @match http://example.com/*
// ==/UserScript==

(function(){
    if (location.hostname !== "example.com")
        return;
    var unsafeWindow= this.unsafeWindow;
    (function(){
        var test_scr= document.createElement("script");
        var tid= ("t" + Math.random() + +(new Date())).replace(/./g, "");
        test_scr.text= "window."+tid+"=true";
        document.querySelector("body").appendChild(test_scr);
        if (typeof(unsafeWindow) == "undefined" || !unsafeWindow[tid]) {
            if (window[tid]) {
                unsafeWindow= window;
            } else {
                var scr= document.createElement("script");
                scr.text= "(" +
                    (function() {
                        var el= document.createElement('unsafeWindow');
                        el.style.display= 'none';
                        el.onclick=function(){return window};
                        document.body.appendChild(el);
                    }).toString() + ")()";
                document.querySelector("body").appendChild(scr);
                this.unsafeWindow= document.querySelector("unsafeWindow").onclick();
                unsafeWindow= window.unsafeWindow;
            };
        }
    })();
    // начиная отсюда, можно писать код
})();

Собственно, вот и всё. Надеюсь, кому-то поможет эта статья.

P. S. Ах да. Для отладки юзерскриптов в Firefox можно обернуть код вашего скрипта в try...catch конструкцию такого вида:
try{
// код скрипта
} catch(e){console.error(e)}
Облегчает жизнь очень сильно.

Автор: trikadin

Поделиться

* - обязательные к заполнению поля