Pull to refresh

Tips & tricks в разработке букмарклетов

Reading time6 min
Views2.6K
Так получилось, что в течение нескольких месяцев я занимался разработкой букмарклетов, сделав их около десятка. Использовал как jQuery, так и native JavaScript. О том, с какими подводными камнями я столкнулся, что нового узнал и нашел — об этом пойдет речь под катом.

1. Подключение


Те, кто уже занимался разработкой букмарклетов, этот пункт могут пропустить, для остальных — простой пример.

<a href="javascript: (function(){var a=document.createElement('script');a.type='text/javascript';a.src='http://example.com/bookm.js?'+(new Date).getTime()/1E5;document.getElementsByTagName('head')[0].appendChild(a)})();" onclick="alert('Drag this button to your browser\'s bookmarks toolbar or right-click it and choose Bookmark This Link.');return false;" title="Bookmarklet Title"><img src="http://example.com/img/bookmarklet.gif" align="absmiddle" alt="Bookmarklet Title" border="0" />
</a>

«href» в более читаемом виде:

javascript: (function () {
  var a = document.createElement('script');
  a.type = 'text/javascript';
  a.src = 'http://example.com/bookm.js?' + (new Date).getTime() / 1E5;
  document.getElementsByTagName('head')[0].appendChild(a)
})();

Здесь мы создаем элемент <script></script>, в качестве src используем наш букмарклет, и добавляем его к странице в <head></head>. Запись

(new Date).getTime() / 1E5

Позволяет всегда подключать актуальную версию букмарклета, избежав его кеширования. Если вы не собираетесь часто менять его содержимое, можете её убрать.

Такой букмарклет можно перетащить Drag-and-Drop'ом в тулбары в Chrome, Safari и FireFox. Для IE нужно будет в контекстном меню выбрать добавление на панель закладок.

2. Структура


Итак, рассмотрим содержимое нашего bookm.js. Вообще, можно привести два стиля написания букмарклетов:

2.1 Функциональный подход

(function () {
  try {
    // код букмарклета здесь
  } catch (e) {
    console.log(e.name + ": " + e.message);
  }  
}())

Основной плюс этого подхода — глобальная область видимости переменных остается абсолютно чистой, мы не засоряем её своими переменными. Но такой подход работает только для небольших, простых скриптов.

2.2 Объектно-ориентированный подход

function ourSuperBookmarklet() {
  // свойства класса, например this.a = '10';
}

ourSuperBookmarklet.prototype.someOtherFunction = function(abc) {
  this.a += abc;
}

ourSuperBookmarklet.prototype.start = function() {
  // код букмарклета здесь
  this.someOtherFunction(abc);  
}

try {
  var __osb = new ourSuperBookmarklet();
  __osb.start();
} catch (e) {
  console.log(e.name + ": " + e.message);
}

Получилось немного больше.
Итак, здесь мы описываем класс, его свойства и методы. Затем в try-catch конструкции создаем экземпляр этого класса и запускаем метод-загрузчик.
В этом случае в глобальную область видимости попадает ourSuperBookmarklet и __osb, однако нам это на самом деле и надо (об этом дальше).

Нужно никогда не забывать, что наш код инжектируется непосредственно в сайт, на который пользователь зашел, и наши ошибки могут сломать всю страницу. try-catch нам в этом немного поможет. Главное — не забудьте убрать все console.log'и из production версии.

3. Работа с экземпляром класса


Рассмотрим такой пример.

ourSuperBookmarklet.prototype.start = function() {
  var si = setInterval(function () {
    if(something) {
      clearInterval(si);
      this.a++;
    }
  }, 500);
}

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

ourSuperBookmarklet.prototype.start = function() {
  var th = this;
  var si = setInterval(function () {
    if(something) {
      clearInterval(si);
      th.a++;
    }
  }, 500);
}

И это будет работать. Однако создавать в каждом методе специальную переменную, так чтобы она была везде одинакова — больше похоже на костыль.
Более элегантное решение (имхо!) это обратиться к экземпляру класса, вот так:

ourSuperBookmarklet.prototype.start = function() {
  var si = setInterval(function () {
    if(something) {
      clearInterval(si);
      __osb.a++;
    }
  }, 500);
}

И в этом огромный плюс объектно-ориентированного подхода. Возможно этот пример не очень убедителен, но посмотрите вот сюда:

ourSuperBookmarklet.prototype.someOtherFunction = function(text, obj) {
  obj.innerHTML = text;
}

ourSuperBookmarklet.prototype.start = function() {
  var a = document.createElement('div');
  a.innerHTML = 'qwerty';
  a.setAttribute('ommouseover', '__osb.someOtherFunction(\'asdf\', this);')
  document.body.appendChild(a);  
}

Таким образом мы можем обращаться к методам и свойствам нашего класса из других скриптов/событий и т.д. Что есть хорошо.

4. AJAX


Вот здесь спешу вас огорчить. Ваши любимые $.post и $.get здесь работать не будут.
Дело в том, что на уровне браузера компоненту xmlHttpRequest запрещено обращаться к другому домену (это относится не ко всем браузерам, и где-то такое будет работать, есть желание — проверьте), подробнее en.wikipedia.org/wiki/Same_origin_policy. А так как наш код становится частью сайта, на который пользователь зашел, то обращение к нашему сайту и есть обращение к другому домену.
Все же, не xmlHttpRequest'ом единым. В свое время я сам пришел к нижеописанному способу, однако потом нашел, что этот метод достаточно популярен.

ourSuperBookmarklet.prototype.request = function(str) {
  var script = document.createElement('script');
  script.setAttribute('type', 'text/javascript');
  script.setAttribute('src', str);
  document.getElementsByTagName('head')[0].appendChild(script);
}

Все просто, и очень похоже на то, как мы подключали букмарклет (см. начало). Просто, если нам нужно просто передать данные. А если мы хотим получить callback? Здесь я использую 2 способа, и они оба связаны с JSON.

4.1 JSONP

Возьму пример из википедии. У нас есть объект

{"Name": "Cheeso", "Rank": 7}

И мы передаем его с помощью script'а (в нашем случае он создается динамически).

<script type="text/javascript" src="http://server2.example.com/getjson?jsonp=parseResponse"></script>

В итоге нам нужно передать

parseResponse({"Name": "Cheeso", "Rank": 7})

Где parseResponse — это функция callback'а в букмарклете, который будет обрабатывать переданный объект. В нашем случае это было бы что-то вроде

__osb.parseResponse({"Name": "Cheeso", "Rank": 7})

4.2 JSONP, но без P

Все хорошо, но как узнать, если мы не смогли запросить нужный ответ? Например, наш сайт лег. В таком случае я использую конструкцию:

var c = 0;
var ci = setInterval(function() {
  if(__osb.getInfo) {
    clearInterval(ci);
    // обработка объекта
  }

  if(c > __osb.timeout) {
    clearInterval(ci);
    // выводим сообщение об ошибке
  }
  c++;
}, 1000);

Здесь __osb.timeout — количество секунд, в течение которых мы будем ждать ответ от сервера. Вопрос — что такое __osb.getInfo?
Изначально (в конструкторе класса) мы проинициализировали getInfo как null. А ответ от сервера будет таким:

__osb.getInfo = {"Name": "Cheeso", "Rank": 7}

Поэтому, как только __osb.getInfo станет не null, мы закончим его ждать и начнем обработку ответа, иначе выведем сообщение об ошибке.

5. JSON


Раз уж зашла речь о JSON'е, хочется немного отойти от темы и сообщить приятную новость. В последних версиях популярных браузеров появился нативный объект window.JSON, который умеет кодировать/декодировать объекты/строки.
Для кодирования объекта в строку используется JSON.stringify, для обратного преобразования — JSON.parse. Нативный объект JSON быстрее выполняет эти преобразования, нежели наши решения, написанные вручную. Поэтому рекомндую проверять наличие объекта window.JSON, и только в случае его отсутствия пользоваться своими решениями для конвертирования.

Вместо заключения


В примерах я использовал только нативный JavaScript, однако вы можете подключить любимый фреймворк перед поключением самого букмарклета. Причины, по которым я обычно не пользуюсь фреймворками:
  • Скорость выполнения. Если у нас много объектов, по которым часто проходиться пробегать, то вопросы процессорного времени и памяти становятся на первое место. Чистый JavaScript всегда будет работать быстрее фреймворков. Хоть они и удобнее.
  • Надежность. Используя нативные функции для работы с DOM я гарантирую, что скрипт будет работать так, как хочу я. Вы можете возразить, что мне приходится писать много лишнего кода для поддержания кроссбраузерности. Очнитесь, люди! За все время написания букмарклетов помню одно серьезное отличие IE от остальных браузеров (работа с выделенным текстом) и пару мелких.
  • Независимость от CDN'ов. Для работы нужен только браузер пользователя и все. Если вы не используете CDN, то это серьезный аргумент, если используете — я немного побуду параноиком и скажу, что теоретически их тоже могут уронить/сломать и прочее…
  • Желание выучить нативный JavaScript и DOM. Для меня было важной причиной ;-)
Tags:
Hubs:
Total votes 65: ↑57 and ↓8+49
Comments17

Articles