Жабаскрипт (веде Віктор Турський)
4.62K subscribers
22 photos
2 videos
273 links
Авторський контент для JavaScript розробників, але не завжди про JS:). Пишу про архітектуру, best practices, продуктивність, безпеку, інструментарій.

Viktor Turskyi (@koorchik), Cofounder at Webbylab, SWE at Google

Рекламу не розміщую!
Download Telegram
Тред от Yehuda Katz про проблемы с кроссплатформенностью NodeJs модулей. Многие разработчики забивают на Windows. В этом нет большой проблемы, пока ты не публикуешь свой модуль на NPM.

Яхуда озвучивает 2 основные проблемы при работе под Windows:
1. Практически каждый package.json содержит команды в секции scripts, которые не работают в Windows. Основная причина - использование && и другого bash синтаксиса. От себя добавлю, часто вижу, что устанавливаются переменные окружения без использования crossenv, к примеру.
2. Это пути на файловой системе. Разработчики ожидают, что разделитель "/" - это валидный разделитель в Windows разделитель "\" ("\\"). Это создает ряд багов связанных с обработкой путей (например, когда мы обабатываем пути регулярками и тд).

Как делать правильно, можно найти в треде
https://twitter.com/wycats/status/1090307478254829569

Делайте свои модули кроссплатформенными, Micsrosoft сделала же VSCode под Linux и Mac :)
В догонку к позавчерашнему сообщению про vm статья о том, как Cloudflare начала использовать изоляты V8 для serverless вычислений. Идея дать возможность пользователям заливать свой код и выполнять его в безопасной песочнице (по примеру AWS Lambda или Google Cloud Functions)

Что мы имеем, в сравнение с классическим подходом, когда лямбды крутятся в docker контейнерах:
1. Холодный старт 5 ms, вместо от 500ms.
2. Потребление памяти 3mb вместо минимальных 35mb необходимых при механизме с контейнерами.
3. Стоимость в 3 раза ниже, чем AWS Lambda. Правда код нужно писать на жабаскрипте :)

СТАТЬЯ: https://blog.cloudflare.com/cloud-computing-without-containers/

После прочтения поста возникает вопрос, что такое "Isolate"? В интернете практически нет информации. Можно найти, что это deprecated фича, но на самом деле это не так.
Если посмотреть в исходники NodeJs, то можно увидеть, NodeJs workers используют изоляты -
https://github.com/nodejs/node/blob/master/src/node_worker.cc , а поверх node_worker, в свою очередь, построены новые worker_threads.

Isolate - это, по сути, отдельный экземпляр V8, со своим состоянием. Важно не перепутать:
1. Изоляты это фича v8, а не nodejs. worker_threads и тот же vm - это уже модули nodejs.
2. Cloudflare не использует NodeJs, а использует именно v8 (и изоляты).
Многие спрашивают про Test Coverage, каким он должен быть? Нужно ли стремится к 100% покрытию кода тестами?

Основная проблема, что уровень покрытия кода тестами ничего не говорит о качестве тестов. Помню мне рассказывали историю, когда покрытие было недостаточным и не могли принять реквест от разработчика. Разработчик не долго думая добавил просто вызовы функций в тесты (без проверки результата), чтобы увеличить покрытие до требуего уровня :).

Если вы стремитесь к 100% покрытию, то скорее всего тратите время не на то, что действительно важно (покрыть более сложные сценарии или граничные случаи). Главный критерий достаточного количества тестов - это количество багов на продакшене и насколько вы боитесь вносить изменения в код в связи с ожиданием дополнительных багов на продакшене.

Нужно ли использовать test coverage? Да, мы используем по следующим причинам:
1. Test coverage не позволяет быть увереным в наличие хороших тестов, но низкий test coverage явно говорит об отсутствии тестов вообще. Для бекенда для нас 80% покрытия всегда достаточно. С nyc (istanbul) отслеживать покрытие очень легко. Для фронтенда у нас нет требований по покрытию.
2. Поиск мертвых кусков кода. Про это в статье не говорится, но при помощи анализа покрытия это очень удобно делать. nyc (istanbul) позволяет в качестве репортера указать html и вы очень легко можете найти код, который никогда не вызывается и никогда не будет вызываться. Поиск мертвых кусков кода сложно переоценить. Часто этого кода бывает много и он сильно мешает потом новым разработчикам разбираться в проекте.
3. Поиск подсистем, которые вообще не покрыты тестами. Бывают ситуации, когда просто не написали тесты к какой-то функциональности по какой-то причине.

Более детально про покрытие кода тестами можно почитать у Мартина Фаулера в статье "Test Coverage" https://martinfowler.com/bliki/TestCoverage.html

Статья уже старая, но вопрос актуальный :)
Отличный пост от Дэна Абрамова про то, как концептуально работает React - https://overreacted.io/react-as-a-ui-runtime/
Пост не перегружен исходниками, просто понятное краткое описание каждого аспекта работы React.
Eсли вы используете React (независимо от вашего уровня junior, middle, senior, architect, guru, js ninja, js samurai etc) и хотите чуть лучше его понимать, то очень рекомендую.
К примеру, я никогда не задумывался про Lazy Evaluation.
Интересно, что в посте ни одного примера на классах, все на компонентах-функциях с использованием хуков. Фейбук делает хуки своим основным API, классы, я думаю, останутся только для соместимости.
Решил продолжить про тестирование.
Когда в Jest добавили снапшпоты и возможность тестировать React компоненты при помощи снапшотов, все ринулись их использовать и писать насколько жизнь стала лучше :). Но снапшот тестирование может принести больше проблем, чем пользы:
1. Снапшот тесты хрупкие. Один assert проверяет все, вместо конктетной функциональности.
2. Нарушают инкапсуляцию. На один компонент Button может опираться масса других компонентов. Его поменяли и все посыпалось (особенно, если без shallow render)
3. Возникает антипаттерн "Guru Checks Output". Когда все посыпется, джун просто запустит jest -u && git commit. То есть, всегда должен быть человек ("Гуру", который знает детали реализации), который проверит, весь вывод и опираясь на свои знания деталей реализации, скажет это мы что-то сломали или тесты посыпались, но мы ничего не сломали и можно комитить.

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

Хороший пост на эту тему https://randycoulman.com/blog/2016/09/06/snapshot-testing-use-with-care/
И про "Guru Checks Output" https://wiki.c2.com/?GuruChecksOutput.
Мы пишем на JavaScript, но бывают случаи, когда нужная нам библиотека есть только на Java. В моей практике была такая ситуация. Мне нужна была библиотека Semgrex со стендфордского NLP парсера. Semgrex - эта библиотека, которая позволяет применять регулярки для выбора узлов с дерева разбора предложения (гуглить dependency tree nlp).
Конечно, можно писать весь код на Java, но не сильно хочется. Другой вариант - это вызывать Java из NodeJs.
Java предоставляет JNI (Java Native Interface), что дает нам возможность вызывать методы Java с другого языка и под NodeJs есть отличный модуль https://www.npmjs.com/package/java, который является мостом к JNI.
В результате, вы без особых проблем можете использовать классы Java внутри вашего NodeJs приложения, как если бы они были классами JavaScript.

В качестве примера, можете глянуть на исходники nodejs обертки вокруг java Semgrex https://www.npmjs.com/package/semgrex
Хочу поделиться отличным постом моего коллеги из WebbyLab. В названии звучит "Redux на бекенде". На самом деле, статья про то, как удалось переиспользовать одни и те же редьюсеры в веб, в мобильном и в бекенд приложениях, автоматически получить полноценную оффлайн работу приложения практически без дополнительных усилий. Статья рассказывает про event sourcing, показывает общее и различное между Redux и event sourcing, демонстрирует необычное, но эффектинове решение на базе совмещения Redux и event sourcing.

Мы привыкли к классическим схемам с React и Redux, но этот пост расширяет кругозор и предлагает другой подход.

https://hackernoon.com/how-we-used-redux-on-backend-and-got-offline-first-mobile-app-as-a-result-b8ab5e7f7a4
Попалась мне сегодня на глаза статья "Using TensorFlow.js to Automate the Chrome Dinosaur Game (part 1)". Да, это неплохой тьюториал, но ничего особенного для тех, кто уже работает с Tensorflow (а так можете почитать), но интересна она по другой причине.

Считается, что ML/AI существует только в Python. А что же с другими языками, в том числе с JavaScript? JavaScript используется для всего сегодня и странно, если бы не началось ML движение на JS. По факту, так и происходит.

К примеру, в прошлом году мы делали проект связанный с интеллектуальным поиском, вся эта история с различными алгоритмами word embedding и тд. Изначально мы начали все делать на python, а потом переключились на JS ввиду того, что на npm есть отличные врапперы вокруг C/C++ реализаций этих алгоритмов (сейчас пишем про это статью). Зачастую эти библиотеки работа комьюнити, но в случае с TensorFlow.js - это официальная библиотека от Google.

Сначала Google выпустил deeplearn.js - библиотека для ML с аппартным ускорением, которая работает в браузере. Браузер не поддерживает CUDA, но все равно можно использовать WebGL и шейдеры для математический расчетов. Вкратце, библиотека создает шейдер, который расчитывает цвет пикселя, а число, которое описывает цвет пикселя, по сути, и есть результат нашей математической операции (еще один костыль в мире веб :)). Эта библиотека и стала ядром для TensorFlow.js и была переименована в "TensorFlow.js Core".
Это очень круто и открывает много возможностей, но это про браузер. Как же работает TensorFlow.js в NodeJS? На самом деле, достаточно долго TensorFlow.js не работал в NodeJs (только недавно добавили поддержку). В NodeJs TensorFlow.js является байндингом к C/C++ имплементации (аналогично, как работает Python версия).

Также важно, что Keras API уже давно есть частью TensorFlow и поддерживается JS версией ( правда небольшие отличия все же есть) и вы можете работать с привычным высокоуровневым API.

Осталось дождаться порта scikit-learn на JS 😁

СТАТЬЯ: https://heartbeat.fritz.ai/automating-chrome-dinosaur-game-part-1-290578f13907
👍1
Отличная статья про микросовервисную архитектуру в Medium. Это тот случай, когда все сделано опираясь на здравый смысл. Изначально, у медиум был монолит на NodeJS, но ввиду ряда ограничений в 2018 они перехали на микросервисы.

Из ключевого:
1. Не делайте микросервисы ради микросервисов. Выносите в микросервисы только то, что имеет смысл выносить.
2. Микросервисы это не про количество строк кода. Микросервисы могут быть большими, главное чтобы они решали одну конкретную задачу.
3. Каждый микросервис имеет свой набор данных (разные способы достижения этого описаны в статье). Использование одной общей базы плохая идея.
4. RPC больше подходит чем REST для коммуникации между микросервисами. Medium использует gRPC.
5. Не обязательно делать микросервисы с нуля. Можно взять часть кода вашего монолита и вынести в отдельный сервис.
6. Плохо сконструированные микросервисы могут быть хуже монолита. Если вы не можете построить рабочий монолит, то с чего вы взяли, что сможете построить микросервисную архитектуру 😁
7. Микросервисы это еще не повод втянуть максимальное количество различных технологий, поскольку это может увеличить стоимость поддержки и фрагментировать команду.
8. Для небольших команд монолит будет лучшим решением, просто делайте его модульным.

СТАТЬЯ: https://medium.engineering/microservice-architecture-at-medium-9c33805eb74f
"The TypeScript Tax: A Cost vs Benefit Analysis" от Эрика Эллиота - пост, который поднимает вопрос дает ли на самом деле TypeScript выигрыш на крупных проектах. Если вы его не читали еще, то очень советую прочитать. К тому же, 11 февраля он был расширен кейсом от AirBnB.

Эрик правильно говорит, что нельзя сравнивать просто TypeScript против JavaScript. Нужно сравнивать JavaScript+tooling+процессы против TypeScript+tooling+процессы. И в таком случае TypeScript дает слишком мало преимуществ.
Когда у вас есть инструменты, отстроены процессы разработки, то из 1000 багов вы находите 900, с остатка максимум 20% вам позволит найти TypeScript. То есть это 20 багов из 1000, и сразу возникает вопрос возврата вложений в технологию. Стоит ли оно того?

Мы попробовали TypeScript и Flow.js у себя на нескольких проектах и вот, что получили:
1. VSCode умеет делать автокомплит и для чистого JavaScript без аннотаций. Иногда удивляет, когда есть фабричный метод, который возвращает объект, в котором экземпляр класса, в котором есть свойства, которые отлично автокомплитятся :)
2. Мы по максимуму используем eslint для статического анализа и это покрывает основную часть проблем. Если нет нужного плагина, то мы пишем свои "eslint-plugin-more"
3. Я согласен с Эриком, что для перехода на TypeScript, разработчику нужно 2-3 месяца и 6-8 месяцев, чтобы им овладеть полностью.
4. Описание аннотаций занимает достаточно большое количество времени и при тех же временных затратах тесты более эффективны. Кроме того, после тестов, типизация дает не настолько существенный выигрыш.
5. Типы можно описывать по разному и достичь какого-то стандарта кросс-проектно достаточно тяжело. Кто-то считает, что нужно выжать все с TypeScript, кто-то - что нужно минимум аннотаций.
6. Далеко не всем JavaScript разработчикам нравится TypeScript

Я не против TypeScript, мне нравится идея статической типизации, считаю TypeScript отличным продуктом, но всегда нужно взвешивать за и против от использования той или иной технологии. Для меня чистый JavaScript это выбор по умолчанию, а TypeScript/Flow уже опция. И для полноты картины, могу привести случай, когда статическая типизация дала нам большой выигрыш. Мы использовали GTK байндинг к Spidermonkey и аргументы неправильного типа приводили к крашу всего приложения. Статическая типизация позволяла избежать этих ситуаций.

В целом, эти же аргументы описывает Эрик в своей статье, но еще он затрагивает вопросы рекрутинга, обучения, рефакторинга кода и тд.
1
Наткнулся на интересный тред в твиттере.
Чувак увидел, что кто-то форкнул его проект, не только форкнул, но и поменял авторство. В результате оказалось, что есть ребята, которые форкают известные репозитарии, называют их по другому, меняют авторство на свое, переписывают историю коммитов на свои аккаунты на Github. В результате, аккаунт, который был создан недавно имеет отличную историю коммитов, красивые графики, море активности и тд. И таких аккаунтов не один, они друг друга фоловят и тд. Зачастую это проекты на JavaScript.
Остается вопрос зачем они это делают. Может это какой-то бот-нет и подготовка к атаке на npm, а может кому-то нужен красивый акк, чтобы получить проект на upwork :). Гипотезы можете найти в Twitter треде.
Почти всегда, при разговоре о производительности веб-сайта, мы говорим о времени ожидания пользователем какого-то события ("First Meaningful Paint" и тд). Мы часто обсуждаем оптимизации фронтенда и бэкенда. И это круто, но остается корень всех бед - скорость света. Часто JavaScript разработчики упускают это проблему из виду 😀

Может ли случится такое, что через 30 лет Интернет будет настолько быстрым, что сайт с сервера в Сан-Франциско будет моментально открываться в Киеве? Физики говорит, что нет.

Допустим:
1. Твой бекенд рендерит страницу (или формирует JSON) за 20мс.
2. Не существует никакого WiFi, провайдеров, маршрутизации и тд. Есть просто оптоволоконный кабель, который одним концом вставлен в твой ноутбук, а вторым напрямую в сервер в Сан-Франциско. Растояние по прямой от Киева до Сан-Франциско - 9,848км (возьмем 10 тыс км для простоты счета).
3. Скорость света в вакуме 300 тыс км в секунду, скорость света в оптоволокне будет ниже - 200 тыс км в секунду.

Если мы посчитаем время, которое проведет наш запрос в пути, то мы получим 100 мс (10 тыс / 200 тыс * 2). Быстрее получить ответ не позволит скорость света. Добавляем время оработки запроса и мы получим 120мс - в 6 раз дольше, чем наш запрос обрабатывает наш бэкенд.

========= Запрос к бэкенду ======
50ms: Kyiv -------запрос-----> SF
20ms: работа бэкенда
50ms: Kyiv <------ответ------- SF


Хорошо, мы уже выяснили, что никогда не поиграем в CS:GO с ребятами с Сан-Франциско с пингом ниже 100мс. Давайте дальше :)

Перед тем как запросить данные с сервера мы должны установить сетевое соединение. Протокол HTTP работает поверх TCP, следовательно нам нужно TCP соединение с сервером.

Для установки TCP соедения используется так называемое "тройное рукопожатие" ("TCP 3-way handshake") и теперь наш запрос выглядит:

========= TCP соединение ========
50ms: Kyiv -------syn--------> SF
50ms: Kyiv <------syn/ack----- SF
50ms: Kyiv -------ack--------> SF
========= Запрос к бэкенду ======
Kyiv -------запрос-----> SF
20ms: работа бэкенда
50ms: Kyiv <------ответ------- SF


Мы не тратим дополнительные 50ms после TCP хендшейка, поскольку мы можем сразу начать отправлять запрос после отправки ack, нам не нужно ждать ответ от сервера. Сервер, как примет ack, посчитает соединение открытым и сразу начнет обрабатывать наш запрос.

То есть ответ пользователь получит через 220ms, в 11 раз дольше, чем отрабатывал наш бэкенд.

Но мы используем HTTPS и нам нужно SSL/TLS соединение и оно устанавливается поверх TCP, и у него есть свой механизм рукопожатия для обменя ключами шифрования, и это нужно сделать до момента, как мы отправим наш запрос на сервер.

Наша схема превращается в:

========= TCP соединение ========
50ms: Kyiv -------syn--------> SF
50ms: Kyiv <------syn/ack----- SF
50ms: Kyiv -------ack--------> SF
========= TLS соединение ========
Kyiv ---представление--> SF
50ms: Kyiv <--сертификаты----- SF
50ms: Kyiv ---обмен ключами--> SF
50ms: Kyiv <--обмен ключами--- SF
========= Запрос к бэкенду ======
50ms: Kyiv -------запрос-----> SF
20ms: работа бэкенда
50ms: Kyiv <------ответ------- SF


То есть в условиях, которые не могут даже существовать, когда пользователь имеет оптоволоконный кабель длинной в 10тыс км от своего ноутбука к серверу, он получит ответ через 420мс, в 21 раз дольше чем отрабатывает наш бэкенд. Это без учета того, что нам нужно еще вначале сбегать к DNS, чтобы получить ip-адрес сервера.

Если мы разрабатываем веб-приложения (не важно фронтенд или бекэнд), то обязаны понимать азы работы веба.

Продолжение следует...
🔥3
Нормализация массива через "for of" и "reduce".

Набросал воскресным утречком бенчмарк https://jsperf.com/normalize-array. Для данного случая реализация через "reduce" работает в 5 раз медленее в Chrome и в 50 раз медленее в FF.
Необходимо понимать причины такой разницы в производительности. Сам "reduce" такой же быстрый как и "for of", просто мы имеем n^2 ввиду создания нового объекта и копирования свойств на каждой итерации.
Конечно, можно мутировать объект в редьюсере, но это противоречит функциональной природе редьюса, наша функция должна быть без побочных эффектов.

Да и вообще нормализация списка через "for of" выглядит в разы понятнее (имхо).
Вот отличная рекомендация по использованию reduce: используйте reduce, когда результат имеет тот же тип, что и элементы и сам редьюсер ассоциативный.

складываем числа
умножаем числа
🚫 строим список объектов
🚫 все остальное (используй цикл)

Обратите внимание на требования к ассоциативности. К примеру, если вы знакомы с парадигмой распределенных вычислений "MapReduce" и посмотрите на "MapReduce" в MongoDB, то найдете там похожий ряд требования к reduce функции:
1. Ассоциативность.

const reducer = (a, b) => a + b;

// Оба варианта должны дать одинаковый результат
[1, 2, 3, 4].reduce(reducer);
[1, 2, [3, 4].reduce(reducer)].reduce(reducer);


2. Коммутативность

const reducer = (a, b) => a + b;
// Оба варианта должны дать одинаковый результат
[1, 2, 3, 4].reduce(reducer);
[4, 2, 1, 3].reduce(reducer);


Есть еще требование к идемпотентности, но для нашего случая оно малоприменимо.

В целом, это все лишь рекомендации и идеи на подумать. Сам JavaScript не запрещает нам ничего.
👍2
Вот и продолжение: "Скорость света и Веб - часть 2" :)

Мы уже разобрались, что есть скорость света и она влияет на задержки при передаче данных. У нас есть задержки на TCP и TLS рукопожатия, также есть время в пути запроса и ответа. Можем ли мы говорить, что это максимальные задержки, которые мы получаем?

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

Есть 2 нюанса, которые важны:
1. TCP контролирует доставку пакетов и для того, чтобы понять, что пакеты были доставлены нужно какое-то подтверждение от получателя. Для этого в ответ отправляется пакет с флагом "ack" (acknowledge).
2. Клиент и сервер изначально не знают доступной на данный момент пропускной способности сети. Она зависит от возможностей сервера, от возможностей промежуточных узлов, от активности других узлов в этой же сети и тд. Единственный способ узнать - это пробовать передавать данные с разной скоростью и смотреть доходят ли они (ждать подтверждения, что вторая сторона получила их).

Как это работает?
Когда мы делаем запрос к серверу он сначала отправляет нам часть данных, потом ждет подтверждения, потом увеличивает объем передаваемых данных в 2 раза и опять ждет ответ. Если все ок, еще раз увеличивает и так далее до момента пока он не достигнет максимального объема данных, которые готов принимать клиент.

Как это все называется?
✳️ Механизм постепенного увеличения скорости передачи данных называется "TCP Slow Start"
✳️ Лимит отправителя на объем данных в пути называется "Congestion window size" (CWND). После отправки этого объема данных, отправитель должен ждать подтверждения о том, что данные дошли. Увеличения этого лимита и есть "TCP Slow Start". ВАЖНО: про этот лимит знает только отправитель и он сам для себя его регулирует. CWND имеряется в "сегментах" (сегмент обычно не более 1,46KB). Стартовое значение по стандарту - 10 сегментов (14.6KB)
✳️ Также есть ограничение получателя на объем данных, которое он может принять - "Receiver window size" (RWND). Получатель отправляет отправителю RWND в каждом пакете с подтвержджением (с флагом ack). Поскольку передача дынных происходит в обе стороны, то каждая сторона может выступать как получателем, так и отправителем. Получатель может передать RWND равным нулю, это говорит о том, что отправитель должен приостановить передачу.

Обе переменные ограничивают количество данных, которое можно отправить, это всегда минимум с CWND и RWND.

Теперь давайте нарисуем, что на самом деле происходит, когда браузер хочет скачать наш JavaScript файл на 50KB.
Возьмем те же локациии - Киев и Сан-Франциско.

========= TCP соединение ========
50ms: Kyiv -------syn--------> SF
50ms: Kyiv <------syn/ack----- SF
50ms: Kyiv -------ack--------> SF
========= TLS соединение ========
Kyiv ---представление--> SF
50ms: Kyiv <--сертификаты----- SF
50ms: Kyiv ---обмен ключами--> SF
50ms: Kyiv <--обмен ключами--- SF
====== HTTP запрос к серверу ====
50ms: Kyiv -------запрос-----> SF
20ms: работа бекенда
50ms: Kyiv <-----14.6KB------- SF
50ms: Kyiv -------ack--------> SF
50ms: Kyiv <-----29.2KB------- SF
50ms: Kyiv -------ack--------> SF
50ms: Kyiv <-----6.2KB-------- SF

Скорость в 100Мбит/с говорит о том, что мы получим 50KB через 4ms, но на самом деле у нас это займет 620ms.
Самое интересное, что если бы наш JS файл был бы 40KB, то мы получили бы его на 100ms раньше.

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

Поэтому используйте Gzip компрессию c HTTP, следите за Cookie (они могут быть большими), сжимайте картинки и удаляйте с них метаданные . Конечно, не забывайте про CDN (может дать существенный выигрышь).

Дальше я попробую описать более детально, что у нас есть, чтобы сделать наши веб-приложения более быстрыми.

Продолжение следует...
👍1
Обычно я не скидывают ссылки просто на новости, но это интересный ресерч, который провели ребята из Snyk (Open Source Security Platform).

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

* Стандартный образ "node:10" содержит 580 известных уязвимостей.
* Слим образ "node:10-slim" содержит 71 известную уязвимость.
* Образ node:10-alpine не содержит известных уязвимостей.

Более того, если взять топ-10 образов на dockerhub, то каждый из них содержит минимум 30 уязвимостей.

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

Если вы используете NodeJs, то смотрите в сторону node:10-alpine. Alpine Linux - это один из самых легковесный дистрибутивов линукса. Чистый образ Alpine Linux занимает 5МБ.
По сути, это musl libc (минималистчиная замена glibc, нужен нам для коммуникация с ядром и базовых операций) и busubox (маленький бинарь, который содержит самые необходимые unix утилиты. К примеру, android его использует).

На базе Alpine есть образы не только для NodeJs, но и для другого софта. Рекомендую свои образы строить тоже на базе Alpine Linux.

СТАТЬЯ: https://snyk.io/blog/top-ten-most-popular-docker-images-each-contain-at-least-30-vulnerabilities/
Я время от времени делаю доклады про архитектуру веб-приложений. И там всегда есть 2 важных пункта:

1. Архитектура вашего приложения не должна зависеть от веб-фреймворка. Нужно писать код так, чтобы вы могли заменить веб-фреймворк (скорее всего вы делать это не будете, но это хорошая мысленная проверка на связанность).
2. Model (из MVC) - это ваша бизнес-логика и логика приложения. Model может быть реализован совершенно разными способами. Чаще всего у нас это Services(use cases) + Domain Model. И слой Model не зависит от Controller и View.

Если все сделано правильно, то вы сможете без проблем заменить JSON в вашем REST API на XML, и вам нужно будет только обновить контроллеры. Или даже больше, вы меняете веб-интерефейс на интерфейс командной строки и вам не нужно переписывать бизнес-логику, поскольку все смещенно в слой Model.

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

И если у вас все хорошо с архитектурой, то вы не работаете с базой данных напрямую. Вы подключаете Domain Model или нужные сервисы в ваши скрипты и работаете уже с ними. Это обеспечивает целостность ваших данных. Кроме того, для многих операций в будущем может появится Web-интерфейс.

API ваших скриптов - это аргументы командной строки, их нужно парсить. Поскольку в WebbyLab мы используем не только NodeJs, то для парсинга аргументов нам зашел https://docopt.org/ Есть реализация под различные языки (есть docopt модуль и на npm). Основная идея - вы пишите справку по использованию, а docopt автоматически парсит аргументы командной строки в соотвествии со справкой.

Отличный пример на сайте docopt, копируете его и правите под себя :)

Naval Fate.

Usage:
naval_fate ship new <name>...
naval_fate ship <name> move <x> <y> [--speed=<kn>]
naval_fate ship shoot <x> <y>
naval_fate mine (set|remove) <x> <y> [--moored|--drifting]
naval_fate -h | --help
naval_fate --version

Options:
-h --help Show this screen.
--version Show version.
--speed=<kn> Speed in knots [default: 10].
--moored Moored (anchored) mine.
--drifting Drifting mine.


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

Но совсем недавно мне скинули неплохой тьюториал для новичков про то, как оформить ваше CLI приложение в npm пакет "A guide to creating a NodeJS command-line package"

Он будет хорошим дополнением ко всему вышесказанному.

Основные моменты статьи:
1. Не забываете про шебанг #!/usr/bin/env node
2. Делайте файл исполняемым chmod +x cli.js
3. Добавьте секцию bin в ваш package.json. Например, bin: {myapp: './cli.js'}. Тогда при установке вашего пакета npm создаст в директории с бинарями симлинк "myapp", который будет указывать на ваш "cli.js".
4. npm link линкует не только пакеты, но и скрипты с секции bin. Этого я сам не знал :)
Сегодня хочу поделиться отличной статьей от одного из моих коллег из WebbyLab. Статья про JavaScript, машинное обучение и Игру Престолов 😁

Не так давно мы делали интересный проект связанный и интелектуальным поиском по медицинским данным. Основная задача - поиск по смыслу (не путайте с полнотекстовым поиском). Например, если мы ищем "рак", то система должна находить "меланома". Мы можем искать по аналогиям вида, для лечения болезни "А" используется "Б", что тогда используется для лечени болезни "C". Чтобы это реализовать, для каждого слова необходимо построить некоторое представление, которое хранит его смысл (смысл определяется словами вокруг целевого слова и называется контекстом). Таким представлением выступает вектор в N-мерном пространстве (обычно от 100 до 300). Имея такие вектора для каждого слова, можно делать математические операция над векторами, что позволяет искать похожие слова по смыслу и тд. Это называется word embeddings и есть различые модели построения векторов (word2vec, GloVe, fastText и тд). Все детали есть в статье.

Но для того, чтобы не было скучно, автор решил обучать модели текстами романа "Игра Престолов" и искать связи между героями 🙈. Что из этого получилось - найдете в статье.

Кроме того, ты сможешь повторить все это самостоятельно с любым текстом (код прилагается).

Статья на Medium: https://medium.com/@WebbyLab/how-to-train-word2vec-model-with-nodejs-in-5-steps-41b602080c1a

Подписывайтесь на наш аккаунт на Medium :)
Хотел поделиться забавным опытом использования JavaScript. Я бы назвал это: "Лямбды на шару в Google документах" 😁

Всегда хочется расширить границы для JavaScript разработчика. Можно делать декстопные приложения, мобильные приложение, можно обучать нейронки при помощи Tensoflow.js и много всего другого. Но мало кто знает, что есть Google Apps Script и это очень простой в использовании и мощный инструмент, и пишити вы скрипты на JavaScript :)

Изначальная задача: пользователь заполняет форму регистрации на ивент, а потом оплачивает услугу. У каждого пользователя своя ссылка для оплаты услуги, поскольку в ссылку зашито описание с именем (нужно, чтобы потом найти платеж в LiqPay), сумма и тд.

Я использую систему LiqPay, ссылка должна генерироваться на сервере, поскольку данные должны быть подписаны секретным ключом, который не должен быть доступен публично.

Шаги:
1. Создаю Google форму регистрации на ивент и в меню выбираю пункт "Script Editor"
2. Разбираюсь, как LiqPay генерирует ссылки (механизм подписи и формат данных) и реализовываю свою версию.
3. Пишу код, который берет с формы email пользователя и генерирует ссылку.
4. Вешаю на onSubmit отправку ссылки для пользователю на его email. Самое крутое, что ничего не нужно настраивать, письмо уйдет через твой Gmail акк.

Но можно пойти дальше. Скрипт можно опубликовать, как add-on. Дописать на HTML/CSS UI для выбора поля из которого брать email, указать сумму для оплаты и тд. И такой add-on можно подключать в любую форму.

Но интереснее даже другое, как использовать Google Docs для своего HTTP бэкенда (по типу serverless). И это тоже можно :)

Пример с LiqPay:
1. Переходим на https://script.google.com/ и выбираем "New Script".
2. Создаем функцию "doGet" ("doPost" тоже поддерживается) и в этой функции описываем логику, которая должна отработать когда пришел к нам запрос. Мой код, только нужны LiqPay ключи.
3. Выбираем "Deploy as web app" в меню "Publish"
4. Используем полученную ссылку как эндпоинт для нашего бэкенда :)

Протестировать можно тут - https://script.google.com/macros/s/AKfycbzGo5SXnnH_CwxQgbKYI88oGuyq8Eh7UBqfTeB2x5S6iEdlA0jS/exec?description=ЖабеНаЕду (можете менять описание на любое другое и бэкенд будет генерировать новую подписанную ссылку для LiqPay)

Ваш скрипт может не только генерировать ссылку, но и может использовать все возможности Google Docs (создавать/читать/обновлять любые типы документов, отправлять почту и тд).

Вот классная статья, которая описывает, как сделать свою serverless CMS на базе "Google Apps Script".В качестве админки для добавления постов - Google Forms, хранилище данных - Google Spreadsheets. Весь фронт на HTML/CSS и общается с "лямбдами" гугла. Есть пагинация, категории, все по AJAX.

Да, вот такими глупостями занимаюсь на выходных 😁
"Скорость света и Веб - часть 3".

Если вы не читали первые 2 части, то найдите их в истории канала. Стоит начать с них.

Мы закончили на том, что даже при канале в 100Мбит/c, нужно более 600мс (а не 4мс), чтобы получить 50КБ данных при первом подключении, если клиент в Киеве, а сервер в Сан-Франциско.

Хотел перейти к решениям, но есть еще одна проблема про которую все-таки стоит сказать - "Head-of-line Blocking". На самом деле, когда говорят про "Head-of-Line Blocking", то могут подразумевать разное. Есть 2 варианта этой проблемы:

1️⃣ "Head-of-line Blocking" на уровне TCP

Мы рассмотрели ситуацию, когда у нас нет потерь пакетов, но на практике пакеты всегда теряются. Более того, TCP Slow Start увеличивает скорость пока не начнут теряться пакеты, потом значительно уменьшает скорость и начинает поднимать более медленно.

Потери пакетов могут приводить к "Head-of-line Blocking" на TCP уровне.
Попробую описать основную идею.
TCP отвечает за то, чтобы пакеты пришли в приложение в правильном порядке. Если сервер отправил:
[1][2][3][4][5]
а получили мы (или в другом порядке)
[2][3][4][5]
То эти пакеты находятся в TCP буфере получателя пока сервер отправляет нам повторно пакет [1]. То есть, задача TCP протокола выстроить пакеты в правильную очередь перед тем, как они попадут в приложение. Это удобно, но далеко не всегда нужно.

2️⃣ "Head-of-line Blocking" на уровне HTTP/1.x

Тут немного другая ситуация.
Допустим, нам нужно сделать 10 HTTP запросов. Браузер отправляет запросы один за одним и получается, чтобы отправить новый, он должен дождаться результата предыдущего.

Схематически это выглядит так:

50ms: Kyiv ------запрос 1----> SF
20ms: работа бэкенда (запрос 1)
50ms: Kyiv <-----ответ 1------ SF
50ms: Kyiv ------запрос 2----> SF
20ms: работа бэкенда (запрос 2)
50ms: Kyiv <-----ответ 2------ SF
50ms: Kyiv ------запрос 3----> SF
20ms: работа бэкенда (запрос 3)
50ms: Kyiv <-----ответ 3------ SF

Для упрощения я упустил все моменты связанные с установкой соединения (TCP-handshake, TLS-handshake, TCP Slow Start).

В связи с этим, в HTTP/1.1 появился "HTTP Pipelining". Суть - отправить сразу пачку запросов и ждать ответы.
"HTTP Pipelining" выглядит так:

50ms: Kyiv ------запрос 1----> SF
Kyiv ------запрос 2----> SF
Kyiv ------запрос 3----> SF
20ms: работа бэкенда (запрос 1)
работа бэкенда (запрос 2)
работа бэкенда (запрос 3)
50ms: Kyiv <-----ответ 1------ SF
Kyiv <-----ответ 2------ SF
Kyiv <-----ответ 3------ SF

Это полезная штука (120мс против 360мс), но на практике она отключена в большинстве браузеров ввиду того, что реализации серверов часто содержат баги. Но даже, если бы это работало, все равно мы имеем проблему "Head of line blocking": если обработка первого запроса займет 1 секунду, то ответы не смогут вернуться раньше чем через секунду (поскольку первый запрос блокирует возврат остальных).

Да, браузер может параллельно открывать 4-6 соединений (это с настройками по умолчания), но это лишь частично спасает ситуацию.

Проблемы с DNS. Я не обсуждал запросы к DNS ввиду:
✳️ В 99% случаев для DNS используется UDP (за редкими исключением, когда ответ не влазит в датаграмму, тогда может быть инициировано TCP соединение). То есть нам почти никогда не нужна установка соединения, что сильно уменьшает нашу проблему. Вопросы безопасности пока упустим.
✳️ Скорее всего, мы обращаемся к DNS серверу провайдера и сервер этот находится достаточно близко. Да, это все равно отдельный запрос, который тоже влияет на то, как быстро пользователь увидит страницу, но в детали пока вдаваться не будем.

Теперь точно можно переходить к обсуждению того, как с этим всем жить и что делать дальше.

Продолжение следует...
👍1
Как мы управляем конфигами для NodeJs приложений?

Решил поделиться, как мы управляем конфигами для NodeJS приложений.
Давайте отталкиваться от того, что у нас есть несколько окружений, к примеру "prod", "staging", "qa".

Как не стоит управлять конфигурацией?
Я часто видел решения, когда в репозитарии есть config-prod.json, config-staging.json, config-qa.json. Рекомендую такого подхода избегать.

Он имеет массу недостатков:

1. Разграничение доступа. Все разработчики, которые имеют доступ к коду не обязательно должны иметь доступ к продакшену. Также такой код нельзя выложить в OpenSource ввиду наличия в репозитории конфигов с параметрами доступа к вашим окружениям.
2. Набор окружений может расширяться: различные дев-боксы, локальное окружение и тд. Это будет тяжело поддерживать. Вам постоянно придется добавлять новые файлы репозиторий и поддерживать много копий конфигов.
3. Создавая билды в виде docker-контейнеров, они будут привязаны к окружению и это нарушает главную идею использования билдов. По правильному было бы протестировать билд на QA, а потом выкатить этот же билд в prod, а не собирать новый. Сборка нового повышает риск, что у нас будет другая конфигурация (например, обновился один из зависимых пакетов).

Подход "Двенадцать факторов" и почему его недостаточно

Если вы используете подход "Двенадцать факторов", то уже все это знаете. По принципу "Двенадцати факторов", конфигурация должна передаваться в переменных окружения - https://12factor.net/ru/config.

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

Но одновременно, передавать переменные окружения, при запуске процесса, не слишком удобно. Есть популярная библиотека dotenv, которая позволяет сохранять переменные окружения в файле .env, но вы не должны коммитить этот файл в репозиторий, обычно в репозитории вы держите файл с примером конфигурации (например ".env.sample", который нужно скопировать в .env файл, если вы деплоите без докера). Вы можете использовать dotenv-defaults, библиотека, которая позволяет вам иметь файл .env.defaults со значениями по умолчанию, который вы уже коммитите в репозиторий.

Но в реальности и этого недостаточно. У вас будут достаточно сложные конфиги с вложенностями и и вы часто не хотите определять все значения в переменных окружения, их слишком много и не все из их изменяются в зависимости от окружения (например ссылка на ваш аккаунт вашего продукта в твиттере должна быть в конфиге, но ее нет смысла передавать в переменных окружения). Если вы знакомы с ansible, то знаете, что можно иметь шаблон конфига и подставлять в него переменные в зависимости от окружения и собирать финальный конфиг, но вы помните, что вы не можете паковать этот конфиг в билд. В связи с этим, мы храним шаблон конфига в репозитории и шаблонизируем его переменными окружения. Также, если в конфиге много опций, то имеет смысл автоматически валидировать его корректность после шаблонизации, это сильно облегчит жизнь разработчикам.
Как мы управляем конфигами для NodeJs приложений? (продолжение предыдущего поста)

Теперь к делу, вместо того, чтобы просто рассказывать про проблемы, я посидел ночь и напилил confme - решение, которое идет в наш бойлерплейт по NodeJS и может быть полезно комьюнити :)

confme совсем небольшая библиотека, но решает все вышеописанные проблемы, это небольшая обертка вокруг dotenv-defaults и LIVR (для валидации).

Что делает confme?

"confme" загружает конфиг и шаблонизирует его переменными окружения. Для получения переменных окружения библиотека использует dotenv-defaults, то есть вы можете создать файл ".env.defaults" (если захотите) для сохранения значений по умалчанию ваших переменных окружения. Если в конфиге у вас есть плейсхолдеры для которых не существует переменных окружения, то "confme" выбросить исключение.

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

Как использовать?

const confme = require("confme");
const config = confme( __dirname + "/config.json" );

Файл с конфигурацией:

{
"listenPort": "{{PORT}}",
"apiPath": "https://{{DOMAIN}}:{{PORT}}/api/v1",
"staticUrl": "https://{{DOMAIN}}:{{PORT}}/static",
"mainPage": "https://{{DOMAIN}}:{{PORT}}",
"mail": {
"from": "MyApp",
"transport": "SMTP",
"auth": {
"user": "{{SMTP_USER}}",
"pass": "{{SMTP_PASS}}"
}
}
}

DOMAIN, PORT, SMTP_PASS, SMTP_PASS нужно передать в переменных окружения любым способом:

Я рекомендую поступать следующим образом:

☑️ Значения по умолчанию для переменных окружения определите в файле ".env.defaults". Они должны подходить для локальной разработки. То есть, разработчику после клонирования репозитория не нужно будет ничего настраивать.
☑️ Если вы деплоите без контейнеров и лямбд, то можете во время деплоймента создавать файл ".env" и переопределять переменные по умолчанию для вашего окружения. Либо можно передавать при запуске, если у вас всего несколько параметров (правда есть риск забыть про них при перезапуске или дебаге продакшена): DOMAIN=myapp.com PORT=80 node app.js.
☑️ Если вы собираете докер контейнер или пакуете лямбды, то просто передавайте переменные окружения способом, который подходит для вашего окружения (для этого всегда есть механизм).

"confme" из коробки поддерживает валидацию конфига при помощи LIVR. Использует LIVR JS и livr-extra-rules (набор дополнительных правил)

Вы можете передать путь к файлу со схемой конфига вторым аргументом:

const confme = require("confme");

const config = confme(
__dirname + "/config.json",
__dirname + "/config-schema.json"
);

Вот пример схемы:

{
"listenPort": ["required", "positive_integer"],
"apiPath": ["required", "url"],
"staticUrl": ["required", "url"],
"mainPage": ["required", "url"],
"mail": ["required", {"nested_object": {
"from": ["required", "string"],
"transport": ["required", {"one_of": ["SMTP", "SENDMAIL"] }],
"auth": {"nested_object": {
"user": ["required", "string"],
"pass": ["required", "string"]
}}
}}]
}

Буду рад получить фидбек от читателей канала. Можно в виде звездочек на гитхабе или пишите мне в ТГ. Я - @koorchik