Букмарклеты или как скачать аудиофайлы с сайта vkontakte?
Сегодня я собираюсь рассказать про одну интересную технологию, как мне кажется, с большим будущим. Называется эта технология bookmarklets(букмарклеты).
Букмарклет – это маленькая JavaScript-программа, оформленная в виде javascript-ссылки и вызываемая как браузерная закладка. Итак, мы знаем, что в атрибуте href гипертекстовой ссылки можно указать url любой существующей страницы, обычно url этот выглядит примерно так <a href=’http://address.org/path/index.html’>ссылка</a>, в данном случае http: – это указание протокола обмена, а все остальное – указание сервера, пути к файлу, и имени файла, к которому обратится браузер после клика на ссылке. Так вот, существует ряд протоколов, таких как, например, http: (протокол гипертекстового обмена), ftp:(протокол обмена файлами), file:(протокол загрузки локальных файлов пользователя).
Помимо этого существует еще один интересный протокол javascript:, который мы и будем использовать для создания букмарклетов. Выглядит это примерно так:
<a href=’javascript:alert(‘Hello, world’);’>букмарклет</a> при щелчке по этой ссылке, выполнится команда alert(‘Hello, world’); Проверьте, как это работает.
Попробуем теперь перетащить эту ссылку в панель закладок браузера, или щелкнув на ней правой кнопкой добавить ее в закладки. После того, как ссылка сохранена в закладки, можно в любой момент выбрать ее и выполнить.
Попробуем немного усложнить нашу закладку, пусть тепреь она выводит адрес документа из которого она вызвана. Вот этот букмарклет:
<a href=’javascript:alert(document.location);’>Адрес текущей страницы</a>.
Добавьте эту ссылку (Адрес текущей страницы) в закладки и вызовите ее, открыв предварительно в браузере какую-нибудь интернет-страницу, например сайт Яндекса. Вы увидите всплывающее оповещение с указанием адреса текущей страницы. Из этого можно сделать один очень важный вывод: букмарклет, вызванный из браузера, как закладка, выполняется в контексте текущей, открытой в браузере страницы. А этот факт дает нам неограниченный доступ к изменению данных на странице, и к извлечению из страницы любых данных, вплоть до полной замены содержимого этой страницы.
К сожалению, длина скрипта, сохраняемого в закладке ограничена. Причем, для разных браузеров она разная. Не углубляясь в различия между браузерами, можно сказать, что длина эта ограничена для различных браузеров от 488 до 2084 символов, что не может не огорчить. В принципе, для несложных букмарклетов нам хватит и этой длины, но что делать, если нам хочется большего? Что если мы хотим написать скрипт, который производит достаточно сложную обработку загруженной страницы, и в 2 кБ скрипта нам никак не уложиться?
Для этого можно использовать способ, заключающийся в следующем: сам букмарклет представляет собой небольшой скрипт, у которого есть лишь одно предназначение – загрузить основной скрипт и запустить его на выполнение.
Созданием этого скрипта мы сейчас и займемся. Но для начала замечу вот что. Какой бы скрипт мы ни создавали, практически всегда в нем мы будем объявлять создание новых функций и новых переменных. А создаваться они будут в контексте загруженной пользователем страницы, где до нас уже могут существовать какие-либо переменные и функции. Понятно, что мы никак не можем предугадать, какие имена будут у этих переменных, а значит, создавая свои собственные, мы можем случайно переопределить уже существующие переменные, внеся тем самым непредсказуемые изменения в логику работы пользовательских скриптов. Таким образом, запустив букмарклет, помимо его полезной работы, мы можем получить и совершенно непредсказуемые побочные явления, связанные с перопределением переменных.
Самый удобный способ избавиться от этих эффектов – применить анонимную функцию. Анонимные функции определяются также как и именованные, за исключением того факта, что им не присваивается имя.
Простое объявление функции:
function fun2(x) { return x + 1; }
Объявление функции, как переменной:
var fun2 = function(x) { return x + 1; }
К обеим этим функциям мы можем обратиться одинаково:
alert(fun1(5));
alert(fun2(5));
Мы получим абсолютно идентичное поведение. Такая же анонимная функция объявляется так:
function(x) { alert(x); }
Однако, если ее просто объявить, то толку от нее не будет никакого. Чтобы от любой функции была польза – ее надо вызвать. Вызов анонимной функции всегда совмещен с ее объявлением, для этого полностью все объявление функции берется в круглые скобки, после которого также в круглых скобках приписываются все ее аргументы вызова:
(function(x) { alert(x); })(5);
А для функции без аргументов – вот так:
(function() { alert('Я функция!'); })();
К чему мы городили весь этот огород? А к тому, что новая область видимости переменных в JavaScript возникает только в одном единственном случае – при объявлении функции. Таким образом, если в функции объявить переменную, то ее имя не перекроется с таким же именем, но объявленным в глобальном контексте. Поясню на примере:
var a = 5; (function(){ var a = 10; alert(a); // alert(10) })(); alert(a); // alert(5)
Как видим второй вызов alert дал значение 5, а не 10, потому что переменная a = 10 Объявлена в другом контексте видимости. В дальнейшем при создании букмарклетов мы всегда будем использовать анонимные функции – они обеспечивают гарантию от побочных явлений за счет добавления всего 17 лишних символов скрипта (function(){})(); а весь код встраивается между фигурными скобками.
А вот теперь пора переходить к анонсированному чуть ранее загрузчику скриптов. Делать это мы будем вот так:
var script = document.createElement('script'); script.src = 'http://script_url.js'; script.type = 'text/javascript'; document.getElementsByTagName('head')[0].appendChild(script);
Как видим здесь нет ничего сложного: при помощи createElement создается элемент SCRIPT, этому элементу задаются необходимые параметры src и type (в src указывается полный url загружаемого скрипта), и, наконец, скрипт при помощи appendChild добавляется в конец раздела HEAD страницы. После этого он тут же начинает загружаться с сервера, и сразу после загрузки – выполняется.
Замечу сразу, что текст скрипта всего букмарклета должен быть собран в единую строку без переносов, чтобы он выглядел как ссылка и его можно было добавить в закладки.
Покажем это на простом примере. Ниже приведен простой букмарклет, загружающий с сервера скрипт и выполняющий его:
<a href=”javascript:(function(){var script=document.createElement(‘script’); script.src=’http://easy-4-web.ru/samples/bookmarklet/3_loaded/script.js’; document.getElementsByTagName(‘head’)[0].appendChild(script)})();”>Загрузить букмарклет</a>
А вот и сам готовый букмарклет: Загрузить букмарклет просто перетащите его в закладки и запустите. Он загружает с адреса http://easy-4-web.ru/samples/bookmarklet/3_loaded/script.js скрипт в котором – очень простое содержимое:
(function(){ alert('Hello, world!'); })();
Да, после запуска букмарклета, мы увидим все то же сообщение “Hello, world!”.
Перестанем, наконец, писать бесполезные букмарклеты и сделаем хоть что-нибудь полезное, чтобы оценить всю мощь и красоту букмарклетов.
Я собираюсь сделать несложный букмарклет, который позволит мне легко и удобно сохранять аудиозаписи с сайта vkontakte.ru. Покопавшись немного в исходниках HTML-страниц сайта, я обнаружил, что при нажатии кнопки PLAY любой из аудиозаписей, происходит вызов функции operate, в которую в качестве параметров передаетсся либо url mp3-файла аудиозаписи, либо несколько параметров, указывающих на имя сервера медиаконтента, ID пользователя и ID файла аудиозаписи из которых путем конкатенации легко составить полный url аудиофайла.
Таким образом, моя идея заключается в следующем: после запуска букмарклет ищет на странице все кнопки PLAY (обычно они имеют class=’playimg’ или class=’play’), затем рядом с каждой такой кнопкой создает ссылку на аудиофайл, url к которому скрипт извлекает из атрибута onClick кнопки PLAY путем несложных текстовых преобразований.
Как видим, здесь напрямую придется работать с DOM-структурой, а мне очень не хотелось бы писать на низкоуровневом JavaScript все эти поиски элементов, извлечение из них атрибутов и создание новых элементов в существующем дереве DOM. Я собираюсь использовать для этого фреймворк jQuery.
Подгрузку jQuery я собирался делать стандартным способом при помощи createElement и appendChild, однако столкнулся вот с какой трудностью – после команды appendChild скрипт начинает загружаться, и нет никаких гарантий по времени его загрузки, в любом случае если сразу после appendChild попытаться обратиться к объекту jQuery, то выскочит ошибка “jQuery not defined“. Возник вопрос, как отследить момент, когда jQuery уже загрузился и можно начинать с ним работать?
Я решил обойтись собственным рукотворным решение, которое заключается в следующем: сразу после appendChild я запускаю цикл отслеживания, который каждые 0,1 сек проверяет, существование объекта jQuery, и как только объект появляется, загрузчик прекращает цикл проверки и вызывает функцию обратного вызова, в теле которой и будет происходить вся обработка страницы.
Также мой загрузчик скриптов перед тем как загружать какой-либо скрипт, проверит, а существует ли уже в DOM-структуре script с совпадающим атрибутом src? И если такой скрипт уже есть то загрузка нового произведена не будет.
Чтобы не утомлять читателя подробным описание его работы, я просто приведу готовый скрипт загрузчика, оснащенный необходимыми комментариями:
var easybml = { // загружает скрипт loadJS: function(url) { var scripts = document.getElementsByTagName('script'), alreadyLoaded = false; for(var i=0; i<scripts.length; i++) { if(scripts[i].src == url) { alreadyLoaded = true; break; } } if(!alreadyLoaded) { var script = document.createElement('script'); script.src = url; script.type = 'text/javascript'; document.getElementsByTagName('head')[0].appendChild(script); } }, // Загружает jQuery с заданного ресурса, после инициализации jQuery вызывает // событие complete loadJQuery: function(src, complete) { easybml.loadJS(src); // если передана функция обратного вызова, то сохраняем указатель на нее // и запускаем циклическую проверку инициализации jQuery if(complete) { easybml.prv.complete = complete; easybml.prv.checkJQuery(); } }, // секция закрытых данных, к этим методам обращаться не надо prv: { complete: null, // циклически проверяет загружен ли jQuery, и когда он загружен // вызывает функцию easybml.prv.complete checkJQuery: function() { if (typeof jQuery == 'undefined') { setTimeout('easybml.prv.checkJQuery()', 100); } else { easybml.prv.complete(); } } } }; (function(){ // загружаем jQuery, если он еще не загружен if (typeof jQuery == 'undefined') { easybml.loadJQuery('http://easy-4-web.ru/samples/bookmarklet/5_jquery/jquery.php', function() { // после загрузки и инициализации jQuery становятся доступными все // методы. Присоединяем к документу обработчик события onclick $(document).ready(function() { $("body").click(function() { alert("Hello world!"); }); }); }); } })();
Функция easybml.loadJS(url) загружает скрипт с адреса url, если скрипт с таким же src еще не был загружен.
Функция easybml.loadJQuery(src, complete) загружает скрипт фреймворка jQuery с адреса src, и после его успешной загрузки и инициализации вызывает функцию обратного вызова complete.
Как видим в данном примере вся полезная функциональность скрипта заключена в том, что он к элементу BODY документа присоединяет обработчик события OnClick, который при клике на странице выводит alert(‘Hello, world!’).
И наконец, остался последний шаг. Вот она – полная версия скрипта, позволяющего загрузить аудиозаписи с сайта vkontakte.ru:
var easybml = { // загружает скрипт loadJS: function(url) { var scripts = document.getElementsByTagName('script'), alreadyLoaded = false; for(var i=0; i<scripts.length; i++) { if(scripts[i].src == url) { alreadyLoaded = true; break; } } if(!alreadyLoaded) { var script = document.createElement('script'); script.src = url; script.type = 'text/javascript'; document.getElementsByTagName('head')[0].appendChild(script); } }, // Загружает jQuery с заданного ресурса, // после инициализации jQuery вызывает событие complete loadJQuery: function(src, complete) { easybml.loadJS(src); // если передана функция обратного вызова, то сохраняем указатель //на нее и запускаем циклическую проверку инициализации jQuery if(complete) { easybml.prv.complete = complete; easybml.prv.checkJQuery(); } }, // секция закрытых данных, к этим методам обращаться не надо prv: { complete: null, // циклически проверяет загружен ли jQuery, и когда он загружен // вызывает функцию easybml.prv.complete checkJQuery: function() { if (typeof jQuery == 'undefined') { setTimeout('easybml.prv.checkJQuery()', 100); } else { easybml.prv.complete(); } } } }; (function(){ function clearQuotes(str) { return jQuery.trim(str).replace(/\'|\"/g,''); } function getMP3url(attr) { var parts = attr.split(","); if(parts.length == 3) { return clearQuotes(parts[1]); } else if(parts.length == 5) { return 'http://cs' + clearQuotes(parts[1]) + '.vkontakte.ru/u' + clearQuotes(parts[2]) + '/audio/' + clearQuotes(parts[3]) + '.mp3'; } else if(parts.length == 2) { return parts[0]; } return parts[3]; } // загружаем jQuery, если он еще не загружен if (typeof jQuery == 'undefined') { easybml.loadJQuery('http://easy-4-web.ru/samples/bookmarklet/ 5_jquery/jquery-1.4.4.min.js', function() { $(document).ready(function() { $(".playimg").each(function(n, el) { var container = $(el).parent(); container.width('auto'); var clickAttr = $(el).attr("onclick").toString(); container.append(("<a href='" + getMP3url(clickAttr) + "' border='0' target='_blank'><img src='http://easy-4-web.ru/samples/ bookmarklet/6_saveaudio/saveicon.png' border='0'></a>")); }); $(".play").each(function(n, el) { var container = $(el).parent(); container.width('auto'); var valueAttr = container.find('input').attr('value').toString(); container.append(("<a href='" + getMP3url(valueAttr) + "' border='0' target='_blank'><img src='http://easy-4-web.ru/samples/ bookmarklet/6_saveaudio/saveicon.png' border='0'></a>")); }); }); }); } })();
Здесь используется ряд вспомогательных функций:
clearQuotes(str) – Очищает строку str от обрамляющих пробелов и скобок,
getMP3url(attr) – Извлекает из атрибута attr собственно сам url аудиофайла,
Ну и наконец после объявления этих функций начинается непосредственно полезная работа скрипта – поиск на странице кнопок PLAY и добавление к ним ссылок на скачивание.
Полную версию букмарклета я разместил по адресу http://easyapi.ru/bml/vksaveaudio/.
Пользуйтесь на здоровье.
UPD 14.04.2011.
Сайт vkontakte изменил способ рендеринга страниц, содержащих проигрыватель audio-файлов. Они унифицировали отображение контейнера проигрывателя и теперь не требуется дополнительных плясок с бубном для различного рода страниц – теперь аудио-проигрыватель везде рендерится абсолютно одинаково, что дает возможность упростить скрипт. Ниже я приведу только текст основной функции скрипта, так как изменился только он. Скрипт стал короче, из него убраны все ветвления оператором if:
(function(){ function getMP3url(attr) { var parts = attr.split(","); return parts[0]; } if (typeof jQuery == 'undefined') { easybml.loadJQuery('http://easyapi.ru/bml/vksaveaudio/jquery-1.4.4.min.js', function() { $(document).ready(function() { $(".play_new").each(function(n, el) { var container = $(el).parent().parent(); container.width('auto'); var valueAttr = container.find('input'). attr('value').toString(); container.append(("<a href='" + getMP3url(valueAttr) + "' border='0' target='_blank'> <img src='http://easyapi.ru/bml/vksaveaudio/saveicon.png' border='0'></a>")); }); }); }); } })();
В тему:
Очень полезная статья. Спасибо :=) Видно, что Вы хороший программист. А такой букмарклет можете сделать, чтобы возле списка песен показывался размер файлов, а не только ссылка? Кажется надо XMLHttpRequest использовать, только у меня ничего не получилось, хотя особенно и не старался. Очень хорошо и полезно было бы, если бы и такой букмарклет описали в статье, как и этот.
akkadites, December 26, 2010 7:03 pmНасколько я понимаю проблему(есть вероятность, что я не прав), JavaScript по причине высокой планки защиты пользовательских данных, не позволяет делать очень многие вещи, в том числе определение размеров загружаемых файлов. Это что касается попыток определить размер файла который пользователь загружает НА сервер. В данном случае ситуация обратная – файл лежит на сервере, и нам нужно перед его загрузкой определить его размер. Размер файла всегда передается в HTTP-заголовках (Content-Length) сразу после начала передачи данных с сервера на клиентскую машину. То есть, запросив файл у сервера, можно прочитать заголовки и определить размер файла, но при этом сразу вслед за нужными нам заголовками сервер отправит нам и содержимое аудиофайла, и я не знаю способов сообщить серверу, что нам это не нужно, сказать ему “Хватит, довольно!”, и сервер не успокоится, пока не передаст нам весь файл. Это касается только JS. Пока не тестировал, но знаю, что на FLash есть возможность также запросить файл и прочитав заголовки, дать серверу команду cancel(), отменив загрузку данных. Недостатком такого подхода является требование к наличию в браузере клиента поддержки Flash, а он есть не у каждого. Есть еще один способ – создание серверного кода, например на PHP, который по ссылке на файл будет запрашивать файл, получать его заголовки и прекращать загрузку, отдав запрашивающему размер файла. Я на досуге протестирую оба этих метода и выложу в блог результаты исследовательской работы.
Вячеслав Гринин, December 27, 2010 11:04 amВячеслав, спасибо Вам за объяснение. Но с этим что “и сервер не успокоится, пока не передаст нам весь файл.” я не согласен. Я пробовал запускать такую функцию:
function flsz(fn)
{
var xmlhttp = new ActiveXObject(“MSXML2.XMLHTTP.3.0”);
xmlhttp.open(“HEAD”, fn, false);
xmlhttp.send();
return(xmlhttp.getResponseHeader(“Content-Length”));
и она работала: отдавался размер без передачи всего файла, или я чего-то напутал, уже давно было. может и ошибаюсь. проблема возникла в другом. из букмарклета она не работала, а только на странице с кнопкой её вызывающей.
а если можно на Flash построить, то тоже не плохо было бы. Вы ведь adobe flash plugin подразумеваете?
}
Еще не разобрался со всеми подробностями, но Ваш код не выполняется в браузерах кроме IE(что логично – там нет объекта MSXML2.XMLHTTP.3.0), а вот в самом IE я получил мертвое зависание браузера, пока не разобрался в причинах.
Вячеслав Гринин, January 16, 2011 7:37 pm