IT — Обновление oAuth Facebook-а на 2.6 версию
В августе сего года заканчивается поддержка GraphAPI 2.0 версии. Facebook уведомляет об этом прямо в своих стандартных уведомлениях соц. сети заголовком %app has a new Developer Alert, за которым следует текст:
%app has been making recent API calls to Graph API v2.0, which will reach the end of the 2-year deprecation window on Monday, August 8, 2016. Please migrate all calls to v2.1 or higher in order to avoid potential broken experiences.
Что именно, касательно OAuth, изменилось? А вот что:
- В graph.facebook.com/… следует добавить v2.6:
- https://graph.facebook.com/v2.6/oauth/access_token
- https://graph.facebook.com/v2.6/%uid%/picture
- etc…
- /oauth/access_token теперь возвращает не queryString, а нормальный JSON
IT — Ненависти к Facebook-у пост
Лет 5 назад я впервые подключал к сайту Facebook SDK, для правильного внедрения like-кнопки и блока с рожицами. По сути было 3 варианта:
- Разместить ссылку с картинкой, которая бы вела на facebook-страницу для share-а или like-а. Самый быстрый вариант, даже, можно сказать, молниеносный, т.к. не нужно ничего грузить. Но такими кнопками пользуются куда реже, чем если они размещены правильно. Т.е. если нужен не факт наличия кнопки, а результат от её использования, то такой вариант отпадает.
- Внедрение через XFBML-разметку. Дело не хитрое. Размести специальный тег куда требуется. Подключи скрипт. Есть варианты. Настройки задаются в виде аттрибутов тегов.
- Примерно тоже самое но сразу через <iframe/>.
Казалось бы дело совсем не хитрое. Но в процессе эксплуатации выяснилось много неприятных подробностей. Дело в том, что like-кнопка после нажатия показывает специальный popup. И этот popup в vk-like и fb-like реализованы по разному. В vk-like панелька показывается нормально вне зависимости от вёрстки, а fb-like панелька сделана ногами и вызывает кучу проблем, если у вас выше по коду установлен какой-нибудь overflow: hidden, или ещё какая-нибудь особенность вёрстки. Помимо прочего, большое значение имеет то, куда вы эту поганую кнопку разместили. Если это нижний правый угол сайта, то попав вылезет не вверх, как стоило бы ожидать, а вниз. И, либо не поместится, либо разопрёт сайт как попало. Настройки "распирайся вверх", разумеется, нет. Честно говоря возможности этих стандартных контролов просто никакие.
Помимо внешнего вида, проблемы вызывает ещё то, что соц. сети грузят вашу страницу crawler-ом, для добычи мета-тегов, со специфическим набором http-заголовков. И если ваш сайт в такой ситуации падает (ну мало ли какой баг), то без debug-страницы вы долго будете гадать, в чём же блин дело.
После удачной загрузки страница кешируется. В случае VK навсегда (если верить новому API, то уже не совсем), а в случае facebook, кеш страницы всё таки можно сбросить. Есть специальный инструмент, который умеет показывать ошибки выгрузки мета-данных. К тому же можно вызывать его программно (к примеру при замене og:image).
В очередной раз я столкнулся с facebook like кнопкой сегодня, когда узнал, что они не работают в мобильной вёрстке. Дело оказалось не в этом… Думал поправлю за 5 минут, и в итоге убил весь вечер. Слишком много изменений, порой сильно не очевидных. К примеру:
- Теперь like и share кнопки — это 2 разные кнопки,
- Но share-кнопку можно паровозиком прицепить к like-кнопке. Выглядит забавно.
- SDK обновился до 2-ой версии. И она не поддерживает решения из 1-й.
- При этом share кнопка из 2-ой версии работает в 1-ой.
- Но только на desktop-ах. А на мобильниках она просто ставит для <body/> класс со стилями { overflow: hidden; position: relative } и молча, без ошибок, дохнет. Скролл естественно ломается. Счастливого дебага, суки... да? :-)
- Раньше для отображения картинок при like-ах и share-ах достаточно было og:image разметки. А теперь… Она всё та же, но появились ограничения по мин. размеру — 200х200. Портал для которого я это домкратил, для всех своих тысяч постов использует картинки меньшего размера. Спасибо, facebook… гори в аду.
- Facebook поменял своё отношение к некоторым аттрибутам к <html/>. Вплоть до ругани красным из-за какой-нибудь ерунды.
- Усердно ругается, если используются аттрибуты name вместо property в <meta/>-тегах.
Подлатал, поправил, переделал. Теперь вроде всё работает. Полагаю, что спустя пол-года они снова в одностороннем порядке что-нибудь изменят и всё сломается. В который раз подряд. Причём, наверное, вёрстка like-кнопки будет и через пару лет сильно кривиться в зависимости от вёрстки сайта, на котором она размещена. Догадаться разместить её в <body/> похоже слишком сложно.
Одновременно с этим на половину сломалась подгрузка изображений vkontakte-ом при share-ах. Причина не ясна. Судя по документации, ему должно хватать og:image. Ограничений по нему не указано. Debug-страницы для просмотра ошибок, похоже, до сих пор нет. Поиск результатов не дал. Спасибо, vKontakte.
IT — Рецепт пуле-непробиваемого toJSON
Многие разработчики не раз за свою практику сталкивались с тем, что стандартная возможность упаковать объект в JSON падает с ошибкой, или, что хуже, уходит в вечный цикл. Причина тому цикличные ссылки. Пример:
const a = {};
const b = { a };
a.b = b;
JSON.stringify(a);
Получаем закономерный Uncaught TypeError: Converting circular structure to JSON. Но, что если вам, к примеру, для нужд debug-а, потребовался полунепробиваемый метод toJSON?
Хорошая новость! Мы можем его написать самостоятельно. Причём, благодаря наличию таких классов, как Map или Set, без особых ухищрений и извращений. Принцип действия алгоритма достаточно прост: всякий раз, когда мы собираемся упаковать очередной объект (а только они передаются по ссылке, и приводят к цикличности), мы должны проверить, а не упаковывали ли мы его ранее. А т.к. Set и Map в качестве ключа в Map и значения в Set понимают всё, что угодно, включая объекты, не приводя их к строковому виду, то и переложить эту работёнку можно и нужно именно на них.
Circular
Т.е. где-то внутри нашего toJSON будет:
if(map.has(obj))
return `$circular:${map.get(obj)}`;
map.set(obj, path);
Где path это путь к текущему итерируемому элементу. Таким образом, столкнувшись с объектом повторно, мы, вместо этого объекта, упакуем строку вида $circular:a.b.c. Что будет очень удобным подспорьем при разборе сложных багов.
Element-ы
Не будет лишним проверять — является ли объект DOMElement-ом, Document-ом или чем-нибудь подобным, т.к. эти сущности не просты внутри, но при этом, едва ли представляют для вас интерес:
if(
obj instanceof RegExp ||
obj instanceof Date ||
obj instanceof Element ||
obj instanceof Document
)
return obj.toString();
Получим строку вида [object HTMLDivElement].
jQuery
В случае, если вы используете jQuery, то стоит все порождённые ею jQuery-массивы превратить в обычные:
if(obj instanceof $) // jQuery-nodes
return _.map(obj, (v, idx) => { /* toJS */ });
В моём случае типовым результатом будет — ["[object HTMLDivElement]", "[object HTMLDivElement]"]. Но можно и усложнить схему, конвертируя DOM-ноды в их CSS-path (например: #section.cls[attr="value"]).
NaN & Null
Скорее всего, внутри своего toJSON вы будете использовать typeof obj === 'object'. Напоминаю о засаде — typeof null === 'object'; Не забудьте это учесть.
Про NaN — JSON.stringify(NaN) === "null". Не самый лучший вариант, не правда ли?
if(_.isNaN(obj))
return '$NaN$';
Дело в том, что JSON не умеет никаких NaN. Но для нужд дебага такие моменты могут быть критичными. Тоже самое и для undefined.
IT — Загрузка больших текстовых файлов из JS-окружения
Текстовый (и не только) файл можно слепить на ходу, используя client-side JS, и отдать юзеру на загрузку. Обычно для этого используют что-то вроде:
var $a = $('<a/>',
{
'href': 'data:' + mime + ';charset=utf-8,' + encodeURIComponent(data),
'download': _.isString(fname) ? fname : 'file.txt'
})
.appendTo(document.body);
$a[0].click();
$a.remove();
Т.е. формируется dataURI с указанием кодировки. А её содержимое экранируется за счёт encodeURI. В случае, если файл превышает некий предел, то начинаются проблемы в Chrome, который не позволяет скачать содержимое ругаясь на ошибку сети. Полагаю, что сбоит или encodeURIComponent, или же длина аттрибута href превышает некую норму. Firefox такими проблемами не страдает — 100+ MiB грузит, в то время как Chrome спотыкается уже на 15 MiB.
Можно попробовать паковать содержимое как base64, оставив суть подхода (dataURI + a[href]) неизменной, а можно воспользоваться Blob-ом.
Blob
Упаковываем строку в Blob-объект:
new Blob([str], { encoding: 'UTF-8', type: mime });
Получаем псевдо-ссылку на этот Blob-ресурс:
const link = (window.URL || window.webkitURL).createObjectURL(blob);
Ссылка будет начинаться с blob://. Подставляем её в [href] и дело в шляпе.
В случае, если загруженный файл содержит какую-то чушь, сбита кодировка, или же вам не удалось получить Blob-ресурс из строки, попробуйте воспользоваться ручной конвертацией в Blob, с использованием обычных или типизированных массивов. Примеры кода легко найти в сети. Да прибудет с вами сила!
IT — Broadcast в PM2
PM2 — это обёртка вокруг node-cluster для запуска nodeJS приложения, в виде нескольких instance's, каждый из которых представлен отдельным процессом. При этом, в стандартном применении, в качестве master-а выступает внутренний скрипт пакета pm2, а в качестве worker-ов ваше приложение. Они всё так же могут слушать один и тот же порт. PM2 (а может и не он) сам будет распределять запросы между worker-ами.
Рано или поздно может возникнуть необходимость коммуникации между worker-ами. Как её осуществить? Изначально worker-ы друг о друге ничего не знают, но если подключить require('pm2'), то через полученное API можно добиться многого. Нормальной документации к этому API, похоже не существует. Но большая часть доступных методов общая с CLI API.
Для начала необходимо подключиться к pm2 из worker-а:
const pm2 = require('pm2');
pm.conect(err =>
{
// с этого момента можно использовать API
});
Для получения всех запущенных pm2-процессов (кроме master-а) можно воспользоваться pm2.list(). Он вернёт вам ВСЕ запущенные им процессы, включая те, что никак не связаны с вашим приложением. Включая текущий instance тоже. Возвращённая пачка данных изобилует подробностями. 3-10 KiB всякой ерунды на каждый worker.
Отфильтровать оттуда текущий instance можно сравнив pmID текущего процесса (process.env.pm_id), с pmID каждого worker-а из списка. Для того чтобы выфильтровать лишние процессы, не связанные с вашим приложением, можно воспользоваться name-ом (process.env.pm_id). Это значение name из json-pm2-профиля вашего приложения.
Метод list несколько избыточен, мягко говоря. Можно получить только pmID-ки worker-ов по указанному name-у. Для этого есть pm2.getProcessIdByName(name). Останется только выфильтровать из него pmID текущего процесса.
А для того, чтобы разослать каждому из worker-ов сообщение с данными, есть метод pm2.sendDataToProcessId(id, packet). Где packet должен быть объектом с ключами: topic (строка с ключом сообщения) и data (сами данные).
Получать сообщения можно как штатными методами pm2, через launchBus (что у меня сделать не получилось), так и штатным process.on('message', cb) от nodejs.
В итоге простой broadcast можно организовать примерно так:
// при инициализации приложения
pm.connect(err => {});
// отправка сообщения
function broadcast(topic, data)
{
pm.getProcessIdByName(process.env.name, (err, ids) =>
{
if(err) {}
else ids
.filter(i => i != process.env.pm_id)
.forEach(id =>
{
pm2.sendDataToProcessId(id, { topic, data }, err => {});
});
});
}
// приём
process.on('message', msg => { /* msg */ });
Ложка дёгтя
На getProcessIdByName на моей машине уходит от 6ms до 15ms. Т.е. очень очень много. Код внутри по любому поводу формирует и гоняет между процессами груду данных. Используются теже механизмы, что и задействуются для pm2 monit. Даже если запрашивался только ID.
Почему нет никаких стандартных и удобных механизмом для коммуникации между процессами мне не ясно. Причём ни в pm2, ни в nodeJS. С nodeJS впринципе всё ясно, ведь использование node-cluster предполагает, что master всему голова и сам всё порешает. Но в случае pm2 ситуация несколько иная.