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

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

Рекламу не розміщую!
Download Telegram
Наткнулся на интересный тред в твиттере.
Чувак увидел, что кто-то форкнул его проект, не только форкнул, но и поменял авторство. В результате оказалось, что есть ребята, которые форкают известные репозитарии, называют их по другому, меняют авторство на свое, переписывают историю коммитов на свои аккаунты на 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
Рабочая архитектура веб-приложений.

Не так давно прошла конференция для архитекторов devpoint.ua. Я делился тем, как мы строим архитектуру веб-приложений у нас в компании.

Архитектура очень схожа на "Clean Architecture" Роберта Мартина. Про "Clean Architecture" многие говорят, но это достаточно абстрактная концепция и от деталей зависит многое, я постарался рассказать, как мы на практике ее используем. Эта архитектура у нас работает на NodeJs, Perl, PHP проектах совершенно разных масштабов (более 60 проектов). Первый проект на этой архитектуре мы построили 7 лет назад, за это время архитектура эволюционировала, но ядро и концепции не поменялись за все годы.

Когда мы внедрили свою архитектуру, то понятия "Clean Architecture" еще не было, мы все строили опираясь на здравый смысл, а потом через какое-то время нашли этому всему правильные названия. На самом деле, вся история про различные шаблоны проектирования именно про названия. То есть это не сокральное знание, а что-то, что люди часто используют и дали ему имя. Зачастую шаблон проектирования это не определение "лучшего решение" (или даже не Best Practices), а просто решение, которое часто работает в определенной ситуации. Думаю, стоит написать отдельный философский пост посвященный моему видению различных архитектурных шаблонов 🙂

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

ВИДЕО ДОКЛАДА: https://www.youtube.com/watch?v=TjvIEgBCxZo

Обещаю ответить на все вопросы в комментариях под видео (если такие будут) 🙂

На самом деле, очень большая проблема найти адекватный бойлерплейт для проектов на NodeJs. Возьмите тот же NodeJs бойлерплейт от Microsoft. Несмотря на 4736 звезд, это не то, что я был взял себе в проект. Из очевидных моментов:
Не хватает слоя сервисов (юз-кейсов) и в результате у нас нет нормальной изоляции от HTTP состовляющей и формата передачи данных.
Не хватает надежного подхода к валидации. То есть, разработчик может смело пропустить валидацию и не ясно, что делать с иерархическими структурами.
Бойлерплейт весь на колбеках (continuation passing style). У нас уже давно есть промисы, и даже более - async/await
И масса других вопросов остаются открытыми (локализация, конфигурации и тд).

В очередной раз думаю, что нужно заопенсорсить наш бойлерплейт, но основной момент - это подготовить документацию и описать идеи, которые стоят за ним. Видео доклада - первый шаг в эту сторону 🙂
👍1
Мое утро началось с просмотра исключительно интересного доклада "Rich Harris - Rethinking reactivity"

Что будет, если фреймворк превратить в компилятор? Получится svelte.

Svelte - библиотека, которая очень похожа на React, но:
Не имеет VirtualDOM, вся реактивность добавляется во время компиляции.
Компоненты получаются компактнее чем в React (исходный текст компонентов).
Svelte в 35 раз быстрее чем React и в 50 раз быстрее чем Vue.
Имеет поддержку CSS изоляции, анимаций и много другого, но это не увеличивает размер вашего бандла, если вы не это не используете. Это работает, поскольку у библиотеки нет рантайма (или почти нет) - все проиходит на этапе компиляции.
Имеется Sapper - аналог Next.js, но только для Svelte.

Не нужно сразу выбрасывать React/Vue/Angular/Ember и переходить на svelte, но точно стоит с этим поиграться, посмотреть на новые идеи.

ВИДЕО: https://www.youtube.com/watch?v=AdNJ3fydeao
Наткнулся на интересную статью на Хабре "Нужно ли чистить строки в JavaScript?"

В двух словах. Если у вас есть строка на 20мб, а вы вырезали из нее подстроку на 13 или больше символов, то v8 будет хранить ссылку на изначальну большую строку и не будет вычищать память.

Пример кода со статьи:

function MemoryLeak() {
// 15МБ строка
let huge = "x".repeat(15*1024*1024);

// Маленький кусочек
let small = huge.substr(0,25);
return small;
}

let arr = [];
setInterval(e=>{
let str = MemoryLeak();
arr.push(str);
console.log(JSON.stringify(arr).length, 'байт');
}, 500);

Я проверил на Node v12.2.0
Размер массива arr у меня был 3837 байт, а размер процесса дорос до 2ГБ и крашнулся с ошибкой
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

Такое поведение существует много лет, тикет был заведен еще в 2013 году.

Насколько это большая проблема? Если мы возьмем бэкенд, то он обычно не хранит состояние в памяти между запросами, и если пропадет ссылка на arr, то сборщик мусора все вычистит. Но вы можете столкнуться с данной проблемой, когда у вас есть кеши, которые живут между запросами, также на фронтенде, когда у вас есть долгоживущее состояние приложения (в SPA).
Мой коллега Рома Пухлий решил сделал полезное дело и написал статью про Linux утилиты, которые следует знать каждому разработчику - "Linux utilities that every developer should know"

Статья содержит примеры решения ежедневных задач при помощи командной стоки. Примеры поделены на разделы:

✳️ Работа с файлами
✳️ Работы с дисками
✳️ Чтение логов
✳️ Поиск по файловой системе
✳️ Работа с процессами
✳️ Запуск процессов в фоне
✳️ Использование системных ресурсов
✳️ Просмотр информации о системе
✳️ Сетевые утилиты
✳️ Другие утилиты

К этому всему есть удобная шпаргалка в PDF https://blog.webbylab.com/images/linux-print.pdf
Большинство команд должно работать и на OSX.
Вот прошел Google.io и я наткнулася на пару релевантных докладов. Один из них - What’s new in JavaScript (Google I/O ’19)

Я много лет в разработке, различные скрипты для управления сетевым оборудованием начал писать где-то в 2003. И практически с самого начала я писал на Perl и на JavaScript. Соответственно, я всегда сравниваю Perl и JavaScript. Я рад, что многие возможности, которые были в том же Perl много лет, появляются в JS.

Когда, я писал первый крупные SPA, то приходилось использовать ES3. С того момента JS кардинально поменялся:

✳️ В ES3 мне очень не хватало перловых "map" и "grep", пока не появились аналоги в JS - "map" и "filter".
✳️ Мне не хватало интерполяции переменных, как в Perl. В Perl каждая переменная начинается со знака $ и в двойных кавычках она интерполируется. То есть можно писать "Hello, ${name}!". Потом в JS появились Template Literals с аналогичным синтаксисом.
✳️ Переменные с лексической областью видимости - одна из фич, которые я прям ждал в JS. Даже до "let" и "const", я все равно объявлял все "var"-переменные , как если бы они имели локальную область видимости - максимально близко к месту их использования, такой код легче потом рефакторить.

JavaScript не варится сам в себе, а смотрит на соседние языки. Грамотно заимствует различные фичи, делая язык все более мощным.

Теперь по докладу:
1️⃣ Наконец-то в JS появился "string.matchAll". Кроме того, в stage 4 "RegExp Named Capture Groups" с синтаксисом как в Perl. Все это позволяет писать более понятный код. Вот пример регулярки на Perl. Еще немного и мы сможем писать так и в JS. Возможно даже грамматики подвезут :)
Главное не забывать слова Jamie Zawinski: "Некоторые люди, когда сталкиваются с проблемой, думают «Я знаю, я решу её с помощью регулярных выражений.» Теперь у них две проблемы"

2️⃣ Поддеркжа приватных свойств в классах. Похожая идея реализована в Perl6. Мне подход с # нравится значительно больше, чем через ключевое слово "private" (как в TypeScript и других языках), поскольку мы везде по коду можем видеть приватное или публичное свойство мы используем, да и объявление лаконичнее.

3️⃣ Разделители в числах. Можно писать 10_000_000, явно легче читается чем 10000000. Это есть во многих языках, включая Perl :)

4️⃣ array.flat и array.flatMap. В Perl я привык, что map может вернуть больше элементов, чем было в изначальном массиве, с flatMap наконец-то это тоже можно сделать и в JS :)

5️⃣ Object.fromEntries (противоположность Object.entries)

6️⃣ Поддержка BigInt из коробки.

7️⃣ Стабильная сортировка в стандарте. Кому-то это важно, я всегда писал код опираясь на идею, что сортировка нестабильная.

8️⃣ Promise.allSettled в дополнение к Promise.all и Promise.race.

9️⃣ Множество различных фич по i18n и l10n. Из наиболее полезных штук - Intl.RelativeTimeFormat.

Также в докладе показали улучшения производительности:
✳️ С node8 до node12 промисы стали в 11 раз быстрее. И что интересно, что нативный async/await в два раза быстрее чем нативные промисы.
✳️ С Chrome 61 до Chrome 65 на 40-100% ускорение скорости парсинга JS на реальных сайтах (типа FB).

На сегодняшний день, JavaScript имеет самый быстрый райнтайм среди динамических языков.
Еще один доклад с Google.io -
Beyond Mobile: Building Flutter Apps for iOS, Android, Chrome OS, and Web (Google I/O'19)

Flutter - это такой себе аналог React Native от Google, где вы пишите все на Dart. На самом деле, архитектура совершенно другая.
Flutter, в отличии React Native, реализует все с нуля. Все рендерится на канвасе через OpenGL и выглядит идентично на всех платформах.

Представьте себе, что вы пишете OpenGL игру, а потом запускаете ее на iOS и Android, она выглядит одинаково там и там, поскольку вы все нарисовали сами на канвасе. То есть, ваше Android приложение на iOS будет выглядеть точно также, как и на Android. Естественно, это не всегда хорошо и вы хотите нативное поведение для платформы. В связи с этим, Google реализовывает 2 набора компонентов, 2 набора механик/физики/анимаций под обе платформы и вы можете на Android увидеть как будет работать ваше iOS приложение и наоборот.

Flutter имеет отличную производительность (Google целится на 120fps, но пока это 60fps). На Android и iOS Dart код компилируется в ассемблерные инструкции для ARM архитектуры. У вас нет медленного JSCore, как в случае с React Native, и асинхронного "моста".

Сам же высокоуровневый API Flutter очень похож на то, что у нас есть в React, только батарейки в комплекте :).

В двух словах суть доклада в том, что Google запускает Flutter для десктопа и для веба:
✳️ https://flutter.dev/desktop
✳️ https://flutter.dev/web

Сейчас в WebbyLab мы смотрим на эту технологию, хотим сделать 1-2 небольших проекта. Один из проектов будет для комьюнити и уйдет в OpenSource. Я уже сам успел поиграться - Developer Experience отличный, рекомендую тоже посмотреть, если вам интересна мобильная разработка.

Начните с того, что установите себе Flutter Gallery и пощелкайте UI и различные опции.
👍1
Меня сняли на скрытую камеру 😁

Позавчера был в Днепре и должен был выступать на RunIT. Во время завтрака в отеле неожиданно встретил Максима Безуглого (основателя "Internation Software Architect Club"), который также должен был выступать на той же конференции. С Максом и Мишей Корбаковым у нас завязалась небольшая дискуссия про архитектуру, graphql, REST. Получился спонтанный выпуск "Архитектура и Кофе" 🙂

ВИДЕО (20 минут): https://www.youtube.com/watch?v=DSwpL3s_eZA

Также рекомендую к просмотру весь цикл "Архитектура и Кофе" (неформальные обсуждения вопросов архитектуры с опытными разработчиками и архитекторами) - https://www.youtube.com/playlist?list=PL9lOYXXTeW0oE-H1fXrJ5tdLLUkMtWJtC
Много лет назад наткнулся на пост Бобука (Григорий Бакунов, был директором по разработке в Яндексе, сейчас директор по распространению технологий) о том, как развивается эмоциональное состояние человека по мере его профессионального роста. Я этот текст перечитавал уже много раз. Ни убавить, ни прибавить.

Это стоит прочитать - "Как закаляется сталь"
https://github.com/bobuk/addmeto.cc/blob/master/pages/2013-04-19.md
Сделал страницу на Github со списком моих докладов на предстоящих конференциях и с материалами по прошедшим (контент все еще добавляю). Если вдруг тоже окажетесь на какой-то из этих конференций, буду рад пообщаться 🙂

Обновляемый список моих докладов: https://github.com/koorchik/my-talks
Сейчас мы делаем несколько проектов для автоматизации умного дома. Софт на NodeJs, запускается в докер-контейнерах на наших хабах. Хабы это железяки по типу raspberry/orange pi. Флеш память приходится сильно экономить и стал вопрос оптимизации размера докер образов.

Если взять стандартный образ node:10 и сделать все без оптимизаций, мы получим образ в 700МБ или больше. Базовый образ будет 600 МБ и 100 МБ слой с приложением.

Ребята из компании решили поделиться опытом и написали отличный пост про оптимизацию размера докер образов для NodeJS приложений.

Статья: Docker image size optimization for your Node.js app in 3 easy-to-use steps.

Результаты:
Размеры образов до оптимизации:
* 600 МБ базовый образ с NodeJS
* 100 МБ на каждый новый образ

Размеры образов после оптимизации:
* 100 МБ базовый образ с NodeJS
* 2.5 МБ на каждый новый образ