Всем привет, хочу поделиться результатом размышлений на тему — каким может быть современное веб-приложение. В качестве примера рассмотрим проектирование доски объявлений для комиксов. В некотором смысле рассматриваемый продукт рассчитан на аудиторию гиков и им сочувствующих, что позволяет проявить вольность в интерфейсе. В технической же составляющей, напротив, необходимо внимание к мелочам. По правде говоря, я ничего не понимаю в комиксах, но люблю барахолки, особенно в формате форума, какие были популярны в нулевых. Отсюда, допущение (возможно ложное), из которого проистекают последующие выводы, только одно — основной тип взаимодействия с приложением — просмотр, второстепенные — размещение объявлений и обсуждение. Нашей целью будет создание простого приложения, без технических ноу-хау лишних свистелок, однако соответствующее современным реалиям. Основные требования, соблюдения которых хотелось бы достичь: Серверная часть: а) Выполняет функции сохранения, валидации, отправки на клиент пользовательских данных
б) Вышеуказанные операции потребляют приемлемое количество ресурсов (времени в т. ч.)
в) Приложение и данные защищены от популярных векторов атак
г) Имеет простое API для сторонних клиентов и межсерверного взаимодействия
д) Кроссплатформенность, простое развёртывание Клиентская часть: а) Предоставляет необходимый функционал для создания и потребления контента
б) Интерфейс удобен для регулярного использования, минимальный путь до любого действия, максимальное количество данных на экран
в) Вне связи с сервером доступны все возможные в этой ситуации функции
г) Интерфейс отображает актуальную версию состояния и контента, без перезагрузок и ожидания
д) Перезапуск приложения не сказывается на его состоянии
е) По возможности переиспользуем DOM-элементы и JS-код
ж) Не станем использовать сторонние библиотеки и фреймворки в рантайме
з) Вёрстка семантична для доступности, парсеров и т. д.
и) Навигация по основному контенту доступна с помощью URL и клавиатуры На мой взгляд логичные требования, и большинство современных приложений в той или иной степени отвечают этим условиям. Посмотрим что получится у нас (ссылка на исходники и демо в конце поста). Языком для сервера будет golang. Простой, быстрый язык с отличной стандартной библиотекой и документацией… немного раздражающий. Первоначальный выбор пал на elixir/erlang, но т. к. go я уже знал (относительно), решено было не усложнять (да и необходимые пакеты были только для go).  Использование веб-фреймворков в go-сообществе не поощряется (обоснованно, стоит признать), мы выбираем компромисс и используем микрофреймворк labstack/echo, тем самым сокращая количество рутины и, как мне кажется, не много проигрывая в производительности.  В качестве базы данных используем tidwall/buntdb. Во-первых встроенное решение удобнее и уменьшает накладные расходы, во-вторых in-memory + key/value — модно, стильно быстро и нет нужды в кэше. Храним и отдаём данные в JSON, валидируя только при изменении.  На i3 второго поколения встроенный логгер показывает время выполнения для разных запросов от 0.5 до 10мс. Запущенный wrk на той же машине также показывает достаточные для наших целей результаты: Пакет comico/model разделён на три файла:
model.go — содержит описание типов данных и общие функции: создание/обновление (buntdb не различает эти операции и наличие записи мы проверяем вручную), валидация, удаление, получение одной записи и получение списка;
rules.go — содержит правила валидации конкретного типа и функции логирования;
files.go — работа с изображениями.
Тип Mtimes хранит данные о последнем изменении остальных типов в бд, таким образом сообщая клиенту какие данные изменились.  Пакет comico/bd содержит обобщенные функции взаимодействия с бд: создание, удаление, выборка и т. д. Buntdb сохраняет все изменения в файл (в нашем случае раз в секунду), в текстовом формате, что в некоторых ситуациях удобно. Файл бд не редактируется, изменения в случае успеха транзакции дописываются в конец. Все мои попытки нарушить целостность данных не увенчались успехом, в худшем случае теряются изменения за последнюю секунду.
В нашей реализации каждый тип соответствует отдельной БД в отдельном файле (кроме логов, которые хранятся исключительно в памяти и при перезагрузке обнуляются). Это обусловлено в большей степени удобством резервного копирования и администрирования, небольшой плюс — транзакция открытая на редактирование блокирует доступ только к одному типу данных.
Данный пакет может быть без особо труда заменён на аналогичный использующий другую базу данных, SQL к примеру. Для этого достаточно реализовать следующие функции: Пакет comico/cnst содержит некоторые константы необходимые во всех пакетах (типы данных, типы действий, типы пользователей). Помимо этого в этом пакете содержатся все человекочитаемые сообщения, которыми наш сервер будет отвечать во внешний мир. Пакет comico/server содержит информацию о роутах. Также, буквально в пару строк (спасибо разработчикам Echo), настроена авторизация с помощью JWT, заголовки CORS, CSP, логгер, раздача статики, gzip, автосертификат ACME и т. д.  * — обязательные поля
api — требует авторизации, pub — нет При get-запросе, не совпадающим с вышеперечисленным, сервер ищет файл в директории для статики (к примеру /img/* — изображения, /index.html — клиент).
Любая точка апи при успехе возвращает код ответа 200, при ошибке — 400 или 404 и краткое сообщение при необходимости.
Права доступа просты: создание записи доступно авторизованному пользователю, редактирование автору и модератору, редактировать и назначать модераторов может админ.
API снабжено простейшим антивандалом: действия логируются вместе с id и IP пользователя, и, в случае частого обращения, возвращается ошибка с просьбой немного подождать (полезно против подбора пароля). Мне нравится концепция реактивного веб'а, считаю что большинство современных сайтов / приложений стоит делать либо в рамках этой концепции, либо полностью статичными. С другой стороны несложный сайт с мегабайтами JS-кода не может не удручать. На мой взгляд эту (и не только) проблему сможет решить Svelte. Этот фреймворк (или скорее язык построения реактивных интерфейсов) не уступает в необходимом функционале тому же Vue, но обладает неоспоримым преимуществом — компоненты компилируются в ванильный JS, что сокращает как в размер бандла, так нагрузку на виртуальную машину (bundle.min.js.gz нашей барахолки занимает скромные, по нынешним меркам, 24КБ). Подробности вы можете узнать из официальной документации. Выбираем для клиентской части нашей барахолки SvelteJS, желаем всяческих благ Rich Harris и дальнейшего развития проекту! PS Никого не хочу обидеть. Уверен, что для каждого специалиста и каждого проекта подходит свой инструментарий. Используем для навигации. Не будем имитировать многостраничный документ, вместо этого используем hash страницы с query-параметрами. Для переходов можно использовать обычный <a> без js. Разделы соответствуют типам данных: /#goods, /#posts, /#users.
Параметры: ?id=идентификатор_записи, ?page=номер_страницы, ?search=поисковый_запрос.  Несколько примеров:  Функции парсинга и формирования урл в нашей реализации выглядят так: Для обмена данными с сервером будем использовать fetch api. Для загрузки обновлённых записей через небольшие промежутки времени делаем запрос к /pub/mtimes, если время последнего изменения для какого-либо типа отличается от локального, загружаем список этого типа. Да, можно было реализовать уведомление об обновлениях через SSE или WebSocket'ы и инкрементную подгрузку, но в данном случае обойдёмся без этого. Что у нас получилось: Для фильтрации и пагинации используем вычисляемые свойства Svelte, основываясь на данных навигации. Направление вычисляемых значений таково: items (массивы записей приходящие от сервера) => ignoredItems (отфильтрованные записи на основе списка игнора текущего пользователя) => scribedItems (отфильтровывает записи по списку подписок, если такой режим активирован) => curItem и curItems (вычисляет текущие записи в зависимости от раздела) => filteredItems (фильтрует записи в зависимости от поискового запроса, если запись одна — фильтрует комментарии к ней) => maxPage (вычисляет количество страниц из расчета 12 записей/комментариев на страницу) => pagedItem (возвращает конечный массив записей/комментариев на основе номера текущей страницы).  Отдельно вычисляются комментарии и изображения (comments и _images), группируясь по типу и записи-владельцу.  Вычисления происходят автоматически и только при изменении связанных данных, промежуточные данные постоянно находятся в памяти. В связи с этим делаем малоприятный вывод — для большого количества информации и/или частого её обновления может расходоваться большое количество ресурсов. Согласно решению делать offline-приложение, реализуем хранение записей и некоторых аспектов состояния в localStorage, файлов изображений в CacheStorage. Работа с localStorage предельно проста, договоримся что свойства обладающие префиксом "_" при изменении автоматически сохраняются и восстанавливаются при перезагрузке. Тогда наше решение может выглядеть так: С файлами немного сложнее. В первую очередь воспользуемся списком всех актуальных файлов (с временем создания), приходящим от сервера. При обновлении этого списка сравниваем со старыми значениями, новые файлы помещаем в CacheStorage, устаревшие удаляем оттуда: Затем нужно переопределить поведение fetch так, чтобы файл брался из CacheStorage без коннекта к серверу. Для этого придётся воспользоваться ServiceWorker'ом. Заодно настроим сохранение в кэш других файлов для работы вне связи с сервером: Выглядит немного коряво, но свои функции выполняет. Структура компонентов:
index.html | main.js
== header.html — содержит логотип, строку состояния, главное меню, нижнее навигационное меню, форму отправки комментария
== aside.html — является контейнером для всех модальных компонентов
==== goodForm.html — форма добавления и редактирования объявления
==== userForm.html — форма редактирования текущего пользователя
====== tableForm.html — фрагмент формы для ввода табличных данных
==== postForm.html — форма для поста форума
==== login.html — форма логина/регистрации
==== activity.html — отображает комментарии обращенные текущему пользователю
==== goodImage.html — просмотр основного и дополнительных фото объявления
== main.html — контейнер для основного содержимого
==== goods.html — карточки списка или одиночного объявления
==== users.html — то же для пользователей
==== posts.html — думаю, понятно
==== cmnts.html — список комментариев к текущей записи
====== cmntsPager.html — пагинация для комментариев Для управления с помощью клавиатуры задействуем следующие комбинации:
Alt+s / Alt+a — переключает страницу записей вперёд / назад, для одной записи переключает страницу коментариев.
Alt+w / Alt+q — осуществляет переход ко следующей / предыдущей записи (если таковые существуют), работает в режиме списка, одной записи и просмотра изображения
Alt+x / Alt+z — прокручивает страницу вниз / вверх. В режиме просмотра изображений переключает изображения вперёд циклично / назад
Escape — закрывает модальное окно, если открыто, возвращает к списку, если открыта одиночная запись, отменяет поисковый запрос в режиме списка
Alt+c — фокусирует на поле поиска или ввода комментария, в зависимости от текущего режима
Alt+v — включает / отключает режим просмотра фото для одиночного объявления
Alt+r — открывает / закрывает список входящих комментариев для авторизованного пользователя
Alt+t — переключает светлую / тёмную темы оформления
Alt+g — список объявлений
Alt+u — пользователей
Alt+p — форум
Знаю, во многих браузерах эти сочетания используются самим браузером, однако для моего хрома я не смог придумать что-то удобнее. Буду рад вашим предложениям. Помимо клавиатуры конечно же можно использовать консоль браузера. Для примера — store.goBack(), store.nextPage(), store.prevPage(), store.nextItem(), store.prevItem(), store.search(stringValue), store.checkUpdate('goods'||'users'||'posts'||'files'||'cmnts') — делают то что подразумевает из название; store.get().comments и store.get()._images — возвращает группированные файлы и комментарии; store.get().ignoredItems и store.get().scribedItems — списки игнорируемых и отслеживаемых вами записей. Полный список всех промежуточных и вычисленных данным доступен из store.get(). Не думаю что это всерьёз может кому-то понадобиться, но, к примеру, отфильтровать записи по пользователю и удалить мне показалось вполне удобно именно из консоли. На этом знакомство с проектом можно закончить, больше подробностей вы можете найти в исходных текстах. Как итог у нас получилось довольно быстрое и компактное приложение, в большинстве валидаторов, чекеров безопасности, скорости, доступности и т. п. показывает высокие результаты без целенаправленной оптимизации.
Хочется узнать мнение сообщества насколько оправданно использованные в прототипе подходы к организации приложений, какие могут быть подводные камни, что бы вы реализовали принципиально по-другому?
Исходный код, примерная инструкция по установке и демо по ссылке (просьба вандалить тестировать в рамках УК РФ).  Постскриптум. Немного меркантильного в завершение. Подскажите с таким уровнем реально начать программировать за деньги? Если нет, на что обратить внимание в первую очередь, если да, подскажите где сейчас ищут интересную работу на схожем стеке. Спасибо. Постпостскриптум. Ещё немного о деньгах и работе. Как вам такая идея: предположим человек готов работать над интересным ему проектом за любую з/п, однако данные о задачах и их оплате будут доступны публично (желательна доступность и кода для оценки качества исполнения), в случае если оплата будет существенно ниже рынка конкуренты работодателя могут предложить большие деньги за выполнение их задач, если выше — многие исполнители смогут предложить свои услуги по меньшей цене. Не будет ли такая схема в некоторых ситуациях более оптимально и справедливо балансировать рынок (IT)? Молодой чемодан