Spense.app "под капотом". Код.
Пока Spense v0.2 в разработке, хочу рассказать о внутренней организации приложения с технической точки зрения. Статья эта в основном для
В двух словах
Бэкенд на Django (Python), фронтенд на Django-шаблонах и Bootstrap, с щепоткой JavaScript и местами htmx (уже нет).
Почему так скучно?
Звучит совсем не хайпово, да. Но стоит помнить, что Spense в текущем его состоянии - это не полноценный продукт. Это скорее прототип, в котором мне часто нужно что-то менять и проверять идеи. Если идеи сработают, я выкину этот код и напишу другой, а если не сработают, просто оставлю на память.
Так что Django я выбрал не потому что я ее очень люблю (на самом деле я ее ненавижу), а потому что за последний год я успел к ней привыкнуть, и прототипировать приложение мне с ней получается легко и быстро.
По части архитектуры бэкенда хвалиться пока нечем, всё ровно так, как обычно делается в Django-проектах. Код разбит на три Django-аппы:
history
- для моделей БД и кусочка логики про кошельки и транзакции;tags
- для моделей БД и кусочка логики вокруг тегов;app
- основные вьюхи с кучей логики внутри, HTML-шаблоны и статика.
Поначалу для меня это разделение имело смысл, но сейчас я скорее от этого страдаю, потому что до сих пор не могу запомнить, где какие модели лежат. Перемещать модели между Django-аппами - это отдельный геморрой, поэтому продолжаю страдать.
С фронтом все еще скучнее. Последний раз я писал более-менее сложный интерфейс на React в 2017 году, и после этого крайне редко касался современных JS фреймворков. Простого HTML мне хватало в большинстве случаев, и jQuery, как верный старый пёс, всегда приходил на помощь. Здесь я, правда, решил jQuery пока не вкатывать, а использовать вместо него "vanilla JS", но на практике выглядит всё похожим образом.
Например, вот кусочек про подгрузку "smart tags" - категорий расходов, которые Spense пытается угадать по введенной сумме:
{% block script %}
<script>
const $amount = document.getElementById('amount');
const $wallet = document.getElementById('wallet');
const $add = document.getElementById('add');
const $listSmartTags = document.getElementsByClassName('smart-tags-list')[0];
/* ... */
$amount.addEventListener('keyup', function (e) {
if (e.target.value) {
$add.removeAttribute("disabled")
updateSmartTags()
} else {
$add.setAttribute("disabled", "")
$listSmartTags.innerHTML = '';
}
})
/* ... */
function updateSmartTags() {
const walletId = $wallet.value;
const amount = $amount.value;
fetch(`{% url 'smart_tags' %}?wallet_id=${walletId}&amount=${amount}`)
.then(res => res.json())
.then(/* ... */)
/* ... */
}
</script>
{% endblock %}
{% url %}
внутри JS-строки вперемешку с вклеенными JS-переменными - 🤌Как видите, все довольно прямолинейно: скрипты я оставляю прямо в теге <script>
на странице, и внутри там почти все как в jQuery, даже префикс $
имеется, чтобы отличать DOM-элементы от других идентификаторов.
Ну а по части Bootstrap - пока это для меня просто выбор по умолчанию. Там вроде Tailwind набирает обороты, но я пока не согласен к нему привыкать. Хоть в моем HTML тоже проскакивает class="row border-top border-black border-opacity-10 pt-1"
или просто инлайновые стили в атрибуте style
, я стараюсь выносить их в отдельный CSS-класс, если их становится много. Tailwind для таких случаев официально рекомендует multi-cursor editing, и я даже не знаю, как это комментировать.
Работа с кодом
На базовом уровне все тривиально: один git-репозиторий, которых хостится на GitHub внутри моей "организации".
В репозитории настроено несколько pre-commit-хуков, включая ruff --fix
и ruff format
(я почти везде переехал с flake8
+black
на ruff
из-за того, что это очень шустрый швейцарский нож).
В самом начале, кстати, не было ни хуков, ни форматирования кода, писал и коммитил просто как есть. Типа, зачем мне какой-то кодстайл, если я это разрабатываю в одно лицо, и мне ни с кем не надо спорить об отступах и скобочках?
Но в прошлом году я понял, что это порождает некоторые трудности, и нужно хотя бы иметь базовую проверку кода и чистить неиспользуемые импорты. Ну а заодно и "причесывать" сам код, потому что против Black code style я вообще ничего не имею, и он мне даже нравится.
Так что в какой-то момент я всё отформатировал, починил, добавил хуки и воспользовался .git-blame-ignore-revs, чтобы git blame
эти коммиты игнорировал.
Тесты
Для тестов я использую pytest
+ pytest-django
, но тестов там сейчас очень мало. В основном получается так, что все фичи так или иначе связаны с интерфейсом, и я их просто руками протыкиваю по время разработки. А так как я еще и активный пользователь, то я автоматически становлюсь и ручным тестировщиком большинства фич. Конечно, код часто покрывает только happy path, с минимальной валидацией входных данных, но большего мне пока и не надо.
Зависимости
За Python-зависимости сейчас отвечает poetry
. Сначала было просто pip install -r requirements.txt
, пока зависимостей было штук 5, которые я два года не обновлял. Но довольно скоро стало понятно, что poetry
+pyproject.toml
гораздо приятнее и удобнее.
Для поддержания актуальности зависимостей я настроил бот Renovate, который находит новые версии вообще для всего, что у меня есть в репозитории. У Renovate есть как минимум два значимых отличия от dependabot:
- он умеет собирать минорные апдейты в один pull request,
- и он умеет сам его мёрджить (но не сразу, а когда у него есть настроение).
То есть в большинстве случаев от меня никаких действий вообще не требуется, разве что если CI на этом pull request ломается.
CHANGELOG.md
Я уже писал, что веду проект в GitHub Projects, но вдобавок я решил еще и записывать значимые изменения в CHANGELOG.md
. Например, для версии 0.1 список изменений выглядит так:
Кстати, существует множество разных инструментов для ведения чейнджлогов. Я попробовал scriv, и процесс генерации чейнджлога мне сначала показался приятным.
Допустим, для следующей версии проекта запланирован десяток фич, которые разрабатываются в разных ветках, вливаются в master
/main
, потом в какой-то момент проект тегается новой версией, и создается релиз. Scriv позволяет создавать в процессе разработки отдельные файлы в каталоге changelod.d
, по кусочку чейнджлога на каждую фичу. При этом каждый кусочек внутри себя может содержать и "Added", и "Removed", и "Fixed" секции. А когда нам нужно оформить релиз, мы запускаем scriv collect
, который красиво сливает эти кусочки в один, берет номер новой версии из pyproject.toml
и добавляет всё вместе к CHANGELOG.md
. Так был сгенерирован список к Spense 0.1 на скриншоте выше.
У этого подхода есть большой плюс, когда несколько разработчиков параллельно работают в разных ветках, и merge-конфликты в чейнджлоге в этом случае исключены. Но после своего релиза я увидел для себя большой минус: из строчки CHANGELOG.md
нельзя "прыгнуть" на коммит или pull request, в котором были реализованы указанные изменения, а можно только посмотреть на коммит с самим чейнджлогом:
git blame
бессиленПоэтому я решил просто редактировать CHANGELOG.md
по ходу разработки, так что для версии 0.2 и далее чейнджлог в сочетании с историей репозитория будет иметь двойную пользу: и расскажет о сути изменений, и укажет на сами изменения.
git blame
счастливВ общем и целом
Как видите, ничего сверхъестественного, но при этом все организовано так, чтобы у разработки был какой-то темп. Идей у меня много, а времени - пару вечеров да выходные, поэтому сейчас важно не заострять внимание на тех деталях, которые совсем не важны, но и не доводить текущий код до неподдерживаемого состояния.
Из всех проблем меня сейчас больше всего волнует отсутствие тестов, потому что я уже чувствую, что новые изменения совсем скоро начнуть ломать текущую функциональность. Поэтому надо хотя бы бэкендную часть покрыть тестами процентов на 80, чтобы я пореже ловил "Internal Server Error" после покупок в магазине.
Ну и это еще не весь рассказ о том, что сейчас "под капотом", так что напишу еще и про CI, и про инфраструктуру, и про то, как я иногда привлекаю AI для код-ревью, и про то, как недавно всё упало, и еще про что-нибудь.
Stay tuned!
Хорошо там, где нас нет
Иногда хочется поменять что-то в своей жизни: переехать в другой город, страну, сменить работу, род занятий, увлечение, религию....
Легкие деньги
Однажды в апреле 2013 года я уронил айфон в унитаз. То был 4s, работал для того времени сносно, а тут такое. Ну я его сразу суну...
🎙 Крафтовый Димарик №20 - Маленький, зато халявный
Как нам цену за квартиру поднимали Карьера бодибилдера завершилась, не начавшись С литовским пока всё Чуток про праздники и п...
Про мигрень
Послушал великолепную беседу с неврологом про головную боль. Всем советую, очень приятный чувак, объясняет классно, книжку напи...
Большая комната
Если много лет живешь в двушке, где люди спят в обеих комнатах, у тебя нет "спальни" и "гостиной". Есть "маленькая комната", "бо...
Учим немецкий
Герц (Гц) - это единица измерения частоты, сколько раз в секунду происходит какое-то событие. Названа в честь Генриха Герца ( He...