Букмарклет: разбор существенных моментов, часть первая

в 9:21, , рубрики: bookmarklets, bookmarks, content security policy, букмарклеты, закладки, заметки

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

Но почему в названии поста: часть первая? Потому, что современный букмарклет «с блэк джеком и шлюхами»* обычно состоит из нескольких взаимодействующих частей:

  1. первая часть букмарклета, которая является собственно букмарклетом это компактный javscript-код — не более 2000 символов, главная, но не единственная задача которого загрузить вторую часть;
  2. вторая часть букмарклета: это javscript-код произвольного размера, который выполняет всю оставшуюся работу;
  3. резервная часть букмараклета – которая запускается в действие, если вторая часть букмарклета не загрузилась.

И, как вы уже наверняка догадались, в данной публикации речь пойдет о первой части букмарклета,

Часть первая обычно выполняет следующие нехитрые действия:

  1. Определяет переменные, которые будут использоваться в букмарклете.
  2. Инициирует начало работы букмарклета или прекращает его работу с уборкой всего внедренного на чужую страничку в режиме вкл. / выкл., а также проверяет особые условия выполнения букмарклета.
  3. Подключает индикатор загрузки, чтобы пользователь не нервничал, пока все богатство функциональности продолжает загружаться.
  4. Подгружает вторую часть букмарклета которая обеспечивает выполнения всей дальнейшей работы.
  5. Если вторая часть букмарклета не может быть подгружена, получает данные на текущей странице, необходимые для передачи в резервную часть букмарклета
  6. Вызывает резервную часть букмарклета и передает ей необходимые данные.

Для краткости предмет публикации первую часть букмарклета далее будем просто называть: букмарклет.

Реальный пример

В качестве примера «из реальной жизни» воспользуемся букмарклетом веб-сервиса TheOnlyPage (сервис хранения закладок, заметок и html-фрагментов).

Чтобы установить букмарклет в ваш браузер достаточно перейти на страничку справочной системы TheOnlyPage и перетянуть соответствующую ссылку на панель закладок браузера.

К выполнению букмарклета можно перейти проделав следующие 4 шага:

Шаг 1: Кликнуть на ссылку букмарклета, если вы еще не входили в TheOnlyPage, то переходите к Шагу 2, если уже вошли, то сразу к заключительному Шагу 4.

Шаг 2: Нажать на кнопку Войти в TheOnlyPage в открывшейся форме.

image

Шаг 3: В результате, в отдельном окне отображается форма входа. Для быстрой регистрации / входа можно воспользоваться кнопками входа через социальные сервисы.

image

Шаг 4: Отображается форма сохранения закладки / заметки / html-фрагмента(картинки) получаемых с текущей просматриваемой страницы.

image

Tеперь пройдемся по javascript-коду букмарклета и увидим как все происходит.

Код букмарклета следующий:

javascript:(function(){var w=this,d=w.document,l=w.location,u=l.hostname,s=w.getSelection(),g=d.getElementById('theonlypageAjaxLoaderGif'),e=encodeURIComponent,i,r,c='';if(u==='www.theonlypage.com'){return void(0);}if(g){g.parentNode.removeChild(g);return void(0);}g=new Image();d.body.appendChild(g);g.id='theonlypageAjaxLoaderGif';g.style.cssText='position:fixed;z-index:2147483647';g.style.left=Math.floor((w.innerWidth-66)/2)+'px';g.style.top=Math.floor((w.innerHeight-66)/3)+'px';g.src='//d2wlh3lh0sssu9.cloudfront.net/img/ajax-loader.gif';r=d.createElement('script');r.src='//d2wlh3lh0sssu9.cloudfront.net/js/mini.bookmarklet.js';r.async=true;r.addEventListener('error',function(){if(s.rangeCount){c=d.createElement('div');for(i=0;i<s.rangeCount;i+=1){c.appendChild(s.getRangeAt(i).cloneContents());}c=c.innerHTML;}l.assign('http://www.theonlypage.com/b/?t='+e(d.title)+'&h='+e(l.href)+'&c='+e(c)+'&u='+e(u))},true);d.body.appendChild(r);})()
Тот же код в удобочитаемом виде

(function(){
  var w=this,
      d=w.document,
      l=w.location,
      u=l.hostname,
      s=w.getSelection(),
      g=d.getElementById('theonlypageAjaxLoaderGif'),
      e=encodeURIComponent,
      i,
      r,
      c='';
  if(u==='www.theonlypage.com'){
    return void(0);
  }
  if(g){
    g.parentNode.removeChild(g);return void(0);
  }
  g=new Image();
  d.body.appendChild(g);
  g.id='theonlypageAjaxLoaderGif';
  g.style.cssText='position:fixed;z-index:2147483647';
  g.style.left=Math.floor((w.innerWidth-66)/2)+'px';
  g.style.top=Math.floor((w.innerHeight-66)/3)+'px';
  g.src='//d2wlh3lh0sssu9.cloudfront.net/img/ajax-loader.gif';
  r=d.createElement('script');
  r.src='//d2wlh3lh0sssu9.cloudfront.net/js/mini.bookmarklet.js';
  r.async=true;
  r.addEventListener('error', function(){
    if(s.rangeCount){
      c=d.createElement('div');
      for(i=0;i<s.rangeCount;i+=1){
        c.appendChild(s.getRangeAt(i).cloneContents());
      }
      c=c.innerHTML;
    }
    l.assign('http://www.theonlypage.com/b/?t='+e(d.title)+'&h='+e(l.href)+'&c='+e(c)+'&u='+e(u))},true);
  d.body.appendChild(r);
})()

Важным моментом является недопущение конфликтов кода букмарклета с исполняемыми скриптами текущей страницы. Для этого используются стандартные подходы:

  • размещение всего кода внутри анонимной функции;
  • использование только локальных переменных, объявленных внутри этой функции, что исключает возможность конфликтов с внешней средой.

Можно было бы изолировать код и так:

// деклакрация функции
var bookmarlet_code = function() {
  //  здесь весь необходиый код 
}
// и выполнение функции
bookmarlet_code();

но при этом создается переменная bookmarlet_code, которая является потенциальным источником конфликта со скриптами текущей страницы. Чтобы не допустить возникновение глобальной переменной, декларацию и выполнение функции совмещают:

// декларация анонимной функции берется в круглые скобки и сразу выполняется 
( function() { 
  // здесь весь необходиый код 
})();

Определение переменных

Главное, при объявлении переменных, это экономия, надо постараться уложиться в отведенные 2000 символов, и для этого:

  1. все переменных объявляются в одном месте, одним ключевым словом var;
  2. имена переменных задаются одним символом;
  3. глобальным переменным, параметрам и функциям, которые будут неоднократно использоваться, следует присвоить односимвольные псевдонимы.

Разумеется, можно не заморачиваться с ужиманием кода вручную, а воспользоваться одной из множества доступных утилит по сжатию javascript-кода.

В нашем примере переменные объявляются следующим образом:

// ключевое слово var используется для всех переменных первый и последний раз
// глобальной переменной window присваиваем псевдоним w
// очень маленькая хитрость this в данном контексте эквивалентно widow
var  w=this, // для ясности лучше было бы w=window
// глобальному параметру window.document присваиваем псевдоним d
d=w.document, 
// глобальному параметру window.location присваиваем псевдоним l
l=w.location,  // адрес текущей страницы
// глобальному параметру window.location.hostname присваиваем псевдоним u
u=l.hostname,  // имя хоста текущей страницы
// объявляем переменную для хранение выделенной области и сразу определяем её значение
s=w.getSelection(),
// получаем, элемент с id='theonlypageAjaxLoaderGif', если он имеется на странице
g=d.getElementById('theonlypageAjaxLoaderGif'),
// неоднократно используемой стандартной функции encodeURIComponent задаем псевдоним e
e=encodeURIComponent, 
// объявляем служебную переменную i для использования в цикле for
i,
// объявляем переменную r для создания и загрузку на страницу второй части букмарклета
r,
// объявляем переменную с для передачи html кода выделенной области
c='';

Включение / выключение, проверка особых условий выполнения букмарклета

Возможно, букмарклет не должен работать на определенных страницах, например, букмарклет веб-сервиса TheOnlyPage не может работать на страничках собственного сайта www.theonlypage.com.

Эти ограничения реализуются проверкой и завершением работы, при выполнении условий завершения.
В нашем примере, если имя хоста текущего документа: www.theonlypage.com — букмарклет завершает работу, возвратом пустого значения: void(0).

if(u==='www.theonlypage.com'){
  return void(0); // возвращаем пустое значение
}

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

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

Механизм включения / выключения, в нашем примере реализуется следующим образом.

Если кликнули первый раз: внедряем в просматриваемый документ картинку-индикатор загрузки

Если кликнули второй раз: обнаруживаем уже внедренной картинку-индикатор загрузки, удаляем эту картинку и завершаем работу, возвратом пустого значения: void(0).

Картинку-индикатор загрузки мы определили в начале, при объявлении всех переменных

g=d.getElementById('theonlypageAjaxLoaderGif')

Если кликнули первый раз, картинки еще нет, g=undefined

Если второй раз, то переменная g содержит картинку и завершение работы происходит следующим образом:

if(g){
  // если картинка присутствует…
  // …удалить картинку
  g.parentNode.removeChild(g);
  // … завершить работу с возвратом пустого значения
  return void(0);
}

Подключение индикатора загрузки

Подключение индикатора загрузки image в нашем примере осуществляется следующим образом

// создаем новый элемент-изображение
g = new Image();
// устанавливаем параметры внедряемого изображения
// устанавливаем id
g.id='theonlypageAjaxLoaderGif'; 
// устанавливаем максимально возможный z-index
// чтобы обеспечить гарантированное отображение
g.style.cssText='position:fixed;z-index:2147483647';
// центрируем по горизонтали
g.style.left=Math.floor((w.innerWidth-66)/2)+'px';
g.style.top=Math.floor((w.innerHeight-66)/3)+'px';
// устанавливаем адрес картики
g.src='//d2wlh3lh0sssu9.cloudfront.net/img/ajax-loader.gif';
// внедряем элемент в тело документа
d.body.appendChild(g);

При создании индикатора загрузки мы присвоили ему id чтобы иметь возможность:

  1. в коде второй части букмарклета, после окончания загрузки, обнаружить индикатор загрузки и отключить;
  2. при повторном клике по ссылке букмарклета обнаружить индикатор загрузки и отключить.

Необходимо обязательно учитывать, что при внедрении на чужую страницу нового элемента, его id не должен совпасть с id какого либо элемента уже присутствующего на странице. Важным моментом является выбор такого id которое, почти наверняка никто кроме вас использовать не будет, например, содержащее название вашего сервиса. Так, в нашем примере для id используется строка 'theonlypageAjaxLoaderGif'

Подгрузка второй части букмарклета

Чтобы загрузить javascript-код второй части букмарклета, следует проделать действия подобные тем, что и при внедрении картинки-индикатора закгрузки.

// создаем новый элемент script
r=d.createElement('script');
// указываем адрес, по которому располагается код второй части букмарклета
r.src='//d2wlh3lh0sssu9.cloudfront.net/js/mini.bookmarklet.js';
// устанавливаем асинхронный режим загрузки скрипта…
// …чтобы наша загрузка не блокировала других процессов в браузере
r.async=true;
// и присоединяем вновь созданный скрипт к телу документа
d.body.appendChild(r);

Важным моментом является присоединение скрипта именно к телу (body), а не к заголовку (head) документа. Для HTML 5 заголовок не является обязательным атрибутом и возможны проблемы, если вместо document.body.appendChild использовать document.head.appendChild.

Резервное выполнение букмарклета

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

Помимо экзотических случаев, типа, просмотр pdf файла браузером Firefox, главной причиной возникновения ошибки загрузки скрипта является новый механизм обеспечения безопасности веб-страниц Content Security Policy.

Основное предназначение нового стандарта Content Security Policy является защита пользователя от кроссайтового выполнения скриптов. Полностью его поддерживают браузеры Firefox и Google Chrome.

Среди сайто-строителей это стандарт используется не слишком широко, для его реализации применяются специальные HTTP-заголовки, что выходит за пределы привычного круга обязанностей. Но некоторые продвинутые специалисты уже задействовали этот стандарт. Например, запрещают внедрение скриптов с чужих серверов сайты: www.facebook.com и GitHub.com.

Что не может не огорчать, а иногда и вызывать недоумение и справедливый гнев у создателя букмарклета. Конечно, можно посоветовать в качестве альтернативы браузер Opera, в котором этот стандарт еще не реализован, но требуется решение и для преданных пользователей браузеров Firefox и Google Chrome.

Стоит отметить, что если сайт оборудован средствами Content Security Policy то «блэк джек со шлюхами»* далеко не всегда получится замутить, но некий резервный вариант, с урезанными функциональностью и презентабельностью возможен.

Для наглядности, зайдем, например, на страничку www.facebook.com и кликнем по ссылке букмарклета TheOnlyPage. Букмарклет сработает, но не так, как обычно.

На этот раз форма букмарклета не отображается непосредственно над текущей страницей сайта, а вместо этого загружается новая страница по адресу:

http://www.theonlypage.com/b/?t=(3)%20Facebook&h=https%3A%2F%2Fwww.facebook.com%2F&c=&u=www.facebook.com

То есть попадаем на специальную страничку веб-сервиса TheOnlyPage, на которой отображается очень знакомая форма, для создания новой закладки, заметки или картинки.

image

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

  1. закодированная строка подписи: (3) Facebook:
    t=(3)%20Facebok
  2. закодированная строка адреса просматриваемой странички https://www.facebook.com/
    h=https%A%2F%2Fwww.facebookcom%2F
  3. html-код выделенного участка – ничего не выделено, никаких данных нет
    с=
  4. имя хоста, которому принадлежит страничка
    u=www.facebook.com

Мы убедились, что запасной вариант работает, осталось увидеть, как он запускается в работу в коде букмарклета.

Совершенно ясно, что резервный вариант букмарклета запускается на выполнение только, если не удалось присоединить код второй части букмарклета. Для того, чтобы перехватить ошибку загрузки скрипта, устанавливаем соответствующий обработчик события error.

r.addEventListener('error',  function(){
  // сначала вычисляем параметры которые нужно будет передать 
  // резервной части букмарклета
  if( s.rangeCount ){
    // если что-то было выделено – отрабатываем выделенные сегменты
    // чтобы получить их html-код
    c=d.createElement('div');
    for(i=0;i<s.rangeCount;i+=1){
      c.appendChild(s.getRangeAt(i).cloneContents());
    }
    c=c.innerHTML;
  }
  // загружаем в текущее окно браузера резервную часть букмарклета, 
  // закодировав и передав все необходимые параметы
  l.assign('http://www.theonlypage.com/b/?t='+e(d.title)+'&h='+e(l.href)+'&c='+e(c)+'&u='+e(u))
}, true);

И, в заключение, еще один важный момент, который нужно учитывать при написании кода букмарклета. При отображении кода букмарклета в html разметке, например, чтобы пользователь мог скопировать код представленный на странице и воспользоваться им для создание букмарклета не забывайте заменять символы:

  • < на &lt;
  • > на &gt;
  • " на &quot;

если такие символы встречаются в javascript-коде букмарклета.

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


«с блэк джеком и шлюхами»* (with blackjack and hookers) — фраза робота Бендера из второго эпизода первого сезона «Футурамы».

Автор: ValentynSolovyov

Источник

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


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js