Скованные одной цепью, или добавим комфорта комментариям Вконтакте


Скованные одной цепью

Обычным вечером заглянул в комментарии одного из сообществ Вконтакте и решился поучаствовать в дискуссии. Но не тут-то было! Чтобы прочесть „беседу“ нескольких ораторов потребовалось пролистать обсуждение и отсеять десятки лишних реплик, не участвовавших в нужном мне диалоге. Очевидная рутина, которую очень хочется спихнуть на механические мозги. Но инструмента, позволяющего вычленить только нужное у Вконтакта почему-то нет. „Что ж? За дело!“ — прокричал один из внутренних голосов, а остальные единогласно поддержали.

Так я начал пилить расширение для браузера Google Chrome?, позволяющее смотреть цепочки связанных комментариев в обсуждениях Вконтакта.

Кому неинтересен сюжет, могут сразу ознакомиться с результатом:


Продолжаем. Прочь, мелочи, вроде написания манифеста! Приступлю сразу к интересному.

Встраивание скрипта в код страницы


Цель — выводить цепочку комментариев по клику на иконку у комментария. Для этого нужно это иконку отрисовать и повесить на неё событие.

иконка
Так эта „иконка“ выглядит в итоге

Сразу стало ясно, что код должен работать «изнутри». Встроит его в страницу обычный для расширений контент-скрипт. Вот так он с этим справится:

var s = document.createElement("script");
s.src = chrome.extension.getURL('/js/inject.js');
(document.head || document.documentElement).appendChild(s);

Ещё встраиваемому коду потребуются некие статичные данные, вроде урлов фоновых изображений и id самого расширения. Их я пробрасываю через самописное событие, вызываемое в контент-скрипте после того, как встраиваемый скрипт подгрузился. Контент-скрипт целиком теперь выглядит следующим образом:

var s = document.createElement("script");
s.src = chrome.extension.getURL('/js/inject.js');
(document.head || document.documentElement).appendChild(s);
s.onload = function() {
    var evt = document.createEvent("CustomEvent");
    var url = chrome.extension.getURL('/img/planet.gif');
    app_uid = chrome.runtime.id;
    evt.initCustomEvent("BindDefData", true, true, { 'url': url, 'app_uid': app_uid });
    document.dispatchEvent(evt);
};

Это финальный его вид. А „слушается“ событие на стороне встраиваемого скрипта. Это довольно банально.

Общение между встраиваемым и фоновым скриптами


Получение комментариев, само собой, происходит через api Вконтакте (а именно через метод wall.getComments). Процесс их получения вовсе не требует, чтобы код был написан во встраиваемом в страницу скрипте. Он будет написан в так называемом «бэкграунд-скрипте». Напомню структуру. У нас есть:

  • контент-скрипт — пробрасывающий непосредственно на страницу встраиваемый код и статичную информацию
  • встраиваемый скрипт — код, отвечающий за логику отображения
  • бэкграунд-скрипт — код, работающий в фоне и ждущий команды от встраиваемого. Ходит по api к Вконтакту в гости за списком не отображённых комментариев

preloader
Во время прогулки до api рисуется прелоадер. Задержки обычно нет, но порой что-то сомневается в недрах Вконтакта и приходится ждать пару секунд

Сам запрос к api выполняется простенькой функцией, реализующей xhr:


var send_api_request = function(data, url, sync) {
    var req = new XMLHttpRequest();
    req.open('GET', url + dict_to_uri(data), sync);
    req.send();
    return req.responseText;
}

Гораздо интереснее бэкграунд принимает команды от встраиваемого скрипта. Для этого был написан «слушатель»:

chrome.extension.onMessageExternal.addListener(function(request, sender, call_back) {
    chain = create_chaine(request.ids_list); //построение цепочки комментариев
    call_back({ 'chain': chain[0], 'pid': request.ids_list[1], 'persons': chain[1] }); //возвращение полученных данных на сторону „клиента“
})

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

Со стороны клиента событие вызывается так:

chrome.runtime.sendMessage(app_uid, { 'ids_list': ids_list }, function(result) { ... })

В нём мы передаём список необходимых id-шников (id оратора, поста, комментария) и затем обрабатываем полученную цепочку комментариев. Процесс построения самого диалога банален, с ним можно ознакомиться в исходниках.

Отрисовка


Пришло время „видимой“ части расширения — отрисовки всплывающего окна.


Так выглядит короткий диалог сейчас

Я не стал изобретать велосипед и после минутного реверс-инжиниринга воспользовался классом MessageBox, который использует сам Вконтакт:


var draw_box = function(html) {
    var box = new MessageBox({
        title: false,
        width: 670,
        onHide: false,
        onDestroy: false,
        bodyStyle: 'background-color: rgba(79, 113, 152, 0.3); border-radius: 6px',
        hideButtons: true,
        grey: true,
        progress: true,
        hideOnBGClick: false
    });
    box.content(html);
    box.show();
    return box;
}

Html-шаблон, который вкладывается, был «содран» и немного переделан с первого попавшегося хипстерского сайтика. Он слишком неуклюжий, чтобы цитировать. А наполняется он с помощью чуть-чуть оттюнингованной вконтактовой функции rs:


var rs_t = function(html, repl) {
    each(repl, function(k, v) {
        if (k == 'text') {
            v = (typeof v === 'undefined' ? '' : v);
            v = v.replace(/(\r|\n)/g, ' <br /> '); // make newlines
            v = v.replace(/((http)?s?(\:\/\/)?((www)?\.?[a-zA-Z0-9]+\.[a-zA-Z]+\/?\S+))/g, '<a href="$1" target="_blank">$4</a>'); //make links
        };
        html = html.replace(new RegExp('%' + k + '%', 'g'), (typeof v === 'undefined' ? '' : v).toString().replace(/\$/g, '$'));
    });
    html = html.replace(/\[(id[0-9]+)\|([^\]+]+)\]/, '<a href="/$1">$2</a>'); // [id|name] -> <a href="/id">name</a>
    return html;
}

Т.е. в шаблоне подменяется конструкция вида %data% на значение переменной data. Этим занималась оригинальная функция. Я добавил ещё пару вещиц: генерацию ссылок на профиль из записей вида [id000000|Иван Иванов], генерацию ссылок из урлов в тексте сообщения и обработку символов конца строки (переноса).


Обрезанный скрин реального обсуждения

Полноценный. Аккуратно, габариты.

В целом, всё уже работает, но нет основного элемента — ручки, дёрнув за которую, мы увидим все эти чудеса. И тут опять не обошлось без толики реверс-инжиниринга. У Вконтакта есть класс Wall и метод класса _repliesLoaded. Он вызывается, когда комментарии подгрузились, о чём нам недвусмысленно намекает название. Я подменяю этот метод своим:

var cloned_function = Wall._repliesLoaded;
Wall._repliesLoaded = function(post, hl, replies, names, data) {
    cloned_function(post, hl, replies, names, data);
    wall_overloaded = true;
    setTimeout(replace_html, 300);
};

Т.е. технически вызываю его же, но сразу после запускаю функцию, которая отрисовывает в нужных свежеподгруженных комментариях иконку, кликнув по которой мы и увидим всплывающее окно с цепочкой комментариев.

Что в итоге?


Расширение работает. Умеет показывать цепочки комментариев с изображениями и ссылками. Умеет следить за появившимся контентом и добавлять кнопку цепочки, куда нужно.


Скрин цепочки комментариев со ссылками, изображениями и переносами строк.

Не умеет строить параллельные цепочки для случаев, когда обсуждение разветвляется и затем вновь «схлапывается» к итоговому комментарию. Скоро научится. В процессе подбора структуры данных.

Фреймворками не обмазано. Для кого-то оно плюс, для кого-то минус. Очень бы хотелось высоко вскинуть голову и гордо заявить: «И не нужны мне они вовсе!». Но нет. Всё прозаичней — почти не знаю местных фреймворков и батареек. JS — неосновной язык.

Ссылка на репозиторий с исходниками. Буду рад форкам и пулл-реквестам.

P.S.: Я не стал оплачивать себе аккаунт разработчика в гугле. Мне он ни к чему. Если кто желает, может смело выставлять расширение в chrome web store. Если ещё и упомянет автора, то будет отлично.

UPD: Отличные новости! Расширение стало доступно каждому! MadDAD выложил его в chrome store — ссылка.
Ура, товарищи! MadDAD, спасибо большое. Я безгранично признателен.
-->


К сожалению, не доступен сервер mySQL