misc/class
lib/jquery_pnotify, lib/moment, lib/lodash, misc/notification, site/engine, misc/social
if( $.browser.msie && $.browser.version <= 8 ) include('lib/respond'); $._social.__cfg = {"init":[{"service":"basic"},{"fb_app_id":"1997094873850041","service":"fb"},{"vk_app_id":"2978320","service":"vk"},{"service":"twi"}],"like":[{"service":"fb"},{"service":"vk"},{"via":"","channel":"","hash_tag":"","service":"twi"}]}; window._SiteEngine = new classes.SiteEngine( { user_id: 0, controller: 'content_tape', action: 'tag', content_css_version: '1459538664', social_enabled: 0} );

Faiwer

Блог web-программиста

Поиск по метке: JavaScript

React 16 & Context decorators

13 апреля 2018

С обновлением React до 16 версии мы получили новый вид контекста, на замену старому. Теперь это не мутное API, как было раньше, а утверждённый вариант, с которым можно смело в бой. В чём же ключевые различия?

  • Старый вариант был многословным. Новый вариант компактнее.
  • Старый вариант не обновлял связанные с контекстом компоненты при изменении контекста. По сути контекст должен был быть имутабельным. Новый вариант обновляет связанные с ним компоненты.
  • Старый вариант можно было использовать в любом методе компонента. Новый только в render-е.
  • Старый вариант не имел пенальти по производительности и не требовал использования HOC-ов, не увеличивал иерархию древа. Новый вариант всё это делает. Скажем для connect-а из redux-а потребуется аж 2 дополнительных уровня иерархии.
  • Старый вариант был оформлен в виде static полей класса, новый вариант в виде function as tag content. И то и другое слегка уродливо. Или не слегка. JSX в целом страшненький. А тут...

В итоге послевкусие специфическое. С одной стороны API хотя бы устаканили. С другой стороны сделали его каким-то очень неудобным и вообще кривоватым. Такое впечатление возникает в целом, глядя на новые возможности из 16+. Но тут я остановлюсь на работе с контекстом.

Писать вот так:

render()
{
	return <Consumer>{arg1 =>
		<Consumer2>{arg2 =>
			<div>/* some code with them */</div>
		}</Consumer2>
	}</Consumer>
}

… невыносимо. Хочется удобства. В итоге это вылилось в такой вариант:

@renderContext('arg1', 'arg2')
render(arg1, arg2)
{
	return <div>/* some code with them */</div>;
}

По сути я воспользовался декораторами из ES7, которые пока под некоторым вопросом. Но т.к. JSX в стандарте тоже нет, то плевать. Что делает данный декоратор? Он оборачивает метод render обёртками из кода выше. Но делает это автоматически.  А в исходный render метод передаёт все контексты как аргументы функции в том же порядке.

Никакой магии в этом нет и все эти обёртки над обёртками никуда не испарились, но теперь хотя бы скрыты с глаз долой. Помимо прочего я написал ещё несколько декораторов. Один для <Provider/>-ов, один для HOC, один для удобтва i18n. Чувствую в декораторы затянет меня с головой. Ух злая эта штука. Это не к добру :)

Сам декоратор устроен вот так:

const renderContext = (...keys) =>
function(target, methodName, descriptor)
{
	const render = descriptor.value;
	descriptor.value = keys
		.map(key => list[key].Consumer)
		.reverse()
		.reduce((inner, Consumer) =>
		{
			return function(...args)
			{
				return <Consumer>
					{arg => inner.call(this, ...args, arg)}
				</Consumer>;
			};
		}, render);
	return descriptor;
};

Забирайте кому надо ;)

onMouseMove & movementX & movementY

16 ноября 2017

Ковыряясь с доступными в Event полями я наткнулся на два незнакомых, которые ныне есть во всех современных браузерах. А именно: movementX & movementY. Показывают сдвиг мыши относительно предыдущего вызова callback-а. Чертовски удобно, т.к. с ними можно не считать эти сдвиги вручную. Эх, ещё бы выдали штатную возможность узнать положение мыши относительно заданного DOMElement-а без груды кода с offsetTop-ми и прочими костылями.

Тернистый путь расширений в Chrome

2 ноября 2017

Довольно часто я сталкиваюсь с тем, что сторонние сайты работают не совсем так, как я бы хотел. И будучи web-программистом я пытаюсь на это дело как-то повлиять. Но одно дело, скажем, стили сайту подправить, и совсем другое вмешаться в его работу JavaScript-ом. Эта заметка о втором.

Итак. На этот раз жертвой пал puzzle-english. Ресурс для изучения английского. Среди прочего там есть возможно уникальная возможность тренировать аудирование на небольших заранее заготовленных фразах, записанных от нескольких дикторов. Предлагается на выбор по 3-5 слов на каждое произнесённое слово. Итого ты или кликаешь по кнопкам мышью, собирая фразу, или с клавиатуры клавишами цифр. Всё бы ничего, если бы не отсутствие поддержки чисел с num-панели. Поддерживаются только те, что над буквами. Что, лично для меня, страшно неудобно. Ну что ж, приступим.

Для начала я решил узнать как оно там вообще работает. Посмотрел все eventListener-ы на document и нашёл нужный методом перебора при debug-е. Он слушает keydown и несколько неуклюже обрабатывает event.which, поддерживая только один диапазон для чисел. Поиграл с ним и понял, что при "патченном" which num-цифры работают прекрасно. Написал небольшой скрипт в консоли примерно следующего толку и решил что дело в шляпе:

document.addEventListener('keydown',  evt =>
{
	if(evt.which > 96 && evt.which < 105)
	{
		const which = evt.which - 48;

		$.event.trigger(
		{
			type: 'keydown',
			which,
			originalEvent: { target: document.body }
		});
	}
});

Забыл упомянуть, puzzle-english агрессивно использует jQuery. Чем я не применул воспользоваться. Там ведь свои обёртки и свои рецепты. Обернул этот код расширением и... Обломался. Chrome не позволяет использовать со страницы жертвы ничего кроме DOM. Т.е. доступа к jQuery нет, доступа к любым кастомным window-полям тоже нет. Ничего нет. 

Ну не беда, подумал я и решил организовать keydown средствами браузера. Что у нас там есть? document.createEvent и initKeyboardEvent. Поковырялся — работает плохо. Да ещё и deprecated. Ок, а как ещё можно? Ага, у нас есть класс KeyboadEvent. Ok:

new KeyboardEvent('keydown', { which: 50 });

Ага. Нифига. В документации одно, в браузере другое. Плевал он на which и прочие поля. Что пишут нам интернеты? Ух, дичь какая... Зато работает:

window.evt = evt = document.createEvent("KeyboardEvent");
evt.initKeyboardEvent('keydown',
	true, false, null, 0, false,
	0, false, which, 0);
evt.__defineGetter__('which', () => which);
document.dispatchEvent(evt);

Правда только из консоли. Почему? Потому что браузер заменяет отправленные из расширения события их копиями в основной тред. В целях безопасности. И того мы снова теряем which.

Что в итоге? В итоге вспомнил, что там ещё мышью можно было кликать. Стало быть можно эмулировать клик. Просто вызвав у DOMElement-а метод click. С этим уже граблей не было.

В общем мораль такова: расширения скованы цепями. Попытки сделать что-то просто — могут упереться в открытые баги или просто запреты. Так и живём ;)

Простейший способ выгрузить все свои слова с lingualeo

14 мая 2017

Рецепт не для программистов, но относительно опытных пользователей ПК:

  1. Открываем раздел Dictionary (Словарь) на lingualeo
  2. Проматываем словарь до упора вниз, чтобы сайт отобразил все слова (он их лениво подгружает)
  3. Открываем консоль разработчика (F12 или Ctrl+Shift+I)
  4. Выбираем в открывшейся панели таб "Console"
  5. Вставляем туда следующую команду: copy(Array.from(document.querySelectorAll('.dict-item-word')).map(el => [el.getAttribute('data-word-value'), el.querySelector('.translates').innerText]))
  6. Открываем этот или любой подобный сайт (который умеет конвертировать JSON в нужный вам формат)
  7. Вставляем в текстовую область через Ctrl+V (Cmd+V) текст из буфера обмена (в п.5 туда были помещены все слова в формате JSON)
  8. Кликаем по ссылке "Download the entire CSV" и сохраняем получившийся файл куда вам нужно (можно пересохранить его в нужный формат, поменять кодировку и пр. (по умолчанию UTF8)).

В случае если разработчики поменяют вёрстку ― п.5 поломается :-) Увы

Написание тестов для React с использованием enzyme

29 апреля 2017

Сразу отмечу, что статья не претендует на истину. Это всего лишь мои заметки о том, как вообще можно написать и запустить простые тесты для React, т.к. тут оказалось много места где заблудиться\окопаться. Итак, начнём.

Babel

Раз React значит и JSX, а значит и babel. Но простой запуск mocha просто запускает nodejs, в котором никакого JSX нет (и наверное никогда не будет). Что делать? Поиск подсказывает, что нужно запускать mocha (далее речь идёт именно о mocha) с флагом --compilers js:babel-core/register. Что сие есть? Это флаг заставит mocha предварительно компилировать код используя babel-core/register, который грубо изнасилуетподменяет require() в nodejs, что позволяет ему предварительно изуродоскомпилировать кодовую базу в нормальный javascript

Import\Export и .babelrc

Ок, но как babel-ю подсказать, что использовать в качестве конфига? Точного внятного ответа пока дать не могу, но как минимум можно создать в корне json-файл с именем .babelrc, который будет содержать json-конфиг. Мне прокатило, но в более сложных ситуациях нужно будет искать вариант поделикатнее. Собственно, чтобы не дублировать конфиг, имеет смысл подключить его и в webpack.config.js. Сразу отмечу, что взять и распарсить .babelrc как json простым вызовом require() nodejs почему-то не смогла (видимо мешает отсутствие расширения .json), поэтому fs.readFileSync и JSON.parse вам в помощь. 

Полифилы, JSDom и прочая нецензурщина

nodeJS это nodeJS, а не браузер, а для ряда тестов потребуется имитация браузера. И похоже, что на данный момент изящных решений нет, т.к. кодовая база react-* грубо сверяется с глобальным наличием window & document. Мде. Помимо прочего всякие полифилы, вроде regenerator-а (ну или отключить транспайлинг async-await в генераторы ещё до этапа компиляции) нужны. 

Для решения вопроса с полифилами достаточно import 'babel-polyfill'. а вот с JSDom так просто не срастётся. Я воспользовался таким решением:

  • import 'jsdom-global/register';
  • принудительно зафиксировал jsdom на 8-й версии (а не 10-й)

Замечу, что жизненноважно подключать этот костыль с JSDom до подключения React, иначе груды неадекватных глупых ошибок без внятных описаний украсят вашу безмятежную жизнь. Суть в том, что в недрах разных react-либ window кешируется ещё на этапе подключения файлов. И в последствии какой-нибудь флаг вида canUseDom === false будет вам яростно мешаться.

Можно подключать эти либы в каждом тест-файле, но тогда вы столкнётесь с тем, что при запуске сразу пачки тестов, babel-polifil будет падать со словами "я уже подключён", jsdom тоже будет что-нибудь базлать. Разумная альтернатива вынести обе эти строки в некий setup.js, а потом его подключать флагом --require tests/setup.js.

Сами тесты

React поставляет в пакете react-dom ряд инструментов для тестирования. Но даже на официальной странице этих инструментов упоминается, что вам стоит воспользоваться некой либой под названием enzyme. Установка оказалась не совсем тривиальной, т.к. сильно зависит от версии вашего React-а. Очень внимательно прочитайте инструкции по установке. Заодно доставьте к нему что-нибудь вроде chai для удобства тестирования.

Отмечу, что enzyme предоставляет по сути 3 набора утилит для работы с вашими React компонентами. А именно shallow для быстрого и поверхностного рендеринга, render для работы уже с html, и mount для полноценной работы с DOM и life-cycle React-компонентов. В любых маломальски сложных ситуациях готовьтесь к танцам с бубнами. Например .simulate метод для вызова событий умеет только React-события, а повешанные через addEventListener уже нет. Готовьтесь к нефинормативным ошибкам, куда же без них. 

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