Делаю личную панель тех задач, которыми пользуюсь часто, и хочу уместить все сразу в 1 месте, чтобы не бегать по млн. вкладок.
на 90% я фронт, но думаю изучать бек на питоне, и думал быть фуллстак (думал начать раст или го или c#, но пока питон побеждает для тех задач, которыми я пользуюсь, и в скорости языка пока не особо нуждаюсь)
в прогу засунул
1) профиль - с личной статистикой своей продуктивностью (время работы, заявки и тд)
2) аи чат - направлен именно под проекты, чтобы выбрать свой проект, и подключить любую модель с любого провайдера (opanAI, claude, и тот же openrouter с моделями бесплатными и тд.)
В панели сразу можно смотреть забитость окна, потраченные $, время генерации, и тд
- Режим 1 - Обычный мод, где выбрать можно будет 1 модель, и она будет отвечать
- Режим 2 - Коллективный, где можно выбрать 2 модели, которые думают, и судья выберет лучший ответ, и дополнит его если надо (Думаю улучшить этот мод, чтобы не 3 модели было, а по + можно было добавить сколько угодно)
- Так же у выбранного проекта есть кнопка перехода в режим IDE, где я сделал можно сказать vs code, чтобы сразу можно было перейти в чать какую-то отрезка, и сразу просмотреть или отредактировать.
3) vpn - просто пока что, потому что недавно сделал, где есть статус подключения, сервера, кнопка подключения (я на своем сервере развернул 3x-ui, и подключаюсь к своему личному серверу, но я столкнулся с таким приколом, что впн порой плохо работает, и я пока не понимаю, как мне улучшить его, чтобы он был качественным, и быстрым)
4) запрет - альтернатива впн, порой когда он не требуется, то просто включить запрет. У я хочу сделать, чтобы по кнопке rotate, у меня шел запрос на github автора, и если там есть новые изменения, то они будут скачиваться ко мне на бек (типа как обновление), чтобы я быстро получал актуальную сборку по 1 клику, и запускал.
P.S
Я знаю, что автора пока что забанили на время, как он говорит, поэтому я просто пока узнаю можно ли так сделать, если это не мой личный репозиторий.
5) Хранилище - когда я доделаю прогу, и отправлю бек на сервак, то он еще будет как хранилище, где я смогу создать папку к примеру для видео, png или лого для сайтов и тд.
И идея заключается в том, если я понимаю, что мне надо будет в другое место пойти не у своего рабочего пк, а с ноутом, то я сомгу зайти в совою прогу, и с сервера забрать что-то нужное.
Так же идея расширения функционала, это после завершения создания, сделать урезанную версию, и скинуть другу, чтобы он то же использовал хранилище, где ключевой фишкой этой вкладки будет общая папка, и добавить человека, где я смогу что-то закидывать (к примеру моды для майна), и он зайдет в эту папку, и скачает от туда их.
6) Заметки - думаю тут не особо есть, что говорить, просто я думаю расширить это для более значимого что-то, где будет отслеживание прогрессов, анализ ии, и цели.
7) Заявки - т.к я порой беру заказы в тг каналов, то я хочу автоматизировать процесс, где бот заходит на мой акк, и делает рассылку с моего профиля, предоставления моих услуг, и когда человек будет мне писать (подумаю как сделать фильтрацию, чтобы люди с каналов в прогу попадали только)
И в заявках будут появляться карточки, от человека, откуда он пишет, тема, само сообщение, ник, и я смогу либо после разговора с ним принять заявку или отменить.
P.S
Так же я еще делаю свой сайт, который потом думаю продвигать так же о предоставлении своих услуг, и думать как улучшить SEO, и люди смогут так же оставлять заявку, и эти заявки будут так же мне в прогу приходить.
8) Удаление фона - т.к я 90% фронт, и я часто редактирую картинки, удалить фон и тд, то у меня есть быстрый редактор, куда я закинул фото, и получил ее без фона в png
8) Скачать видео - тоже часто пользуюсь для како-то фона на сайте, и часто беру с pinterest, то я просто копирую ссылку, вставляю, и получаю его скаченным (потому что видео нельзя скачать от туда)
9) Виджеты - сделал все виджеты интерактивными, чтобы быстро взаимодействовать, и для быстрой статистики.
- Дата
- Впн (вкл/выкл/перезагрузка)
- Сколько осталось места в хранилище
- Погода
- Последний проект в аи чате (перейти)
- Время
- Своя картинка
- Перетащить картинку для удаления фона
- Вставить ссылку для скачивания видео
- Таймер (запускаю его, чтобы смотреть сколько я работал) / Секундомер (если надо сделать отсчет)
- Активность за неделю (блок как в гитхабе)
- Работа пк (выключить, перезагрузка, спящий режим) по нажатию на любую из этих кнопок идет таймер 3 > 2 > 1, чтобы при повторном нажатии если что отменить.
P.S
Виджеты можно включать/выключать, перемещать по всему экрану, и так же отслеживаются мониторы, чтобы они на каком-то определенном мониторе показывались
10) Сервера - еще не сделал, но я хочу сделать как в termius, чтобы был список своих серверов, подключится к ним, и файлы перекидывать на сервак
11) Настройки - бональщина пока что по типу автозапуска, тема, страница по умолчанию и тд. (не знаю, что еще пока добавить нужного)
Я просто хочу услышать какие-то еще идеи, которые на самом деле нужные, но они пока мне в голову не приходят.
Я еще думал, что чтобы у меня в панели мой гит был, чтобы я если что мог сразу перейти в свой репозиторий и тд, но пока не углублялся этим вопросом.
P.S
Так же думал потом сделать может быть свое личное приложение, где будет какой-то функционал, где будет мой впн, где могу делать подключение, дистанционное выкл пк (иногда, когда прилег, а уже вставать не охота, то с телефона просто выключил пк, и все)
У меня айфон, и поэтому тут еще проблема, как сделать приложение для себя, и скачать его, я знаю, по типу альстор, но каждые 7 дней ради этого мне надоест переустанавливать сертификат.
Поэтому идея, которая мне пришла это сделать так же как банки веб версию, где закрепить ее домой, и будет открываться типа приложение, но я пока не знаю, как точно это работает. Это просто обычный адаптив? когда мобилка, то просто приложение показывается?
В 2026 году в React появился хук use(), который меняет подход к работе с асинхронными данными и контекстом. Он входит в состав React 19 и уже доступен в стабильной версии.
Проблема, которую решает use()
Раньше загрузка данных в компоненте требовала написания однотипного кода:
useState для хранения данных
useState для состояния загрузки
useState для ошибки
useEffect для выполнения запроса
Ручное обновление всех состояний
Этот подход работал, но создавал много шаблонного кода и размазывал логику по разным хукам.
Как работает use()
use() -это хук, который принимает промис и «разворачивает» его прямо в теле компонента. Если промис ещё не завершился, React приостанавливает рендеринг компонента. Когда промис резолвится, компонент перерендеривается с полученными данными.
import { use, Suspense } from 'react'; function UserProfile({ userId }) { const user = use(fetchUser(userId)); return <div>{user.name}</div>; }
Здесь fetchUser(userId) возвращает промис. use() блокирует рендеринг до тех пор, пока этот промис не будет разрешён.
Роль Suspense
Для корректной работы use() необходимо использовать компонент Suspense. Он отлавливает состояние «приостановки» дочернего компонента и показывает fallback-интерфейс.
function App()
{ return
( <Suspense fallback={<div>Загрузка...</div>}>
<UserProfile userId="123" />
</Suspense> ); }
Пока UserProfile ожидает данные, пользователь видит сообщение «Загрузка...». После завершения запроса fallback заменяется на готовый UI.
Обработка ошибок
Ошибки, возникающие внутри use(), не обрабатываются автоматически. Для их перехвата используется ErrorBoundary — компонент, который ловит ошибки в дочерних компонентах.
<ErrorBoundary
fallback={<div>Ошибка загрузки</div>}>
<Suspense fallback={<div>Загрузка...</div>}>
<UserProfile userId="123" />
</Suspense>
</ErrorBoundary>
Это стандартный для React подход к обработке ошибок.
Детали реализации
use() не создаёт новый запрос при каждом рендере. Если переданный промис уже был разрешён, use() возвращает данные мгновенно.
use() можно вызывать условно. В отличие от useContext, use() не требует, чтобы хук вызывался всегда в одном и том же порядке.
use() можно использовать не только с промисами. Он также работает с контекстом, что делает его универсальным инструментом для чтения данных в компоненте.
Код сокращается в 3–4 раза, логика становится декларативной.
use() — это новый инструмент в экосистеме React, который делает код более читаемым и предсказуемым. В сочетании с Suspense и ErrorBoundary он позволяет строить компоненты, которые описывают что должно отображаться, а не как это должно загружаться. Это шаг в сторону декларативной модели, к которой React стремится с момента своего появления.
Если вам интересна тема веб-разработки, я также публикую разборы других кейсов в своем Telegram-канале и на Максе. Буду рад единомышленникам.
Microsoft наконец-то переписала компилятор на Go, заодно добавила многопоточность.
Почему именно GO,а не C# или Rust.
Синтаксис Go очень похож на JavaScript, что упростило буквальный "построчный" перенос 14-летней кодовой базы, сохранив всю его сложную логику без единого бага.
В отличие от Rust, в Go есть сборщик мусора, и для TypeScript оказалось выгоднее просто отключать его во время компиляции.
Go создан для эффективной работы с многопоточностью, что дало примерно половину прироста в производительности (вторая половина - от нативного машинного кода)
Что же это дало =>
VS Code собирается не за 125 секунд, а за 10.6, Playwright - с 12.8 секунд до 1.47.
Памяти при этом ест на 18–26% меньше. Ошибки в редакторе теперь появляются почти мгновенно, а не через 17 секунд.
По итогу ts,стал в 8–12 раз быстрее.
чему нужно быть готовым
TypeScript 7.0 - это не просто "поставил и забыл". Есть несколько важных изменений, которые могут задеть старые проекты :
Строгий режим включен по умолчанию. Это хорошо для новых проектов, но на старых может вызвать сотни новых ошибок типов.
Убрана поддержка устаревшего. Больше нельзя компилировать под ES5, использовать старые модульные системы (AMD, UMD, SystemJS) и некоторые устаревшие опции в tsconfig.json.
Для инструментов API пока нет. Стабильный программный API для работы с компилятором появится только в TypeScript 7.1. Поэтому сейчас TypeScript 6 и 7 могут работать параллельно в одном проекте через специальный пакет совместимости
Если вам интересна тема веб-разработки, я также публикую разборы других кейсов в своем Telegram-канале и на Максе. Буду рад единомышленникам.
Бывает что на руках есть лишь «бинарная» сборка сайта на модном фреймворке вроде Angular или React, в которой «срочно надо что‑то поправить». А исходного кода нет.
Есть лишь вы, «бандл» с обфрусцированным JavaScript внутри и горящие сроки. Рассказываю что с этим можно cделать кроме увольнения.
Процесс восстановления исходников из «source map» как есть.
Проблема
Как-то так получилось, что связка из Typescript и упаковщиков вроде Webpack захватила современную веб-разработку практически целиком, а модель построения веб-приложений под названием «Single Page Application» (SPA) стала применяться для всего вообще — от простейших лендингов и сайтов-визиток до сложных CRM-систем с динамической подгрузкой данных.
Из-за того что Typescript является компилируемым языком, в котором существует разделение на исходный код и конечный код, обфрусцированный и упакованный в специальные «бандлы» — произошло некое смешение смыслов:
Многие заказчики теперь понятия не имеют, что даже у статичных лендингов и сайтов-визиток могут быть исходники.
Особенно если пришли из веб-разработки начала 2000х, когда был кругом статичный HTML, а весь JavaScript-код был очень простым и вставлялся прямо на страницу.
Более того, поскольку результат сборки с помощью Webpack это внезапно тоже код, некоторые особо хитрые джентельмены умудрялись сдавать проекты в виде конечной сборки и набора бандлов, без предоставления реальных исходников, мотивируя тем что «это и есть рабочий исходный код».
И с точки зрения пунктов договора на разработку они вообщем-то были правы.
Вот вам небольшой пример такой сборки, взятый с сайта JHipster, чтобы было понятно о чем речь:
А вот так выглядит этот же код после частичного восстановления декомпилятором (который на самом деле больше де-обфрускатор):
(() => { "use strict"; var e, v = {}, m = {}; function r(e) { var i = m[e]; if (void 0 !== i) return i.exports; var t = m[e] = { exports: {} }; return v[e](t, t.exports, r), t.exports } r.m = v, e = [], r.O = (i, t, f, o) => { if (!t) { var a = 1 / 0; for (n = 0; n < e.length; n++) { for (var [t, f, o] = e[n], c = !0, u = 0; u < t.length; u++)(!1 & o || a >= o) && Object.keys(r.O).every(p => r.O[p](t[u])) ? t.splice(u--, 1) : (c = !1, o < a && (a = o)); if (c) { e.splice(n--, 1); var l = f(); void 0 !== l && (i = l) } } return i } o = o || 0; for (var n = e.length; n > 0 && e[n - 1][2] > o; n--) e[n] = e[n - 1]; e[n] = [t, f, o] }, r.d = (e, i) => { for (var t in i) r.o(i, t) && !r.o(e, t) && Object.defineProperty(e, t, { enumerable: !0, get: i[t] }) }, r.f = {}, r.e = e => Promise.all(Object.keys(r.f).reduce((i, t) => (r.f[t](e, i), i), [])), r.u = e => (592 === e ? "common" : e) + "." + { 127: "3939584411b29233", 146: "9e6e63e24ba057f5", 462: "3c011262c4aafd67", 592: "1a0b39952c0d48d5", 679: "dc91bdcb440d5d58", 792: "f4d5b583a515fb1b", 848: "b617a814d1d8d8d1", 905: "e873b5c1adf6b3c3", 920: "a0a741fb2015a1e4", 972: "bd8f95f56699519f", 994: "4c7d5415c98549f2" } [e] + ".js", r.miniCssF = e => {}, r.o = (e, i) => Object.prototype.hasOwnProperty.call(e, i), (() => { var e = {}, i = "jhonline:"; r.l = (t, f, o, n) => { if (e[t]) e[t].push(f); else { var a, c; if (void 0 !== o) for (var u = document.getElementsByTagName("script"), l = 0; l < u.length; l++) { var d = u[l]; if (d.getAttribute("src") == t || d.getAttribute("data-webpack") == i + o) { a = d; break } } a || (c = !0, (a = document.createElement("script")).type = "module", a.charset = "utf-8", a.timeout = 120, r.nc && a.setAttribute("nonce", r.nc), a.setAttribute("data-webpack", i + o), a.src = r.tu(t)), e[t] = [f]; var b = (g, p) => { a.onerror = a.onload = null, clearTimeout(s); var h = e[t]; if (delete e[t], a.parentNode && a.parentNode.removeChild(a), h && h.forEach(y => y(p)), g) return g(p) }, s = setTimeout(b.bind(null, void 0, { type: "timeout", target: a }), 12e4); a.onerror = b.bind(null, a.onerror), a.onload = b.bind(null, a.onload), c && document.head.appendChild(a) } } })(), r.r = e => { typeof Symbol < "u" && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(e, "__esModule", { value: !0 }) }, (() => { var e; r.tt = () => (void 0 === e && (e = { createScriptURL: i => i }, typeof trustedTypes < "u" && trustedTypes.createPolicy && (e = trustedTypes.createPolicy("angular#bundler", e))), e) })(), r.tu = e => r.tt().createScriptURL(e), r.p = "", (() => { var e = { 666: 0 }; r.f.j = (f, o) => { var n = r.o(e, f) ? e[f] : void 0; if (0 !== n) if (n) o.push(n[2]); elseif (666 != f) { var a = newPromise((d, b) => n = e[f] = [d, b]); o.push(n[2] = a); var c = r.p + r.u(f), u = newError; r.l(c, d => { if (r.o(e, f) && (0 !== (n = e[f]) && (e[f] = void 0), n)) { var b = d && ("load" === d.type ? "missing" : d.type), s = d && d.target && d.target.src; u.message = "Loading chunk " + f + " failed.\n(" + b + ": " + s + ")", u.name = "ChunkLoadError", u.type = b, u.request = s, n[1](u) } }, "chunk-" + f, f) } else e[f] = 0 }, r.O.j = f => 0 === e[f]; var i = (f, o) => { var u, l, [n, a, c] = o, d = 0; if (n.some(s => 0 !== e[s])) { for (u in a) r.o(a, u) && (r.m[u] = a[u]); if (c) var b = c(r) } for (f && f(o); d < n.length; d++) r.o(e, l = n[d]) && e[l] && e[l][0](), e[l] = 0; return r.O(b) }, t = self.webpackChunkjhonline = self.webpackChunkjhonline || []; t.forEach(i.bind(null, 0)), t.push = i.bind(null, t.push.bind(t)) })() })();
Думаю очевидно что попытка сопровождать такой код вашими хилыми силами это примерно как попытка участия 100кг туши в балете — и то и другое хотя и теоретически возможно, но врядли продлится долго.
Другой пример
Допустим вы сделали самый обычный «сайт‑визитку» для стартапа, через полгода стартап внезапно выстреливает и идет волна заказов.
Теперь сайт по-хорошему надо делать заново, но времени нет, а старый (он же текущий) при этом отключать нельзя — надо туда постоянно вносить мелкие правки текста: новые контакты, правила, адреса, ссылки и так далее.
И правок этих будет миллион.
А исходников нет. Забыли, потеряли, пролюбили в хаосе начинающей компании — кто работал в стартапах тот поймет.
И вот вы уже в позиции «вечной Золушки», вынуждены снова и снова «добавлять мелкие правки» в некогда статичный сайт прямо на ходу.
Тестовый пример
К сожалению не получится использовать один из наших рабочих проектов в качестве примера для этой статьи — Webpack генерирует чудовищного размера сборки для более-менее объемного проекта, разбираться в которых будет слишком уж долго.
Поэтому был взят шаблон лендинга на React, посвежее и более менее похожий на то что бывает в реальности. И сейчас я покажу на нем что и как можно сделать в столь печальной ситуации.
Выглядит оригинальный шаблон не без изысков вот так:
Цвет фона медленно меняется а звезды двигаются — автор хотел показать свое мастерство.
Сам проект технически вообщем-то тривиален, а его cборка максимально упрощена:
В папке build будет релизная сборка, а в build/js — те самые бандлы.
Каталог build со всем содержимым и будет выступать нашим тестовым стендом, на котором я буду показывать все чудеса эквилибристики с декомпиляторами и деобфрускаторами.
Но начнем мы все же с немного другого, поскольку самый короткий путь — часто самый лучший.
Восстановление исходников из .git
Очень и очень многие современные разработчики — скажем прямо "не отличаются умом и сообразительностью", поэтому не могут натворить бед работодателю даже когда им очень этого хочется.
Поэтому действительно на практике случаются ситуации экстремального дебилизма, хорошо показанные в фильмах Гая Ричи.
Например история вот этих парней:
Нет, эти даже поумнее некоторых моих коллег по отрасли, если честно.
Да, вы правильно поняли — все это про сохранение каталога .git одновременно с попыткой удаления файлов исходников, например с целью шантажа работодателя. Такое тоже бывает в жизни, причем чаще чем вы думаете.
Если кто вдруг еще не знает то сообщаю:
в каталоге .git хранится полная копия всего вашего исходного кода, еще и с историей всех изменений
Потому что это часть системы контроля версий, так она работает.
Было:
Я взял и удалил все папки с исходниками, оставив лишь финальную сборку и каталог .git
Запускаем восстановление:
git reset --hard
Стало:
Вуаля! Все вернулось из небытия.
Так что если видите папку .git на сервере или в архиве с вашим сайтом — скорее всего жизнь не так плоха и печальна.
«Скорее всего» тут по той простой причине, что бывают варианты, когда git используется не по прямому назначению, а например для передачи готовых сборок на сервер через сервис CI.
В этом случае фиксироваться будут только изменения в бандлах, а исходники останутся на уровне CI. Но это уже история не про лендинги и сайты-визитки, а про что-то большое, где полная утеря исходников маловероятна.
Восстановление из файлов «source maps»
Следующий рабочий вариант как вытащить исходники React-приложения из небытия — попытаться восстановить их из файлов «source maps».
Подробно про технологию «source map» можно почитать вот тут в оригинале или тут на русском в переводе Гоблина.
Если кратко, то это такие специальные файлы, которые генерируются при сборке проекта и содержат метаданные по исходному коду. Эти самые медатанные во время работы приложения позволяют формировать корректную трассировку исключений: с номерами строк и читаемыми названиями методов и классов.
Вот так выглядит небольшая часть «source map» файла:
Технически это просто большой JSON, с кучей вложенных объектов и закодированных частей.
Оказалось что метаданных из файлов «source maps» вполне достаточно для восстановления оригинального исходного кода.
Сейчас покажу как это работает.
Инструментов для восстановления исходников из «source map» файлов многоразных, я использовал вот такой, в первую очередь из‑за того что он сохраняет сразу на диск все найденное. По этой ссылке находится статья от автора, с детальным описанием работы.
Я использовал NPM-пакет http-server для эмуляции отдачи статики, поскольку он не связан с отладочным режимом Webpack (когда работает перекомпиляция на лету) и может отдавать только статический контент — получается полная симуляция старого доброго HTTP-сервера для отдачи статики, вроде Apache или Nginx.
Запускается вот так:
http-server ./build
По-умолчанию отдает контент на порту 8081 и использует путь «/», а аргумент «./build» это указание на каталог со статикой, все просто.
Теперь посмотрим что же удалось вытащить из source maps:
Как видите вытащить получилось дофига, настолько дофига, что некоторые коллеги после демонстрации такого восстановления хватаются за сердце и срочно убирают генерацию файлов «source maps» из своих продуктовых сборок.
Вот вам для сравнения оригинальный каталог с исходниками:
Нет лишь статики (картинок и стилей оформления), которая просто выносится при сборке в отдельный каталог:
Теперь посмотрим внимательно на востановленный исходный код — что именно и до какой степени в нем восстановилось.
Вот восстановленная копия стартового скрипта нашего тестового веб-приложения на React:
А вот так выглядит оригинальный файл:
Что тоже в легком удивлении?
В заголовке окна показывается полный путь к файлу, если вы вдруг подумали что я ошибся и дваджы открыл один и тот же файл ;)
Но это еще не все, следующий файл будет с JSX-шаблоном компонента — специально прокрутил до места где он начинается.
Восстановленная копия:
А теперь оригинал для сравнения:
Как видите восстановилось фактически вообще все.
Все исходные файлы, вся структура каталогов, весь исходный код включая даже комментарии и форматирование — спокойно восстанавливается из файлов source maps.
Прекрасный новый мир современных веб-технологий!
Ложка дегтя
К сожалению кое-что все же не восстанавливается: скрипты сборки и внешние пакеты. И то и другое придется собирать по крупицам и создавать заново, что в случае большого проекта легко может стать экстремально сложной задачей.
Тем не менее, такое восстановление исходников из файлов «source maps» это отлично работающий вариант для мелких лендингов и сайтов-визиток, по чьей-то безумной прихоти реализованных на компилируемом языке и столь сложном фреймворке.
Теперь наконец переходим к настоящей жести.
Работа с обфрусцированным бандлом
Да, это именно тот самый вариант, на который меня и подобных товарищей обычно и зовут. Каталога .git нет, никаких «source maps» тоже нет, есть лишь статика в виде набора файлов, а index.html выглядит как-то так:
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link href="https://use.fontawesome.com/releases/v5.4.1/css/all.css" rel="stylesheet"/><meta name="theme-color" content="#000000"/><meta name="description" content="My name is Hashir Shoaib. I’m a graduate of 2020 from National University of Sciences and Technology at Islamabad with a degree in Computer Engineering. I'm most passionate about giving back to the community, and my goal is to pursue this passion within the field of software engineering. In my free time I like working on open source projects."/><link rel="apple-touch-icon" href="logo192.png"/><link rel="manifest" href="/manifest.json"/><meta property="twitter:image" content="/social-image.png"/><meta property="og:image" content="/social-image.png"/><title>Hashir Shoaib</title><script defer="defer" src="/static/js/main.2911d091.js"></script><link href="/static/css/main.fe927caf.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
Ну и в наличии сами бандлы на JavaScript, прошедшие через Tree Shaking, минификацию и обфрускацию.
Задачи
Давайте начнем с типичного набора задач, которые ставились лично мне в таких случаях:
изменить текст в нужном месте,
добавить пункт меню в шапке,
добавить кнопку или изменить поведение существующей,
скрыть существующий или добавить блок данных.
Все это напоминаю надо проделать в обфрусцированном бандле, сгенерированным с помощью упаковщика Webpack и без доступа к исходникам.
Есть ряд вещей, которые необходимо знать о внутреннем устройстве «упакованной» версии веб-приложения на React прежде чем мы продолжим.
Первое и самое главное:
весь контент, все что вы видите на странице это один сплошной JavaScript, никакого HTML кроме стартового index.html в таком веб-приложении нет.
Для иллюстрации возьмем вот эту красивую кнопку:
Вот так выглядит восстановленный код (прогнанный через несколько разных деобфрускаторов), который ее создает и показывает:
Как видите к нормальному HTML это отношения не имеет, а правка такой дичи — не имеет ничего общего с обычной версткой.
Второе:
веб-приложение на React максимально изолировано от внешнего окружения.
Отправлять и получать события из кода вне React хоть и возможно но очень сложно и требует определенных действий в самом приложении.
Помимо этого, приложение на React поддерживает внутреннее состояние всех компонентов и не реагирует (по-умолчанию) на изменения в DOM-дереве страницы, произведенные снаружи приложения.
Это одновременно и хорошо и плохо.
Хорошо, потому что дает возможность производить манипуляции c DOM-деревом не влезая внутрь самого приложения.
Плохо, потому что ваши изменения могут быть легко затерты приложением, когда оно решит что «время пришло» и надо обновлять компонент. А обновлять его оно будет разумеется из своего внутреннего состояния.
Так что налучший подход это минимальное вмешательство во внутренности таких приложений и решение задачи малой кровью — снаружи.
Что я сейчас и продемонстрирую.
Начнем с самого простой, но самой частой задачи — с удаления элемента на странице.
Задача первая: "С глаз долой, из сердца вон"
Разумеется физически удалять из DOM-дерева ничего не стоит (помним про внутренее состояние React-приложения), благо есть вариант проще — скрытие ненужного элемента через CSS-стили.
Допустим, нам надо убрать пункт меню «About» из шапки страницы нашего тестового проекта.
Открываем index.html и добавляем новый блок <style></style> внутрь тега <head>, пишем :
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="shortcut icon" href="/favicon.ico" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <link href="https://use.fontawesome.com/releases/v5.4.1/css/all.css" rel="stylesheet" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="My name is Hashir Shoaib. I’m a graduate of 2020 from National University of Sciences and Technology at Islamabad with a degree in Computer Engineering. I'm most passionate about giving back to the community, and my goal is to pursue this passion within the field of software engineering. In my free time I like working on open source projects." /> <link rel="apple-touch-icon" href="logo192.png" /> <link rel="manifest" href="/manifest.json" /> <meta property="twitter:image" content="/social-image.png" /> <meta property="og:image" content="/social-image.png" /> <title>Hashir Shoaib</title> <script defer="defer" src="/static/js/main.2911d091.js"></script> <link href="/static/css/main.fe927caf.css" rel="stylesheet"> <!-- наша коварная вставка --> <style> #basic-navbar-nav > div.navbar-nav > a:nth-of-type(3) { display: none; } </style> </head> <body><noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body> </html>
Результат:
Внимание на пункты меню вверху, раздел "About" пропал
Теперь рассказываю как и почему это работает.
Если открыть «Developer Tools» в любимом браузере Chrome (клавиша F12) и перейти на вкладку «Elements», можно увидеть что внутри пустого тега
<div id="root"></div>
внезапно появилась жизнь — все что вы визуально можете наблюдать на странице в браузере находится внутри этого самого тега:
Рабочие будни
То что вы наблюдаете есть результат работы рендера React, который «отрисовывает» каждый компонент приложения внутри корневого элемента согласно его текущему состоянию.
И до тех пор пока в каком-то из скрываемых нами компонентов не произойдет изменения свойства «display» — он так и будет невидимым.
Что касается самого CSS, то используется достаточно сложный селектор, где происходит одновременно выборка по id элемента:
#basic-navbar-nav
затем обращение по цепочке ко вложенным элементам:
> div.navbar-nav > a
а потом еще и обращение по порядковому номеру:
a:nth-of-type(3)
Селектор a:nth-of-type(3) означает что запрашивается третий по порядку элемент <a>. Обращение по уникальному id работает поскольку он был указан в исходном компоненте React:
<Navbar.Collapse id="basic-navbar-nav">
Который после стадии рендеринга попадает и в конечный DOM-элемент:
Код выше скроет самую большую надпись на странице с именем автора:
Огромной надписи выше строки "Passionate about.." больше нет.
Думаю этого примера будет достаточно и читателям теперь понятно как и почему оно работает.
Замечу что сей метод и на практике (на больших приложениях) работает «на ура», честно говоря большая часть работы с подобными задачами это и есть столь банальное скрытие элементов: ссылок, кнопок, пунктов меню или просто разделов с данными.
Но это еще не конец статьи, поэтому мы погружаемся глубже в дикий треш.
Задача вторая: "немного поправить текст на странице"
Следущая стадия это скромная просьба «немного изменить текст на странице», напоминаю что речь про обфрусцированный и минимизированный бандл, созданный с помощью упаковщика, о чем разумеется просящие скромно умалчивают.
Тут уже потребуется немного кода на JavaScript, поэтому добавляем тег <script></script> и помолясь начинаем ваять:
let intervalID;
function makeWhenReady() { let el3 = document.querySelector('span.react-loading-skeleton'); if (!el3) { let el = document.querySelector('#home > div.container > div.text-center > h1.display-1'); el.innerHTML = "Да здраствует хардкор!"; el.style.display='block'; clearInterval(intervalID); } } window.addEventListener('load', function() { intervalID = setInterval(makeWhenReady, 500); });
Вот так выглядит результат работы:
Результат правки
Теперь немного расскажу о том как и почему это работает.
Попытка поработать с DOM-деревом непосредственно из такого обработчика приведет к тому что вместо данных вы увидите фигу шаблон их загрузки — в проекте используется модуль react-loading-skeleton для создания таких шаблонов.
Поэтому мы поступаем хитрее: устанавливаем свой собственный обработчик на событие load у «окна» браузера:
Внутри обработчика уставливаем еще и фоновую обработку — функцию, которая будет вызываться с определенной переодичностью (в нашем случае раз в 500 миллисекунд):
intervalID = setInterval(makeWhenReady, 500);
intervalID как нетрудно догадаться это ее идентификатор, который будет нужен далее для остановки этой фоновой функции.
Внутри этой фоновой функции мы делаем проверку на наличие в DOM-дереве элементов <span> с классом react-loading-skeleton — сие означает что еще не все компоненты загружены:
function makeWhenReady() { let el3 = document.querySelector('span.react-loading-skeleton'); if (!el3) { .. } }
Если таких элементов не было найдено — считаем что все компоненты загрузились, производим наши манипуляции с данными и останавливаем фоновую функцию:
if (!el3) { let el = document.querySelector('#home > div.container > div.text-center > h1.display-1'); el.innerHTML = "Да здравствует хардкор!"; el.style.display='block'; clearInterval(intervalID); }
Поскольку между рендером оригинала и нашими изменениями есть видимая задержка — изменяемый текст будет «моргать»:
сначала будет отображен оригинал, который через какое-то время изменится на нашу версию.
И пользователи это увидят и обязательно огорчатся. Чтобы их не смущать, необходимо спрятать изменяемый блок через CSS:
Причем на вставленный таким образом блок будут распространяться и все эффекты оригинальных элементов:
тень и «подпрыгивание» блока при наведении мыши.
Ну что ж, с этой задачей разобрались, самое время повысить накал жести.
Задача третья: "поправить валидацию формы"
Разумеется есть и нормальныеспособы организовать взаимодействие с React-приложением в обе стороны — из стороннего JavaScript-кода вызывать React-компонент и из такого компонента вызывать сторонний JavaScript-код.
Но к сожалению все они требуют внутренних изменений в приложении, вроде регистрации компонента в контексте window, которые очевидно никто для вас вносить не станет.
Поэтому стандартные способы, о которых вы при желании сможете прочитать в документации и других статьях — для наших специфических задач к сожалению не применимы.
Поэтому будем применять нестандартные, как обычно.
Для лучшей иллюстрации я добавил в проект простую форму с логикой валидации:
Визуально это что-то вроде формы регистрации, ключевой компонент React с реализацией формы был немного изменен по сравнению с оригиналом, выглядит так:
import { FC } from 'react'; import { useForm } from '../hooks/useForm'; import './Registration.scss'; import { Container, } from "react-bootstrap";
const Registration: FC = () => { const { handleSubmit, handleChange, data: user, errors } = useForm<User>({ validations: { name: { pattern: { value: '^[A-Za-z]*#x27;, message: "You're not allowed to use special characters or numbers in your name.", }, }, age: { custom: { isValid: (value) => parseInt(value, 10) > 17, message: 'You have to be at least 18 years old.', }, }, password: { custom: { isValid: (value) => value?.length > 6, message: 'The password needs to be at least 6 characters long.', }, }, }, onSubmit: () => alert('User submitted!'), });
Помещен он был рядом с другими комонентами, с сохранением принципов их именования. Сами стили при этом не менялись и были взяты из оригинального проекта «как есть»:
EventTarget.prototype.addEventListener = function (a, b, c) { if (c == undefined) c = false; if (a == 'submit') { console.log('hijack listener ', a); if (!this.eventListenerList) this.eventListenerList = {}; if (!this.eventListenerList[a]) this.eventListenerList[a] = []; this.eventListenerList[a].push({ listener: b, options: c }); } this._addEventListener(a, b, c); };
EventTarget.prototype._getEventListeners = function (a) { if (!this.eventListenerList) this.eventListenerList = {}; if (a == undefined) { return this.eventListenerList; } return this.eventListenerList[a]; };
В результате его работы, вы увидите в консоли браузера все регистрации обработчиков на отправку формы:
Но главное что у всех DOM-элементов, в которые происходили добавления обработчиков появится новая функция _getEventListeners(), которая будет содержать ссылки на все обработчики.
Зачем это надо?
Например для того чтобы эти обработчики было можно легко удалить:
let r = document.querySelector('#root'); console.log('root element:', r);
let rEvents = r._getEventListeners(); console.log('events:', rEvents);
for (let evt of Object.keys(dvevents)) { console.log('evt:',evt); for (let i = 0; i < dvevents[evt].length; i++) { dv.removeEventListener(evt,dvevents[evt][i].listener); } }
Код выше необходимо вставить все в ту же функцию makeWhenReady(), которая как вы помните вызывается после полной инициализации React-приложения.
Пару слов про обработчики в React.
Оказалось что все обработчики React регистрируются в родительском DOM-элементе, внутри которого происходит отрисовка всех компонентов React:
<body><noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body>
Очевидно что при таком подходе обработчиков там будет очень много — имейте это ввиду.
Наконец для того чтобы добавить наш собственный обработчик для отправки формы, вставляем вот такой код (все также в функцию makeWhenReady()):
let elForm = document.querySelector('form.registration-wrapper'); elForm.addEventListener("submit", function (e) { e.preventDefault(); alert('Hi there!'); });
Результат:
Вместо отправки на сервер вызывается наш обработчик.
Эпилог
В общем случае действительно стоит отказаться от попыток доработки обфрусцированных бандлов — это «путь в никуда», решение которое невозможно поддерживать долго и рано или поздно оно все равно сломается, похоронив под собой и все ваши доработки.
Только в исключительных случаях, когда действительно горит и надо «поправить вчера» на такое имеет смысл идти.
Каждый раз, когда начинаешь новый веб-проект, встаёт один и тот же вопрос: как рендерить страницы? CSR, SSR, SSG — звучит как аббревиатуры из учебника, но на практике именно этот выбор определяет скорость сайта, позиции в поиске и удобство разработки.
CSR — Client-Side Rendering
Сервер отдаёт почти пустой HTML с одним тегом div и большим JS-бандлом. Браузер скачивает JavaScript, выполняет его, делает запросы к API — и только после этого пользователь видит контент.
Плюсы:
Быстрая навигация после загрузки
Богатые интерактивные интерфейсы
Минусы:
Долгая первая загрузка (Time to Interactive)Долгая первая загрузка (Time to Interactive)
Плохо для SEO — боты видят пустой HTML
Требует хорошего интернета у пользователя
Главная проблема CSR — поисковые боты. Googlebot умеет выполнять JavaScript, но делает это с задержкой и не всегда корректно. Яндекс справляется хуже. В итоге страницы индексируются медленнее и хуже.
Когда CSR — правильный выбор:
• Дашборды и админки за авторизацией — SEO не нужен
• SPA с богатой интерактивностью: редакторы, конструкторы, таблицы
• Внутренние инструменты компании
• Приложения, где пользователь авторизован и контент персональный
SSR — Server-Side Rendering
При каждом запросе сервер генерирует готовый HTML с данными и отдаёт его браузеру. Пользователь сразу видит контент — не надо ждать, пока выполнится JavaScript.
Плюсы:
Отличный SEO — боты видят готовый HTML
Актуальные данные на каждый запрос
Минусы:
Нагрузка на сервер при каждом запросе
Медленнее навигация, чем CSR
Нужен сервер — хостинг дороже
SSR — лучший выбор для SEO-важных страниц. Поисковик получает готовый HTML с контентом, мета-тегами и структурированными данными. Индексация быстрее и надёжнее, чем при CSR.
Когда SSR — правильный выбор:
• Интернет-магазины — карточки товаров должны индексироваться
• Новостные сайты и блоги с часто меняющимся контентом
• Страницы с персонализированным контентом (лента, рекомендации)
• Любые публичные страницы, где важен SEO и актуальность данных
SSG — Static Site Generation
HTML генерируется один раз — во время сборки проекта. Готовые файлы раздаются с CDN без участия сервера. Это самый быстрый способ доставить контент пользователю.
Плюсы:
Максимальная скорость
Отличный SEO
Дешёвый хостинг — просто файлы
Высокая надёжность — нет сервера
Минусы:
Данные могут быть устаревшими
Долгая сборка при большом числе страниц
Не подходит для часто меняющихся данных
Когда SSG — правильный выбор:
• Лендинги и маркетинговые сайты
• Документация и справочники
• Блоги и портфолио
• Сайты, где контент меняется редко или по расписанию
Главное про SEO
SEO — это часто решающий фактор при выборе рендеринга. Вот простое правило:
• Страница публичная и должна находиться в поиске → SSR или SSG
• Страница за авторизацией или контент персональный → CSR
• Контент меняется часто → SSR
• Контент меняется редко → SSG
Отдельно про Core Web Vitals — Google учитывает LCP, FID и CLS в ранжировании. SSG и SSR дают лучшие показатели, чем CSR по умолчанию, но и CSR можно оптимизировать через lazy loading, code splitting и prefetching.
Хорошая новость: Next.js позволяет миксовать все подходы в одном проекте. Лендинг — SSG, карточка товара — SSR, личный кабинет — CSR. Именно поэтому он стал стандартом индустрии.
Больше материалов про разработку и запуск своих продуктов — в Telegram-канале @deep_in_prod
Современный Frontend давно перестал быть слоем поверх API. В реальном продукте он решает те же вопросы, что и любая распределённая система: где живут данные, когда они считаются свежими, как пережить задержку сети, что показать при частичном отказе, где провести границу между сервером и клиентом, как не превратить локальное состояние в теневую базу данных.
Проблема в том, что мы всё ещё говорим о Frontend языком интерфейса, хотя строим уже не интерфейс. Мы строим систему доставки смысла пользователю.
Классическая схема была понятной. Сервер собирает страницу, браузер показывает HTML, пользователь кликает ссылку, сервер отдаёт новую страницу. Почти вся сложность жила на сервере: маршруты, права, данные, шаблоны, ошибки.
Потом интерфейсы стали богаче, и мы перенесли часть логики в браузер. Страница перестала быть документом и стала приложением. Браузер начал хранить состояние, валидировать формы, строить маршруты, кэшировать данные, оптимистично обновлять UI. Это дало скорость и интерактивность - и заодно растащило Frontend по нескольким слоям.
Часть живёт на сервере: рендеринг, подготовка данных, проверка прав. Часть - в браузере: интерактивность, ввод, мгновенная обратная связь. Часть - в кэше: CDN, query cache, prefetch, service worker. А часть вообще не выглядит как Frontend: она в контракте API, в схеме данных, в дизайн-системе, в правилах релиза.
Когда всё работает, пользователь видит простую вещь: страница открылась, кнопка нажалась, действие сохранилось. Когда ломается, выясняется, что «кнопка» была последним звеном в длинной цепочке решений.
Кнопка не просто вызывает onClick. Она может запускать optimistic update, инвалидировать кэш, отправлять событие аналитики, менять URL, зависеть от прав пользователя, показывать pending-состояние, откатывать UI после ошибки или повторять запрос.
Это уже не «покрасить кнопку». Это проектирование поведения системы в условиях неопределённости.
Ernest Peixotto (American, 1869–1940)
Компоненты не спасают архитектуру
Когда Frontend стал сложнее, мы нашли удобный ответ - компонентную модель. Разбей интерфейс на части, переиспользуй их, собери экран из маленьких элементов. Сильная идея, но без неё современные интерфейсы было бы трудно поддерживать.
Но компонентная модель хорошо описывает форму интерфейса и плохо описывает поведение продукта. Компонент может быть маленьким, типизированным и красивым, а архитектура всё равно слабой, потому что настоящая сложность живёт между компонентами.
Самая частая ошибка - смешать UI-состояние, серверные данные, процесс сохранения, ошибки и производные значения в одном месте. Тогда форма знает про API, кнопка - про бизнес-правила, таблица - про инвалидирование кэша, а страница хранит копию данных из query cache «на всякий случай». Дальше начинаются исключения, быстрые фиксы, дублирование, страх менять экран и фраза «проще переписать».
Redux, Zustand, React Query, SWR, Signals, Context - ни один инструмент не решит за команду главный вопрос: где источник правды.
Если правда на сервере, Frontend должен уважать сервер и не показывать пользователю фантомное состояние. Если правда в пользовательском вводе, нельзя терять его после refetch. Если правда в URL, состояние должно переживать перезагрузку и передаваться ссылкой. Плохая архитектура начинается там, где источник не выбран: сервер говорит одно, кэш помнит другое, форма хранит третье, URL показывает четвёртое, а компонент делает пятое, потому что «так быстрее».
Near Richmond, Yorkshire (1877)
Где должна жить логика
Главный вопрос: кто за что отвечает, и что произойдёт, когда что-то пойдёт не так.
Возьмём простой сценарий - пользователь переименовывает проект.
Слабая версия: Пользователь жмёт «Сохранить». Форма отправляет PATCH, показывает спиннер. Через 800 мс ответ приходит, спиннер исчезает, имя в форме обновляется. Хорошо. Но в сайдбаре имя меняется ещё через секунду, потому что список проектов перезапрашивается отдельно. А в соседней вкладке, открытой час назад, имя вообще старое. Пользователь видит три разных состояния одной сущности и не понимает, какому верить.
Сильная версия: То же действие. Имя в инпуте меняется мгновенно - здесь источник правды сам пользователь. После сабмита UI сразу показывает новое имя во всех местах: в сайдбаре, в заголовке, в хлебных крошках, но визуально помечает его как ещё не сохранённое (например, приглушённым цветом). Если сервер подтвердил, тогда пометка исчезает. Если вернул ошибку - UI откатывается к старому имени и показывает причину человеческим языком: «нет прав» или «имя уже занято». Список проектов в кэше обновляется по одному правилу, а не по совпадению.
Из этого вытекает несколько практических правил.
Источник правды - один на каждое значение. Если правда на сервере, Frontend читает её оттуда и не хранит копий «на всякий случай». Если правда в URL - фильтры, поиск, выбранная вкладка, она переживает перезагрузку и передаётся ссылкой. Если правда во вводе пользователя, её нельзя потерять при refetch. Дублирование источников - это не ускорение, это будущий баг.
Frontend не решает то, за что не отвечает. Он может скрыть кнопку под роль пользователя, но не должен сам определять, разрешено ли действие. Это решает сервер. Frontend может показать бизнес-правило и объяснить его, но само правило живёт в домене, а не в компоненте. Если правило про деньги, права или статусы случайно осело в JSX, его рано или поздно нарушат.
Кэш это договор, а не оптимизация. Когда мы держим данные в query cache, мы говорим пользователю: «верь этому, пока я не скажу обратное». У договора должны быть условия: сколько данным можно верить, кто их обновляет, когда инвалидировать, что показывать, если они устарели. Быстрый интерфейс не должен быстро врать.
Производительность и ошибки
Производительность часто сводят к Lighthouse, Core Web Vitals и размеру бандла. Метрики полезны, но не объясняют главного.
Пользователь не обязан ждать. Медленный Frontend почти всегда - сумма решений: слишком много клиентского кода, тяжёлая дизайн-система, водопад запросов, дорогой рендер, небрежная гидратация. Это нельзя починить в конце. Это проектируется в начале: что отдать с сервера, что загрузить заранее, что отложить, какой экран остаётся полезным при частичном отказе.
С ошибками то же самое. В продакшене сеть медленная, API отвечает не сразу, сессия истекает, пользователь кликает быстро, данные меняются в другой вкладке. Ошибка валидации должна жить рядом с формой. Ошибка прав - приходить от сервера. Ошибка сети - объяснять, можно ли повторить действие. Ошибка бизнес-правила должно быть написана по-человечески: не «400 Bad Request», а «Нельзя удалить проект, пока в нём есть активные задачи».
Надёжность Frontend - это не отсутствие ошибок в консоли. Это способность интерфейса вести себя предсказуемо, когда мир вокруг неидеален.
A Winter Scene (mid 1640s)
Что отличает сильного Frontend-инженера
Не количество выученных хуков и не скорость написания компонентов. Он понимает границы: где заканчивается серверное состояние и начинается клиентское, где данные можно кэшировать, где нужна мгновенная реакция, а где честный loading, где типы помогают, а где нужна runtime-проверка, где абстракция снижает стоимость изменений, а где делает код мутным.
Он проектирует не только компоненты, но и договоры: между клиентом и сервером, между страницей и компонентом, между системой и пользователем.
Плохая архитектура почти всегда выглядит нормально в первый месяц. Потом она начинает требовать всё больше энергии за всё меньший результат. Переписывать приходится не потому, что React плохой или Vue плохой. Чаще всего в начале никто не ответил на скучные вопросы: что является источником правды, какие данные могут устареть, что пользователь увидит при медленной сети, что произойдёт, если запрос выполнится дважды, какая логика принадлежит домену, а какая - интерфейсу.
Заключение
Frontend больше не тонкая оболочка вокруг API. Он стал местом, где техническая архитектура встречается с человеческим ожиданием. Пользователь не видит server components, query cache, hydration и source of truth. Он видит другое: страница открылась или нет, действие сохранилось или потерялось, ошибка объяснена или спрятана, интерфейс помогает или заставляет сомневаться.
Frontend - это слой доверия. Он отвечает за договор между человеком и системой: нажал - увидел реакцию, сохранил - понял результат, ошибся - понял, как исправить, вернулся - попал туда, где ожидал быть. Хорошая архитектура не убирает сложность. Она кладёт её туда, где её можно понять.
Этой вводной статьёй я хотел поделиться мыслями и начать этот блог. Всем спокойных релизов и поменьше багов в продакшене ❤
За 25 дней нам удалось добиться довольно больших изменений в нашем проекте.
Мы провели:
Рефакторинг back-end сервисов
Убрали часть легаси кода на фронте
Переработали некоторый UI элементы и добавили плавности
Добавили новый функционал
Чем бы дитя не тешилось, лишь бы не плакало
Именно так мы подумали и решили завершать первый важный этап нашего MVP проекта. Мы подготовили всю инфраструктуру к работе, подбили UI что бы если и есть баги - то которые мы явно упустили за 30 часов тестирования.
Новый функционал
Мы добавили в приложение следующее:
Рассылка кодов авторизации
Поиск серверных чатов
Подписка у пользователя в сообщениях какая это роль
Переработали дизайн тредов, теперь у него больше настроек
Сохранение сообщений
Поиск общих чатов
Не есть что конечно, слизано с приложения Discord, но мое виденье его внутри компании немного другого формата.
В чем вообще задумка его для корпоративного мессенджера? Для начала легко найти чаты внутри команд или структур. Группировка - это настраиваемые элементы, мы вывели их в отдельный конфиг, так что поправить под компанию - 5 минут. Настраивается из настроек чата.
Подписка у пользователя в сообщениях какая это роль
Представим что к вам пришел новый сотрудник, а в команде 50 человек. И вот ему надо привыкнуть еще к тому кто разработчик, кто тестировщик, кто стрим лид и тп, а еще же ведь и правильных людей тегать. Эту проблему и позволяет решить данная приписка.
Переработали дизайн тредов, теперь у него больше настроек
Вот тут наверное самое важное, то что плохо реализовано в корпоративных тредах. Долго думали что же нам не хватает и как попытаться уместить все в обычную форму.
Мы расширили настройки на создание тредов
Теперь кроме названия треда и иконки можно:
Проставить теги
Написать нормальное описание
После создания тредов теперь мы можем не только посмотреть их список, но и так же:
Отфильтровать по названию, даже по одному слову в середине текста названия или описания
Отфильтровать по тегам, идет сортировка по часто используемым
Закрепить тред (все закрепленные всегда будут вверху, только потом будут идти не закрепленные, даже при поступлении в них новых сообщений)
Возможно, предвижу, кому-то удобнее прям внутри сообщений писать, тем самым создавая обсуждения. Потом мы добавим такое, но не в рамки обсуждения, а в рамках внутренних комментариев.
Рассылка кодов авторизации
Инфраструктура под это дело заложена в сервис авторизации. При регистрации или авторизации сервис будет отправлять на почту пользователя сообщение в формате HTML.
Пока еще не заводили отдельную почту для нашего сервиса.
Рефакторинг back-end сервисов
Мы изменили логику проверки прав, она стала более замудренная, но в тоже время и более правильная.
Теперь Gateway (основной сервис который либо пропускает запрос, либо нет) - подключен к redis pub/sub, инвалидирует версию прав и бит маску. Если не было события изменения - берет из мемори, если была - обновляет свой кеш. Кеш одного пользователя занимает 600.B. что не является слишком много. Для тяжелых прав на 2000 пользователей в одном сервер чате это около 4.5мб. В целом - пойдет.
Кеш хранимый в gateway:
Сервис топиков тоже потерпел изменения. теперь он не ходит в Permission сервис что бы уточнить права пользователя на просмотр, а Gateway: ProxyRouter приклеивает заголовки:
А сервис топиков их начинает парсить:
На выход топик сервис уже отдает отфильтрованные темы которые соответствуют правам пользователя внутри его роли:
Наконец то вынесли все коллекции сервис в отдельные базы. Раньше была одна общая БД, делалось для быстрого написания кода, но пораждало много зависимостей и хождений в чужие коллекции. Теперь все сервисы имеют свою БД со своими коллециями. Отдельный инстанс имеет только сервис сообщений.
50% сервисов перевели в режим публикации событий. Т.е. раньше было много HTTP вызовов между ними, что пораждало лишние задержки в 1-4мс. Мелочь, но в рамках 1000 или 10000 пользователей это уже существенная нагрузка. Теперь HTTP вызовы служат только для получения информации от другого сервиса, все события которые раньше были - переложены на Redis. Пример: Пользователь зашел в новый чат. Сервис чата сформирует событие что у него появился новый memories, сервис прав подхватит это событие и присвоит ему права и положит их в Redis, сервис Gateway увидит новые данные и заберет их к себе.
Мы работаем над переводом остальных сервисов, но не все так быстро))
Удаление легаси кода
При создании проекта, я вообще ничего не знал о методах разработки фронтенда. Сейчас когда нас несколько уже и мы отходим от AI разработки - начинаем сталкиваться с проблемами. Например тогдлы переключения - так как фронт писал ИИ, он в каждую форму где есть тогл - делал новый стилевый файл, и таких файлов скопилось 15+ ед. Мы перевели их на shared и пере-используем. И таких компонентов было очень много. В итоге нам получилось подчистить примерно 2к стилевых строк кода, сократить в JSX около 150 строк, потому что теперь это общие подключения.
Выделили отдельные сервисы кеша. Тонкие обёртки над универсальным key-value store для кэширования. Не хранят данные сами — только формирует ключи и инкапсулирует логику работы с одним конкретным namespace. Пример ниже:
Аналогично сделали и с WS сервисом, вынесли все WS действия в отдельные сервисы, для сообщений, топиков, прав, тредов и тп. Раньше это были одни монолитные файлы.
Большую часть действующих фронт сервисов перевели на isMobile проверку. Например анимации фона на мобилке - не нужны, мы их не грузим и в сборку они так же не попадут.
Изменения в UI
Часть изменений и доработок написал выше.
Изменение в тулбаре голосовой комнаты:
Теперь это отдельный виджет. Кнопки анимированные, при наведении на ПК показывают анимацию
Добавили выбор устройства на ПК:
Тоглам кнопкам добавили плавный переход при нажатии, на фото это не будет видно.
Для мобильной версии сделали 2 вида тапа.
Короткий: Открывает меню взаимодействия с сообщениями
Короткий тап который открывает меню управления
Длинный: позволяет выбрать несколько сообщений сразу. Удалить, переслать или ответить
Итог
За 25 дней мы проделали огромную работу над проектом. Работали порой и до часу ночи. Все прошлые выходные провели с утра до ночи в нашем проекте.
Сейчас у нас:
16 микросервисов бэка + 5 инфраструктурных
137 микрофронтенд сервисов. Да и кажется что "Как-то много" - действительно так, много логики.
Что сейчас:
Наши инфраструктурные сервисы готовы. Мы начинаем фазу тестирования. Активно ищем сервер в аренду с простой поддержкой и не очень дорогой. Хотели 20.04 уже запуститься, но не успели, мысли приходили на лету и где то правили, где-то дорабатывали. Плюс еще потратили кучу времени на обучение RNNoise модели. Вроде теперь давит шумы хорошо, не уровень Krisp конечно.
Спасибо за то что прочитали! Задавайте вопросы если интересно. Готовы версии для PC, IOS, Android. Они на WebWiev, но мы старались мобильные версии оптимизировать.
P.s. Если есть небольшая компания со своими мощностями в которую можно запустить тест для сотрудников не Free основе - напиши в ТГ: AN_Cayo. Настроем под вас наши конфиги и поможем расскатиться) (Ну мало ли повезет и найдется такая возможность. Без буллинга плиз)
Upd. Мессенджер не только для корпоративного общения. Для общего с функциями друзья, черные списки, E2E шифрования лс - будет чуть позже.
Всем привет, я соло джуниор/мидл разработчик, сейчас обновляю образовательного Телеграм-бота для клиента. Мы переходим от простого текстового бота к полноценному Telegram Mini App.
Клиент постоянно добавляет новые фичи, и хотя мне нравится работа, мой синдром самозванца сходит с ума по поводу цены. Я запросил около 50-60 тысяч рублей за весь объем, разбив его на 3 этапа.
Вот стек технологий и запрошенный функционал:
Стек: React.js, Python (Backend), PostgreSQL.
Объем работ:
• Полное обновление дизайна: Современный дизайн (Glassmorphism), адаптивная верстка, автоматическая смена темной/светлой темы.
• Умная регистрация: Каскадные списки (выбор города подтягивает нужные школы из БД).
• Система тестирования: Тесты по Алгебре, Геометрии и др. с активным таймером обратного отсчета и автозавершением.
• ИИ-разбор: Интеграция с нейросетью для разбора ошибок.
• Сложные рейтинги: Глобальный топ учеников и сложные SQL-запросы для рейтинга по школам.
• Геймификация (Дуэли): Асинхронная система дуэлей 1 на 1.
Я делаю всё сам: архитектуру БД, бэкенд и фронтенд.
Мой вопрос: 50-60 тысяч рублей — это справедливая цена за такой объем работы для соло-фрилансера, или я сильно демпингую? Но хотел бы услышать ваши мысли о соотношении объема работы и оплаты. Спасибо!