Tag: JavaScript

Рецепт пуле-непробиваемого 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

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 ситуация несколько иная.

V8 & unicode RegExp-ы

Не так давно в V8 движок добавили поддержку флага /u (т.е. поддержку unicode) для регулярных выражений. В nodeJS можно подключить флагом –harmony_unicode_regexps. Вот небольшой обзор по новым возможностям. При установленном флаге . понимает иероглифы, их же стало можно использовать в описании регулярного выражения без экранирования, появилась поддержка i флага.

Но! Они недобавили самого вкусного ― \p{L}. При помощи модификатора \p можно кратко описать многие вещи (см. раздел Unicode Categories). К примеру можно описать в пару символом регулярку, которая будет проверять принадлежность символа к алфавиту какого-либо языка. Например, /^\p{L}[\p{L}\d-_ ]+$/u отлично подойдёт для валидации имени пользователя, не поставив в незавидное положение ни грузин, ни китайцев, ни русских. Но отлично отфильтрует рандомный бред с клавиатуры.

Надеюсь, что это временно. А пока можно воспользоваться, к примеру, XRegExp-ом.

Resize изображений на стороне браузера

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

Для начала нам потребуется <input type=“file”>. Без него нам не открыть диалог для выбора файла. Чтобы вызвать его вручную необходимо в обработчике вида onclick (onkeydown и пр. отпадают из-за Firefox) вызвать file.click(). Вызвать диалог в произвольный момент времени не получится.

Отслеживаем изменение выбора файла по onchange. Сам же файл достаём из file.files[index]. Если наш <input> работает не в multiple режиме, то index, соответственно, равен 0. Присмотревшись к свойствам этого объекта мы увидим его mime тип, размер, имя файла. Но не содержимое.

Для получения содержимого воспользуемся FileReader-ом.

const reader = new FileReader();
reader.onload = e => { e.target.result; /* base64 string */ };
reader.readAsDataURL(file);

Через него получаем DataURL вариант содержимого файла (асинхронно). Затем создаём новый Image, вешаем обработчик на onload, задаём этот dataURL ему как аттрибут src. В результате получаем валидный <img> тег с загруженным готовым к использованию изображением.

Resize изображения будем осуществлять за счёт <canvas>. У contextcanvas-а есть метод drawImage, который сделает за нас всю грязную работу. Ниже пример работы с ним (стояла задача вписать изображение в определённые рамки, но только если оно превышает эти сами рамки):

const canvas = document.createElement('canvas');

const w_ratio = img.width / width;
const h_ratio = img.height / height;
let ratio = Math.max(w_ratio, h_ratio);
if(ratio < 1)
	ratio = 1;

const w = Math.floor(img.width / ratio);
const h = Math.floor(img.height / ratio);

canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, w, h);
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, w, h);

return canvas.toDataURL('image/jpeg', 80);

Обратите внимание на то, что <canvas> даже не обязательно цеплять к DOM-у браузера, и тем более делать его видимым. В результате получаем снова dataURL, но уже правильного вида и размера. Но что нам теперь с ним делать? 

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

const fd = new FormData(form);
fd.append('image', blob);

Обратите внимание на blob. Дело в том, что для маскировки под обычный input[type=file] нам нужно нашу dataURL перевести в Blob вариант. Я взял готовое решение со stackoverflow:

function dataUriToBlob(dataURI)
{
	let byteString;
	if( dataURI.split(',')[0].indexOf('base64') >= 0 )
		byteString = atob(dataURI.split(',')[1]);
	else
		byteString = window.unescape(dataURI.split(',')[1]);

	// separate out the mime component
	const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];

	// write the bytes of the string to a typed array
	const ia = new Uint8Array(byteString.length);
	for(let i = 0; i < byteString.length; i ++)
		ia[i] = byteString.charCodeAt(i);

	return new Blob([ ia ], { type: mimeString });
}

Задача решена. Относительно без крови. Однако, мы воспользовались FormData, FileReader, Blob, Uint8Array,  Canvas. Тот же самый IE9 всего этого не умеет.

Об IE9 замолвите слово

Минутка гнева… Internet Explorer 9 не умеет:

  • Placeholder
  • FormDataFileReaderBlobUint8Array
  • Flexbox

Помимо этого обладает ужаасным набором инструментов, постоянно дохнет и очень медленно работает. В общем, этот браузер не так уж и далеко ушёл от своего 8-го собрата.