Отсуствие дилеммы использования внешних библиотек npm, когда ты мидл+

Timofey Aksenov

Timofey Aksenov

Front End Developer

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

Эта история началась с того, что мне необходимо было настроить функциональность достижения целей и передавать их в АПИ Яндекс метрики

Ок, приступаю к реализации... т.е. к гуглению. Быстро нахожу библиотеку react-yandex-metrika, имплементирую ее в проект и довольно потирая руки начинаю проверять.

И тут fail. А причина очень простая, яндекс метрика в проект поступает через gooole tag и используя библиотеку react-yandex-metrika я сталкиваюсь с проблемой, что у меня происходит дублирование функциональности, т.к. яндекс метрика инициализируется дважды.

Не унываю, начинаю изучать проблему, проверяю код из документации яндекс метрики, смотрю куда сохранается ее замыкание. И в результате: удаляю react-yandex-metrika из проекта, а вся ее реализация заменяется тремя строчками кода.

Как стало:

const handleDoSomethingYMWrapper = React.useCallback(() => {
const ymClosure = (...args: any[]) => null;
const ym = window?.ym ?? ymClosure;
ym(YANDEX_METRIKA_ID, 'reachGoal', TARGET_NAME);
handleDoSomething();
});

Чтобы котик начал давить крыс, ему нужно в детсве давать сырую вырезку оленины для включения реликтовых триггеров хищника 🐱

Каналы в JS - это когда "тебе" не только кнопочки "рисовать"

Timofey Aksenov

Timofey Aksenov

Front End Developer

Работал я как-то свою работу на работе: "рисовал кнопочки на реакте" и вдруг бах! Тимлид мне говорит:

Тимофейчик, нам нужно срочно в проект закатить поддержку web sockets с возможностью одновременного подключения нескольких разных web socket серверов и с возможностью подключаться к множеству различных каналов внутри web socket сервера, а так же нужно как-то активными каналами управлять, gracefull stopping им, независимо друг от друга, делать и следить за их количеством и т.д. и т.п. И да, вот мы тут развернули сервак на технологии centrifuge. Сделай красиво, в общем.

Делать нечего, нужно сделать.

Когда я учился на курсах по Golang, а да если вы не в курсе, то я прошел курсы на степике и успешно их сдал на максимальный балл.

Разработка веб-сервисов на Go - основы языка / Результат 100%

Разработка веб-сервисов на Golang, часть 2 / Результат 100%

В общем на этих курсах изучая, как работают горутины и пишутся микросервисы, я узнал про каналы, успешно реализовал их в ДЗ и запомнил, что каналы вещь нужная и полезная.

И вот в процессе первоначального мозгового штурма, я вспомнил, что в документации к redux-saga читал про eventChannels и там был пример создания канала для web sockets на основе socket.io client. О да, если бы не курсы по гошечке, то я бы эту часть документации пропустил и пошел дальше экшенами в стор пуляться... ой "рисовать кнопочки на реакте" конечно же.

Изучив пример из документации redux-saga, я понял, что eventChannels это то что мне нужно. Но нужно доработать свой код, чтобы учесть технологические требования:

  1. Подключение к различным физическим серверам web sockets (оборачивание каждого в отдельный канал)
  2. Подключение к различным каналам внутри одного физического сервера web sockets (оборачивание каждого в отдельный канал)
  3. Независимый запуск каналов
  4. Независимая остановка каналов
  5. Возможность провести инвентаризацию активных каналов

Реализация задуманного:

В файле chanSaga.ts я создал 5 функций, которые обладают следущей функциональностью:

/**
* Создает канал событий для указанного подключения к сокет серверу
*
* @param {{ data: any; socket: any }} { data, socket }
* @returns
*/
export function createSocketChannel({
data,
socket,
outerCallback,
}: {
data: any;
socket: any;
outerCallback: (params: any) => void;
}) {
const { token, channel } = data;
return eventChannel((emit: any) => {
socket.setToken(token);
// NB: Обратите внимание на строку кода `outerCallback({ message, emit });` будет бонус
const publishHandlerFunction = (message: any) => {
outerCallback({ message, emit });
};
const subscribeErrorHandlerFunction = (errorEvent: any) => {
emit(new Error(errorEvent.reason));
};
const subscription = socket.subscribe(channel);
subscription.on('publish', publishHandlerFunction);
subscription.on('error', subscribeErrorHandlerFunction);
socket.connect();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
socket.on('disconnect', (_context: any) => {
emit(END);
});
// unsubscribe
return () => {
socket.disconnect();
};
});
}
/**
* Получает инициализационные данные о сокет сервере из АПИ,
* для последующего создания соединения с сокет сервером
*
* @export
* @param {IPayload} payload
* @returns {(Generator<CallEffect<Promise<AxiosResponse<any>> | null>, IInitData, IRes>)}
*/
export function* initSocketConnection(
payload: IPayload,
): Generator<CallEffect<Promise<AxiosResponse<any>> | null>, IInitData, IRes> {
const { userToken: token, path } = payload;
const socketInitData: IRes = yield call(socketAPI.postDataInitSocket, {
token,
path,
});
const { data } = socketInitData;
const contractStatus = socketInitDataContract.decode(data);
if (E.isLeft(contractStatus)) {
console.log('Socket init: contract error, path:', path);
return { status: InitStatusEnum.ERROR };
}
return { status: InitStatusEnum.SUCCESS, data };
}
/**
* Создать подключение к сокет серверу.
*
* Клиент сокет сервера: Centrifuge
*
* @export
* @param {IData} data
* @returns
*/
export function createSocketConnection(data: IData) {
const { socket } = data;
const { proto, host, port } = socket;
const suffix = environment.WEB_SOCKET_SUFFIX;
const centrifuge = new Centrifuge(`${proto}${host}:${port}${suffix}`);
return centrifuge;
}
/**
* Отключиться от сокет сервера, а так же закрыть канал `eventChannel`
*
* @export
* @param {string} socketChanName
*/
export function* terminateSocketChan(socketChanName: string) {
const scktSelector = yield select(socketSelector);
const centrifuge =
get(scktSelector, `${socketChanName}`, 'NO_CHAN') || 'NO_CHAN';
if (centrifuge === 'NO_CHAN') {
return null;
}
centrifuge.disconnect();
const tmpScktData = omit(scktSelector, `${socketChanName}`);
yield put({
type: socketSaveSocket.toString(),
payload: {
...tmpScktData,
},
});
}
/**
* Вотчер.
*
* Следит за каналом принимая сообщения от сокет сервера или команду останова.
*
* Прерывается через вызов функции terminateSocketChan из других саг:
* `yield call(terminateSocketChan, `${channelName}`);`
*
* @export
* @param {} chanName
*/
export function* watchSocketChan(chanName: any) {
while (true) {
try {
const action = yield take(chanName);
yield put(action);
yield fork(terminateSocketChan, '');
} catch (err) {
console.log('socket error:', err);
chanName.close();
}
}
}

Практическая реализация задуманного:

После того, как я реализовал всю необходимую инфраструктуру в файле chanSaga.ts, теперь я могу в любом месте системы делать вот так:

// Выберем нужный сокет канал
const scktSelector: any = yield select(socketSelector);
// Прервем его, если он вдруг остался запущен,
yield call(terminateSocketChan, SUPPORT_MESSAGES_CHAN);
yield put({
type: socketSaveSocket.toString(),
payload: {},
});
// Создадим сокет
const socket = yield call(createSocketConnection, data);
// Создадим канал для сокета
// NB: Обратите внимание на строку кода `outerCallback: chatListSupportSocketCallback,` будет бонус
const channel = yield call(createSocketChannel, {
data,
socket,
outerCallback: chatListSupportSocketCallback,
});
yield put({
type: socketSaveSocket.toString(),
payload: {
...scktSelector,
[SUPPORT_MESSAGES_CHAN]: socket,
},
});
// Запустим вотчер, принимающий сообщения в канал от внешнего web socket сервера
yield call(watchSocketChan, channel);

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

Бонус про зубодробительный пример с замыканием:

/**
* Коллбек, который помещается в event канал с сокетами,
* где при событии от сокет сервера `publish`
* "вытягивает" emit (эмиттер) в замыкание.
* Эмиттер (emit) это аргумент коллбека из функции `eventChannel` пакета redux-saga:
*
* `eventChannel((emit: any) => {...}`
*
* Эмиттер нужен, чтобы потом "дернуть" экшен и передать в его `payload`
* данные полученные от сокет сервера
*
* @param {IParams} params
* @param message any;
* @param emit (input: any) => void;
*/
export const socketCallback = (params: IParams) => {
const { message } = params;
const { data } = message;
checkContract({
contract: soketServerMsgContract,
data,
direction: DIRECTION.B_F,
});
params.emit({
type: socketSupportClearBeforeRes.toString(),
});
params.emit({
type: chatListSupportResMsgFromSocket.toString(),
payload: data,
});
};

Котики любят каналы, все любят каналы 🐱

Когда функциональное программирование это хорошо, правильно и нужно

Timofey Aksenov

Timofey Aksenov

Front End Developer

Совсем недавно я закончил писать код очень большого модуля со складским учетом готовой продукции.

В процессе написания, мне очень хорошо запомнился один момент, а именно: установка бейджика со статусом для коробки, паллеты, партии с готовой продукцией. Один и тот же объект в бизнес-логике складского учета может иметь различные статусы, которые отображаются в таблице с итоговыми выборками. С бекенда я получал только статусы в виде строк IN_PARTY и номера этих самых партий, которые преобразовывал в человекочитаемый вид. Например: В коробке, В партии, В партии: №2, В паллете, В паллете: №6.

Код первоначальной версии:

const {
...
logisticStatus,
partyNumber = -1,
} = item;
...
const statusForHuman = getLogisticStatus(logisticStatus);
const storeTwoPalletStatusNumber =
logisticStatus === 'IN_PARTY' ? partyNumber : -1;
...
return (
...
<Table.Cell>
{statusForHuman}
{storeTwoPalletStatusNumber !== -1
? `: №${storeTwoPalletStatusNumber}`
: null}
</Table.Cell>
...
)

Когда я прототипировал и готовил первоначальную версию кода, меня совершенно не смутила вот эта часть кода:

<Table.Cell>
{statusForHuman}
{storeTwoPalletStatusNumber !== -1
? `: №${storeTwoPalletStatusNumber}`
: null}
</Table.Cell>

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

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

И о чудо! Решение нашлось в самом неожиданном месте: на досуге я смотрел доклад по ЯП elixir и обратил внимание на оператор пайпа в данном языке |>. Это натолкнуло меня на мысль, что мне для решения моей проблемы с размазанной по шаблонам логикой поможет функциональное программирование.

Сказано, сделано. Я открыл гайд по ФП в js , Lodash FP Guide и быстро выяснил, что для решения моей проблемы подойдет lodash метод _.flow([funcs]), который по сути реализует пайп или выполнение функций слева на право

Код итоговой версии:

...
const {
...
logisticStatus,
partyNumber = -1,
} = item;
// Один раз передаем данные на вход потоку функций
const statusForHuman = setPalletStatusForHuman(
logisticStatus,
partyNumber,
);
...
return (
...
{/* Пользуемся удобными бейджиками во множестве мест компонента */}
<Table.Cell>{statusForHuman}</Table.Cell>
<Table.Cell>{statusForHuman}</Table.Cell>
<Table.Cell>{statusForHuman}</Table.Cell>
...
)

Код реализации хелперов, устанавливающих человекочитаемые статусы

import flow from 'lodash/flow';
/**
* Интерфейс с описанием типов статуса
*
* @interface IStatus
*/
interface IStatus {
logisticStatus: string;
correctNumber?: number;
}
/**
* Установка номера паллеты или номера партии для полного статуса
*
* @param {string} logisticStatus
* @param {number} palletNumber
* @param {number} partyNumber
* @returns {object} {
* {string} logisticStatus,
* {number | undefined | -1} correctNumber,
* }
*/
const storeTwoPackStatusNumber: (s: string, n: number, p: number) => IStatus = (
logisticStatus: string,
palletNumber: number,
partyNumber: number,
) => {
return logisticStatus === 'IN_PALLET'
? { logisticStatus, correctNumber: palletNumber }
: { logisticStatus, correctNumber: partyNumber };
};
/**
* Установка номера паллеты для полного статуса
*
* @param {string} logisticStatus
* @param {number} partyNumber
* @returns{object} {
* {string} logisticStatus,
* {number | undefined | -1} correctNumber,
* }
*/
const storeTwoPalletStatusNumber: (s: string, p: number) => IStatus = (
logisticStatus: string,
partyNumber: number,
) => {
return logisticStatus === 'IN_PARTY'
? { logisticStatus, correctNumber: partyNumber }
: { logisticStatus, correctNumber: -1 };
};
/**
* Преобразование кода статуса, полученного с бекенда в человекочитаемый статус
*
* @param {*} {
* logisticStatus,
* correctNumber,
* }
* @returns {object} {
* {string} logisticStatus,
* {number | undefined | -1} correctNumber,
* }
*/
const getHumanLogisticStatus: (data: IStatus) => IStatus = ({
logisticStatus,
correctNumber,
}) => {
let statusForHuman = logisticStatus;
let temp = -1;
if (correctNumber) {
temp = correctNumber;
}
switch (logisticStatus) {
case 'DISASSEMBLED':
statusForHuman = 'Разобрано';
break;
case 'ASSEMBLED':
statusForHuman = 'Собрано';
break;
case 'IN_PALLET':
statusForHuman = 'В паллете';
break;
case 'IN_PARTY':
statusForHuman = 'В партии';
break;
case 'SHIPPING':
statusForHuman = 'Отгружено';
break;
default:
statusForHuman = logisticStatus;
break;
}
return { logisticStatus: statusForHuman, correctNumber: temp };
};
/**
* Получение статуса в человекочитаемом формате, с указанием номера (при необходимости)
*
* @param {*} {
* logisticStatus,
* correctNumber,
* }
* @returns {string} statusForHuman
*/
const getLogisticStatus: (data: IStatus) => string = ({
logisticStatus,
correctNumber,
}) => {
let statusForHuman = logisticStatus;
switch (logisticStatus) {
case 'В паллете':
statusForHuman = `В паллете${
correctNumber !== -1 ? `: №${correctNumber}` : ''
}`;
break;
case 'В партии':
statusForHuman = `В партии${
correctNumber !== -1 ? `: №${correctNumber}` : ''
}`;
break;
default:
statusForHuman = logisticStatus;
break;
}
return statusForHuman;
};
/**
* Pipe для установки отформатированного статуса паллеты в компоненте
*
* @param {string} logisticStatus
* @param {number} partyNumber
*
* @returns {string} statusForHuman
*/
export const setPalletStatusForHuman = flow([
storeTwoPalletStatusNumber,
getHumanLogisticStatus,
getLogisticStatus,
]);
/**
* Pipe для установки отформатированного статуса коробки в компоненте
*
* @param {string} logisticStatus
* @param {number} palletNumber
* @param {number} partyNumber
*
* @returns {string} statusForHuman
*/
export const setPackStatusForHuman = flow([
storeTwoPackStatusNumber,
getHumanLogisticStatus,
getLogisticStatus,
]);
/**
* Pipe для установки отформатированного статуса для общего случая в компоненте
*
* @param {*} {
* {string} logisticStatus - код статуса с бекенда,
* {number | undefined | -1} correctNumber - номер объекта,
* }
*
* @returns {string} statusForHuman
*/
export const setCommonStatusForHuman = flow([
getHumanLogisticStatus,
getLogisticStatus,
]);

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

И программисты рады, и котики рады! 🐱

Непростой выбор для простого решения

Timofey Aksenov

Timofey Aksenov

Front End Developer

Изучаю постепенно основы-основы в информатике. И сразу же натыкаюсь на Алгоритм Евклида wiki.

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

Результат коллективной работы меня в какой-то степени восхитил, озадачил и одновременно принес сладкое чувство удовлетворения от того, что я понимаю суть происходящего.

А теперь к сути, вернее к коду

Вариант 1. Код первоначальной версии:

/**
* @param {number} a
* @param {number} b
* @return {number|null}
*/
export default function euclideanAlgorithm(a, b) {
if (a === 0 && b === 0) {
return null;
}
if (a === 0 && b !== 0) {
return b;
}
if (a !== 0 && b === 0) {
return a;
}
if (a > b) {
return euclideanAlgorithm(a - b, b);
}
return euclideanAlgorithm(b - a, a);
}

Вариант 2. Код итоговой версии:

/**
* Recursive version of Euclidean Algorithm of finding greatest common divisor (GCD).
* @param {number} originalA
* @param {number} originalB
* @return {number}
*/
export default function euclideanAlgorithm(originalA, originalB) {
// Make input numbers positive.
const a = Math.abs(originalA);
const b = Math.abs(originalB);
// To make algorithm work faster instead of subtracting one number from the other
// we may use modulo operation.
return b === 0 ? a : euclideanAlgorithm(b, a % b);
}

Вариант 3. Еще раз код итогой версии без лишних комментариев

/**
* Recursive version of Euclidean Algorithm of finding greatest common divisor (GCD).
* @param {number} originalA
* @param {number} originalB
* @return {number}
*/
export default function euclideanAlgorithm(originalA, originalB) {
const a = Math.abs(originalA);
const b = Math.abs(originalB);
return b === 0 ? a : euclideanAlgorithm(b, a % b);
}

Вроде бы великолепный результат, вместо 15 строк кода, мы получаем 5 строк. Но с уменьшением количества строк мы увеличиваем сложность.

Первый вариант кода можно читать без предварительной математической подготовки (имеется в виду, что хватит беглого просмотра статьи из википедии). Второй вариант, потребует вдумчивого чтения комментариев и соответсвующей более детальной математической подготовки. С третьим вариантом, как говорится "без пол литра не разобраться".

Внимание!
Личное, ни на что не претендующее, мнение автора.

Разработчик (абсолютно любого уровня), должен понимать свою ответсвенность перед другими разработчиками, коллегами, бизнесом и многими, многими, многими... Оставив вариант №3, вы нанесете удар всем остальным людям. Будте готовы, что вас будут ненавидеть и вспоминать плохими словами, после того как вы наиграетесь и пойдете писать сложные вещи, туда где вас понимают или сыграет свою роль BUS_FACTOR *irony.

Выход из проблемы это варианты №1 и №2, т.к. они дают бизнесу уверенность, что за обозримое время недорогие разработчики разберутся в вашем коде. А разработчикам сопроводжающим код не прийдется уходить в запой от чувства собственной ничтожности.

Возлюбите ближнего своего — пишите код не для себя, а для людей! 🐱

Прощай чистый код!

Timofey Aksenov

Timofey Aksenov

Front End Developer

Ден Абрамов в своем блоге опубликовал крайне интересную статью о том, когда и почему мы нарушали, нарушаем и будем нарушать принцип DRY.

Код для привлечения внимания

let { top, bottom, left, right } = Directions;
function createHandle(directions) {
// 20 lines of code
}
let fourCorners = [
createHandle([top, left]),
createHandle([top, right]),
createHandle([bottom, left]),
createHandle([bottom, right]),
];
let fourSides = [
createHandle([top]),
createHandle([left]),
createHandle([right]),
createHandle([bottom]),
];
let twoSides = [createHandle([left]), createHandle([right])];
function createBox(shape, handles) {
// 20 lines of code
}
let Rectangle = createBox(Shapes.Rectangle, fourCorners);
let Oval = createBox(Shapes.Oval, fourSides);
let Header = createBox(Shapes.Rectangle, twoSides);
let TextBox = createBox(Shapes.Rectangle, fourCorners);