Здравствуйте, меня зовут Дмитрий Карловский и я.. как 30 лет назад вошёл в IT, и всё никак не могу из него выйти. И всё это время я не только работал работу, но и точил свой набор топоров, благодаря которому путь стартапа от идеи до прода сокращается с минимум недели до максимум часа.
Заинтригованы? Тогда следите за руками внимательно, иначе пропустите как мы во мгновение ока нарисуем полноценный фронтенд без чат-ботов, поднимем базу данных без сервера, задеплоим всё на статический сервер без боли, и будем наслаждаться результатом без тормозов.
![](https://habrastorage.org/getpro/habr/upload_files/230/1cb/8e5/2301cb8e536090ef0cdc48e914938b50.jpeg)
Evrone Meetup
Это - текстовая версия доклада с Evrone <Frontend> Meetup. Можете глянуть видео запись с живыми запинками, а можете читать далее с более выверенными формулировками.
Дмитрий Карловский
Кто я такой, чтобы меня слушать? Книжек я не писал, так что никто. Однако, я широко известен в узких кругах, как мастер спорта по велосипедостроению.
Дмитрий Карловский
30 лет в IT, но всё ещё не скуксилсяЯндекс, 1C, Газпром
Wrike, SAPRUN, Deutsche Bank10+ докладов, 70+ статей, 60+ видео
ОРП, ФТ, Tree, MT, HARP
MAM, mol, HyOO, CRUS🦿DB
Выделенные жирным велосипеды мы и будем далее использовать.
SlowDating
Начнём с лирического отступления. Недавно я познакомился с одной околоайтишницей и позвал её на свидание. Оно прошло прекрасно, как мне показалось, но были какие-то противоречивые сигналы.
![](https://habrastorage.org/getpro/habr/upload_files/0a7/49c/ba1/0a749cba144b0e1701234f337210affc.jpeg)
И захотел я получить о неё обратную связь. Ведь многие проблемы человечества возникают всего лишь из-за недопонимания.
Сервис сбора обратной связи
Но решить проблему здесь и сейчас - слишком мелкая задача. Так что решил я сделать целый сервис для сбора отзывов, который могли бы применять все в самых разных контекстах.
И тут вы, наверно, спросите меня: "А чего бы просто не создать Гугл Яндекс форму?". Ну, помимо наличия у них фатального недостатка, этими набившими оскомину формами уже никого не впечатлишь. А вот собственным, поднятым в кратчайшие сроки, сервисом - есть шанс. Кроме того..
💡 Мы же спортсмены
💡 Сапожник должен есть свои сапоги
💡 Можно сделать гораздо лучше
Рабочее окружение в один клик
Итак, приступим. Самый простой способ поднять рабочее окружение - это пройти по ссылке на облачную среду разработки.
https://gitpod.io/#https://github.com/hyoo-ru/mam
Там уже есть VSCode со всеми нужными настройками и плагинами, так что можно сразу приступать к написанию кода. Но это не спортивно..
Старт проекта с нуля
Лучше всё же развернуть рабочее окружение локально и не зависеть от сети. Благо делается это тоже крайне просто - один раз в жизни создаём пустую директорию, запускаем в ней сервер разработчика и всё готово.
mkdir mam
cd mam
npm exec mam
start http://localhost:9080/my/survey/app/-/test.html
Обратите внимание на последнюю строчку - она просто открывает в браузере точку входа в наше приложение. Сборщик отслеживает все её зависимости и автоматически скачивает и подключает их по мере использования, а при изменении кода автоматически пересобирает бандлы и перезапускает приложение.
Подробнее про MAM
Я подробно рассказывал про принципы разработки в нашей экосистеме в отдельной статье..
![](https://habrastorage.org/getpro/habr/upload_files/4d5/918/1f7/4d59181f7bc331c697bb3258c8ddffb8.png)
Так что подробно на этом мы сейчас останавливаться не будем.
Прикладная область
Итак, мы уже готовы писать код, но про что именно мы его будем писать? Давайте опишем нашу предметную область..
![](https://habrastorage.org/getpro/habr/upload_files/189/6cf/c4d/1896cfc4da91000c8e79124249bfed2a.png)
Любой пользователь нашего сервиса является персоной (Person
). У каждого из них есть список встреч (Meet
), у каждой из которых есть список отзывов (Opinion
). Вроде бы не сложно, да?
Как это обычно делают
Давайте представим, как это всё выглядело бы с традиционным подходом. Берётся реляционная СУБД. В ней создаётся пяток табличек и десяток индексов. Работать с SQL напрямую не удобно, поэтому обмазываются ORM. Работать напрямую с СУБД не безопасно, поэтому прячут её за API сервером. На клиенте работать напрямую c API не удобно, поэтому придумываются какую-нибудь клиентскую обвязку. А чтобы он с сервером не подрался, то прикручивают какую-нибудь изоморфную схему данных с кодо-генераторами, доко-генеараторами и прочими валидаторо-нормализаторами. И это не говоря о куче инфраструктурных вещей, о которых надо как минимум подумать, а как максимум реализовать, проверить, что они работают как следует, завернуть в кубер-докеры и, наконец, задеплоить в кластере.
🕛 5+ табличек и 10+ индексов
🕐 Object-Relation Mapping
🕑 API сервер
🕒 Изоморфная схема
🕔 Клиентская обвязка
🕓 Бэкапы и репликация
🕖 Аутентификация и авторизация
🕗 Всё это в контейнеры
Короче, всё это за часок, пока едете домой, вы точно не успеете.
Структура базы данных в CRUS🦿DB
Мы же будем использовать децентрализованную, безопасную, неубиваемую базу данных CRUS🦿DB, которая живёт прямо в браузерах на клиентских устройствах, и без конфликтов синхронизируется через любые каналы связи. В ней используется не реляционная алгебра, а графовая модель, поэтому предметная область ложится на неё практически 1-в-1.
![](https://habrastorage.org/getpro/habr/upload_files/f2b/a54/e3c/f2ba54e3c1665ac036447dc97216239d.png)
Обратите внимание, что на схеме у нас теперь отражены ещё и политики доступа. Данные персоны будут зашифрованы и доступны только самой персоне. Данные встречи читать смогут все, у кого есть на неё ссылка. Содержимое отзыва будет доступно лишь его автору и владельцу встречи. Плюс появилась дополнительная сущность - отзывы (Opinions
), куда любой пользователь сможет прилинковать свой отзыв.
Схема базы данных
Чтобы формализовать схему предметной области, переложим её в код на TypeScript, и получим модель, с которой уже можно будет работать.
class $my_survey_person extends Dict.with({
Meets: List_ref( ()=> $my_survey_meet ),
}) {}
class $my_survey_meet extends Dict.with({
Title: Str,
Owner: Ref( ()=> $my_survey_person ),
Opinions: Ref( ()=> $my_survey_opinions ) ),
}) {}
class $my_survey_opinions extends Dict_to(
Ref( ()=> $my_survey_opinion )
) {}
class $hyoo_survey_opinion extends Dict.with({
Descr: Text,
}) {}
Через неё уже можно будет читать и писать в базу данных, она будет синхронизироваться и тд.
Возможно вы обратили внимание, что Title
и Descr
имеют разные типы, хотя и там, и там хранятся буковки. Str
в данном случае - это атомарная строка, которая хранит лишь наиболее свежее значение, стирая все предыдущие. А вот Text
- это более хитрый тип данных, который автоматически мёржит изменения.
Когда вы вводите одну букву, другим пирам посылается не весь текст, а маленькая дельта, которая применяется к другому тексту без потерь уже внесённых в него изменений. По научному это называется dCvRDT, и лежит оно в основе всей CRUS🦿DB.
Создание новой встречи
Давайте немного расширим нашу модель методами, позволяющим сделать работу с нашей предметной областью ещё более удобной. Например, добавим в персону метод для создания новой встречи.
class $my_survey_person extends Dict.with({
Meets: List_ref( ()=> $my_survey_meet ),
}) {
@act meet_make() {
const meet = this.Meets(null)!.remote_make({ '': Rank.get })!
meet.Owner(null)!.val( this.land().auth().lord() )
return meet
}
}
Тут мы берём объявленное нами в схеме свойство `Meets` и просим его добавить в список ссылку на свежесозданную встречу с правами на чтение для всех подряд. А потом устанавливаем в ней ссылку на владельца - текущего пользователя. `null` в аргументах тут означает автоматическое создание записи для свойства, если его ещё нет.
Получение своего мнения
А теперь добавим во встречу метод получения модели мнения текущего пользователя. Это уже чуть более сложный код, так как нам нужно позаботиться о конфиденциальности.
export class $my_survey_meet extends Dict.with({ ... }) {
@mem opinion_my() {
const my_auth = this.land().auth()
const owner_key = this.Owner()?.remote()?.land().key()
if( !owner_key ) return null
const opinions = this.Opinions(null)?.remote_ensure({ '': Rank.add })
const opinion = opinions?.key( my_auth.peer(), null )?.remote_ensure({
[ owner_key.toString() ]: Rank.get,
[ my_auth.public().toString() ]: Rank.law,
}) ?? null
return opinion
}
}
Тут мы видим три логических блока. Сперва мы получаем авторизацию текущего пользователя и публичный ключ владельца встречи. Потом мы убеждаемся, что у встречи есть ссылка на словарь с правами на добавления всем подряд ссылок на свои мнения. И на конец, убеждаемся, что в этом словаре есть мнение текущего пользователя с правами на чтение для владельца встречи и полными правами для текущего пользователя.
Получение всех мнений
Для модели встречи нам далее пригодятся ещё пара методов: получение списка идентификаторов респондентов из ключей словаря со ссылками на мнения, и получение модели мнения по идентификатору респондента.
@mem responder_refs() {
return this.Opinions()?.remote()?.keys()
.map( $hyoo_crus_vary_cast_ref )
.filter( $mol_guard_defined ) ?? []
}
@mems opinion( responder: $hyoo_crus_ref ) {
const options = this.Opinions()?.remote()
return options?.key( responder.description! )?.remote() ?? null
}
Всё, API готово!
Всё, наша умная модель полностью завершена и может использоваться где угодно.
Мы не возились с языками запросов, и не боролись с SQL и HTTP инъекциями. Всё, что нам потребовалось - это строго типизированное API на том же языки, на котором мы пишем и клиент. Причём написали мы исключительно прикладной код, без бойлерплейта связанного с проверками безопасности, обеспечения надёжности и своевременным обновлением и тд.
Нам не пришлось поднимать сервера, базы данных, контейнеры и тп. Более того, мы не написали ни строчки серверного кода - всё исполняется на клиенте. То есть мы получили настоящую бессерверность, а не то, что маркетологи продают нам как serverless, за которым скрываются на самом деле облачные серверные функции.
Наконец, мы можем не беспокоиться о любой компрометации серверов: будь то хоть слив данных, хоть троян-шифровальщик. Никакой узел нашей распределённой не доверяет никакому другому, и автоматически всё проверяет, используя криптографию.
🕛 Не надо поднимать СУБД
🕐 Не надо писать код сервера
🕑 Не надо настраивать бэкапы и репликацию
🕒 Не надо делать аутентификацию и авторизацию
🕓 Не надо изолировать периметр
Да какое API!
Кроме того, мы из коробки получаем уникальные фичи, которые вы мало где вообще встретите. Например, динамическая, но строгая типизация, которой мы действительно можем доверять.
Или независимость от сети - мы можем как читать, так и писать данные в базу даже в не ограниченно долгом офлайне. А при появлении сети изменения разных пиров сливаются без конфликтов благодаря авторским CRDT алгоритмам. Причём делают они это в реальном времени благодаря синхронизации маленькими дельтами.
Наконец, положенные в основу базы данных E2E-шифрованиие и цифровые подписи для всех изменений дают беспрецедентную безопасность. Это не как в Телеграм, где по умолчанию все ваши переписки доступны и на чтение, и на запись одному Дурову известно какому кругу лиц, а для включения e2e-шифрования надо создавать отдельный неудобный секретный чат. В CRUS🦿DB всё по взрослому - вы сами контролируете кто и какой доступ имеет к вашим приватным данным.
✨ Строгая типизация
✨ Работа офлайн
✨ Синхронизация в реальном времени
✨ Слияние без конфликтов
✨ End-to-End шифрование
Форма сбора отзывов
Ну что ж, приступим, наконец, к написанию интерфейса. Сделать это быстрее и проще, конечно же, на $mol.
Используя язык view.tree опишем основную страницу нашего приложения - форму сбора обратной связи.
$my_survey_meet_form $mol_page
meet $my_survey_meet
-
Title $mol_string_button
value? <=> title? \
hint @ \Meet title
-
body /
<= Bid $mol_text
<= Opinion_my $mol_textarea
<= Opinions $mol_section
Стандартный Title
, выводящийся в шапке $mol_page
, нас не устроил, поэтому мы заменили его на поле строкового ввода, которое связали со свойством title
, которое уже через TS свяжем с соответствующим свойством модели встречи, которую мы будем получать из вне.
@mem title( next?: string ) {
return this.meet()
.Title( next )?.val( next ) ?? ''
}
А в теле страницы мы выводим три блока, которые детализируем чуть позже. Но уже сейчас интерфейс работоспособен, хоть ничего и не сохраняет.
Побуждение к действию
Первый блок поясняет респонденту, что мы от него хотим. Пояснения мы оформляем используя легковесную разметку MarkedText, которая быстро и безопасно визуализируется благодаря компоненту $mol_text
.
Bid $mol_text text @ \
\//Constructive feedback makes the world a better!// Tell:
\💗 What did you **like**?
\📌 What could be done **better**?
\🙏 More ideas and **suggestions** for the future?
Редактирование отзыва
Следующий блок - умная $mol_textarea
для ввода своего мнения, которую мы связываем с соответствующим полем модели мнения.
Opinion_my $mol_textarea
value? <=> opinion_my? \
@mem opinion_my( next?: string ) {
return this.meet().opinion_my()
?.Descr( next )?.text( next ) ?? ''
}
Умная она потому, что умеет в ленивый рендеринг, подсветку синтаксиса, распознавание ссылок, отображение номеров строк, богатый набор хоткеев и тд.
Список отзывов
Ну и в третьем блоке для владельца встречи нам нужно вывести все отзывы по ней. Для этого мы воспользуемся секцией $mol_section, под заголовком которой выведем вертикальный список мнений, каждое из которых визуализируем опять же через $mol_text
, то есть в отзывах тоже можно будет пользоваться легковесной разметкой текста.
Opinions $mol_section
title @ \Opinions
content <= opinions /
<= Opinion*0 $mol_text
text <= opinion* \
@mem opinions() {
return this.meet().responder_refs()
.map( person => this.Opinion( person ) ) ?? []
}
@mems opinion( responder: $hyoo_crus_ref ) {
return this.meet().opinion( responder )
?.Descr()?.text() ?? ''
}
Если вы обратили внимание, то порой перед строками во view.tree можно встретит собачку - это значит, что данная строка не просто константа, а локализуемая строка. А это значит, что сборщик, увидев это, вырезает текст в отдельный JSON файл с английскими текстами, оставляя в коде лишь обращение к локализации по человеко-понятному глобально-уникальному идентификатору.
Локализация
Положим рядом JSON с текстами на русском и любом другом языке, чтобы добавить поддержку и их. Нужный JSON загружается динамически на основе настроек браузера с фолбэком до английского, взятого из исходного кода.
my/survey/form/form.locale=ru.json
{ "$hyoo_survey_meet_form_Title_hint": "Название встречи", "$hyoo_survey_meet_form_Bid_text": "//Конструктивная обратная связь делает мир лучше!// ...", "$hyoo_survey_meet_form_Opinions_title": "Отзывы" }
Общая обвязка
Осталось дело за малым - сделать обвязку, чтобы у нас было меню со своими встречами и прочее. Для этого проще всего воспользоваться компонентом $mol_book2_catalog
, который рисует типовую книжку из двух страниц: меню и развёртка для выбранного пункта.
$my_survey_app $mol_book2_catalog
realm $hyoo_crus_realm
param \meet
Spread* <= Meet* $my_survey_meet_form
meet <= meet* $my_survey_meet
Placeholder $mol_page
title @ \🤗 Graceful Feedback
Объявленный в начале realm
- это, собственно, вся база данных. param
задаёт имя параметра в урле, в котором будет храниться идентификатор текущей развёртки. В Spread
мы указываем, что в качестве развёртки надо использовать описанную нами ранее страницу встречи, которой передаём модель встречи. А в Placeholder
, заполняющий оставшееся свободное место на экране, отобразим страничку с дополнительной информацией.
Теперь провяжем объявленные свойства друг с другом, используя тайпскрипт..
@mem profile() {
return this.realm().home()
.hall_by( $my_survey_person, {} )
}
@mem spread_ids() {
return this.profile()
?.Meets()?.remote_list()
.map( meet => meet.ref().description! ) ?? []
}
@mems meet( id: string ) {
const ref = $hyoo_crus_ref( id )
return this.realm()
.Node( ref, $my_survey_meet )
}
Тут мы получаем профиль текущего пользователя как экземпляр модели персоны. Если его ещё нет, то он будет создан автоматически с правами доступа никому, кроме автора. А это значит, что он будет автоматически зашифрован.
Потом мы формируем список идентификаторов развёрток для меню. И потом уточняем, как по этим идентификаторам получать из базы экземпляры встреч.
Первый результат
Наконец, запустив своё приложение, мы с удивлением обнаруживаем, что оно.. работает. Можно создавать встречи, делиться ссылками, оставлять отзывы и видеть как они прилетают в реально времени.
![](https://habrastorage.org/getpro/habr/upload_files/65e/f54/395/65ef54395a166a1c57fe06b566ec6421.png)
Но показывать его кому-то ещё немного стыдно. Тут не выровнено, там поехало.
Наведение красоты
Давайте отполируем визуал, используя статически типизированные каскадные стили - фичу, которая есть только в $mol..
$mol_style_define( $my_survey_meet_form, {
flex: {
basis: `40rem`,
},
margin: [ 0, 'auto' ],
Opinion: {
margin: $mol_gap.block,
background: {
color: $mol_theme.card,
},
},
} )
Тут мы подцепляем к нашей форме обратной связи и его содержимому дополнительные стили, помимо тех, что там уже есть. При равной специфичности, эти стили будут иметь больший приоритет. А тайпскрипт проверит, что Opinion
, на который мы навешиваем стили, действительно существует на нашей странице.
Аналогичным образом мы поправим стили и для всего приложения..
$mol_style_define( $hyoo_survey_app, {
Menu: {
flex: {
basis: `20rem`,
},
},
Placeholder: {
flex: {
basis: `20rem`,
},
},
} )
Статически типизированные каскадные стили
Подробно я рассказывал про нашу систему стилизации в докладе Продвинутый CSS-in-TS на PiterJS #46.
Результат
Ну что ж, вот и финальный результат.
![Вид респондента на мобилке, вводящего отзыв. Вид респондента на мобилке, вводящего отзыв.](https://habrastorage.org/getpro/habr/upload_files/939/5fe/772/9395fe772ce29f72ca00788f3ba375c1.png)
![Вид владельца встречи, смотрящего отзывы на десктопе. Вид владельца встречи, смотрящего отзывы на десктопе.](https://habrastorage.org/getpro/habr/upload_files/047/a9f/310/047a9f310bd0bc317a00b70125b7235d.png)
Добавляем офлайн
Остался маленький штрих. Хотя база данных и работает в офлайне, веб приложения из офлайна не запускаются, пока не установишь специальный сервис-воркер, раздающий статику. Делается у нас это буквально одной строчкой кода..
my/survey/app/app.meta.tree
include \/mol/offline/install
Деплой на сервер
После сборки у нас получается набор бандлов, который можно выложить на любой статический хостинг. Для примера, давайте выложим всё на GitHub Pages. Для этого добавим добавим файл с настройками пайплайна..
# my/survey/.github/workflows/my_survey_app.yml
steps:
- uses: hyoo-ru/mam_build@master2
with:
package: 'my/survey'
modules: 'app'
- uses: hyoo-ru/gh-deploy@v4.4.1
if: github.ref == 'refs/heads/master'
with:
folder: 'my/survey/app/-'
Тут у нас два экшена: собственно сборка проекта под ключ, и выкладка его нас CDN гитхаба. Далее самое долгое - ожидание когда же раскатится DNS и сгенерируется HTTPS сертификат. Об этом, конечно, стоило позаботиться до начала разработки, но нам уж очень не терпелось поскорее взяться за код.
Статистика
Ну что ж, пришло время оценить, что у нас получилось.
10 Files
300 SLOC
100 KB bundle
Мы написали 300 строк кода - по 30 на файл в среднем. Всё это с зависимостями весит сотню килобайт. Казалось бы много, пока не вспомнишь, что там внутри собственная база данных, системы реактивности и виртуального рендеринга, визуализатор легковесной разметки, умная текстария и много других, на первый взгляд не заметных, но необходимых вещей. И при этом даже на мобилке полностью запускается всего за 2 секунды, что быстрее чем 99% используемых вами приложений, которые хорошо если хоть шапку покажут за это время.
![](https://habrastorage.org/getpro/habr/upload_files/050/195/d5f/050195d5f6af1ef80679ab01c6f77e88.png)
Финальный твист
И вот, я такой довольный собой беру ссылку на форму обратной связи, отсылаю девушке, а она.. ответила, что обратную связь лучше давать лично, и даже не стала пробовать моё творение. Было очень обидно. Надо ли говорить, что второго свидания у нас с ней уже не было, и обратную связь я так и не получил?
![](https://habrastorage.org/getpro/habr/upload_files/bf8/356/2be/bf83562bed11a63a4e53d47e9c115402.jpeg)
Эпилог
Так что я всё ещё без девушки, зато с новым стартапом! И чтобы попробовать его в деле, предлагаю вам оставит мне отзыв на это моё выступление по этой ссылке..
![](https://habrastorage.org/getpro/habr/upload_files/dff/eae/ff8/dffeaeff84c63b82bdfcc90dd7b96871.png)
survey.hyoo.ru - отзыв на этот доклад
mol.hyoo.ru, mam_mol - про фреймворк и веб
crus.hyoo.ru - про БД и платформу
h_y_o_o - обсуждение децентрализации
mol_news - новости о проектах
jin.hyoo.ru - обо мне и от меня
boosty.to/hyoo - поддержка рублём
На портале мола вы найдёте целый кладезь информации о веб разработке. И, в частности, про $mol. Та же картинка и с ютуб каналом, где я разбираю разные веб-технологии, и рассказываю, почему они нашим в подмётки не годятся.
На странице CRUS🦿DB вы найдёте подробности о базе данных - она уже почти готова к продакшену, но нужно ещё немного потерпеть. А вот пробовать своими руками и давать обратную связь уже можно и нужно.
Вскоре там появится информация и о разрабатываемой на её базе платформе, в рамках которой планируется куча ультра-соверменных, супер-реактивных синергетически-интегрированных ортогональных веб-сервисов, которые закроют все нужды людей. И всё это совершенно бесплатно, без рекламы и тёмных практик.
Если хотите поучаствовать в этой маленькой революции, то поднимайте свои сервера синхронизации и участвуйте в поддержании устойчивости сети. Поддержите нас звонкой монетой. Ну или присоединяйтесь к обсуждению децентрализованных систем на форуме Гипер Дев.
Наконец, на моей личной страничке вы найдёте информацию обо мне и различные материалы от меня: от хардкорных статей о разработке, до фоточек и стихов. Кстати, создана она в сервисе для ведения баз знаний, вот уже больше года крутящемся на прототипе CRUS🦿DB, так что скоро нам предстоит увлекательный переезд.
А на этом пока что всё, следите за новостями.
Актуальный оригинал на $hyoo_page.
![](https://habrastorage.org/getpro/habr/upload_files/921/a3b/88f/921a3b88f1101ed724f1e722ed2447aa.png)