Pull to refresh

Comments 108

Жесть, прямо сплю и вижу чтобы писать вот такой код и чтобы все вокруг так писали:
https://github.com/balajahe/balajahe.github.io/blob/master/real_agent/obj-list.js
import * as WcMixin from '/WcApp/WcMixin.js'

const me = 'obj-list'
customElements.define(me, class extends HTMLElement {

	connectedCallback() {
		WcMixin.addAdjacentHTML(this, `
			<style scoped>
				${me} > #listDiv {
					flex-flow: column;
					margin-bottom: var(--margin2);
				}
				${me} obj-list-item {
					min-width: 100%;
					margin-top: var(--margin2);
					padding-bottom: var(--margin1);
					border-bottom: 1px solid silver;
					flex-flow: column;
					overflow: auto;
					cursor: pointer; 
				}
				${me} #objDesc {
					display: block;
					padding-left: var(--margin2);
				}
				${me} #objProps {
					display: flex; flex-flow: row wrap;
					padding-left: var(--margin2);
				}
				${me} > footer { 
					text-align: center;
					padding-left: 1rem; padding-right: 1rem;
				}
			</style>
			<div w-id='listDiv'></div>
			<footer>Real Agent is a database of arbitrary objects with geolocation, photos and videos.</footer>
		`)

		APP.forceSetHash(me)

		this.appBar = [
			['sep'],
			['new', async () => {
				let mman = document.querySelector('#obj-new-medias')
				if (!mman) mman = document.createElement('media-manager').build(
					[],
					[
						['msg', 'Take photo, video, or audio:'],
						['back'],
						['next', () => APP.route('obj-new')]
					]
				)
				APP.route('obj-new-medias', mman)
			}]
		]

		this.addEventListener('save-item', async (ev) => {
			await this.saveExistObj(ev.val)
			this.querySelector('#' + ev.val.created).replaceWith(document.createElement('obj-list-item').build(ev.val))
		})

		this.addEventListener('delete-item', async (ev) => {
			await this.deleteObj(ev.val)
			this.querySelector('#' + ev.val).remove()
		})

		this.refreshList()
	}

	refreshList() {
		this.listDiv.innerHTML = ''
		APP.db.transaction("Objects").objectStore("Objects").openCursor(null,'prev').onsuccess = (ev) => {
			const cursor = ev.target.result
			if (cursor) {
				this.listDiv.append(document.createElement('obj-list-item').build(cursor.value))
				cursor.continue()
			}
		}
	}

	async addItem(obj) {
		await this.saveNewObj(obj)
		this.listDiv.prepend(document.createElement('obj-list-item').build(obj))
	}

	async saveNewObj(obj) {
		const origins = []
		for (const media of obj.medias) {
			media.created = obj.created + media.created
			origins.push({ created: media.created, origin: media.origin })
			media.origin = null
		}

		const tran = APP.db.transaction(['Objects', 'Origins'], 'readwrite')
		const proms = []
		proms.push(new Promise(resolve => tran.objectStore("Objects").add(obj).onsuccess = resolve))
		for (const origin of origins)
			proms.push(new Promise(resolve => tran.objectStore("Origins").add(origin).onsuccess = resolve))
		await Promise.all(proms)
	}

	async saveExistObj(obj) {
		const origins = []
		for (const media of obj.medias) {
			if (media.origin) {
				media.created = obj.created + media.created
				origins.push({ created: media.created, origin: media.origin })
				media.origin = null
			}
		}
		const tran = APP.db.transaction(['Objects', 'Origins'], 'readwrite')
		const proms = []
		proms.push(new Promise(resolve => tran.objectStore("Objects").put(obj).onsuccess = resolve))
		for (const origin of origins)
			proms.push(new Promise(resolve => tran.objectStore("Origins").put(origin).onsuccess = resolve))
		await Promise.all(proms)
	}

	async deleteObj(id) {
		const tran = APP.db.transaction(['Objects', 'Origins'], 'readwrite')
		const proms = []
		proms.push(new Promise(resolve => tran.objectStore("Objects").delete(id).onsuccess = resolve))
		proms.push(new Promise(resolve => tran.objectStore("Origins").delete(IDBKeyRange.bound(id, id + '_'.repeat(id.length))).onsuccess = resolve))
		await Promise.all(proms)
	}
})

Я даже не знаю что и сказать не используя матных слов, в принципе этот код сам за себя говорит) Представляю когда проект написанный вот так попадется в чьи-то руки на доработку, вот кто-то обрадуется))
Что конкретно вам не нравится? Внутренние стили? Ну используйте внешние. Работу с БД в отдельный компонент? Никто не мешает. Только зачем.
Мне тут не нравится абсолютно все, вообще весь код, а не что-то конкретное.

Мы не в детском саду, чтобы оперировать понятиями "нравится / не нравится".


Конкретно этот код плох следующим:


  1. В глобальном неймспейсе (имена веб-компонент) используются короткие конфликтоопасные имена.
  2. insertAdjacentHTML довольно медленная штука.
  3. В строковых литералах не работает подсветка синтаксиса, автодополнение, переход к объявлению, поиск всех использований, авторефакторинг, проверка типов и тд.
  4. Ручное дёрганье обновлений плохо масштабируется по мере роста числа состояний. В итоге вы придёте либо к полному обновлению всего на свете по любому чиху, либо баги вида "тут что-то недообновилось" будут вашими вечными спутниками. По мере роста приложения вы неизбежно дойдёте до состояния "90% времени тратится на фиксы подобных багов и лишь 10% на новые фичи".
  5. Хранить данные в DOM и вообще взаимодействовать в DOM по каждому чиху — это верный путь к тормозам, как только приложение станет чуть больше, чем пара простых форм и список на 10 элементов.
  6. Ну и, конечно, объединение в кучу всего от стилей до работы с базой приводит к куче лишней копипасты, на поддержку которой будет требоваться всё больше и больше времени. Забавно видеть в статье упоминание SOLID и тут же нарушение первой же буквы S.

Все что вы перечислили, имеет место быть. Но как только вы устраните все эти проблемы, у вас появятся другие, например количество файлов в проекте, и вообще кода — утроится. Покажите мне совершенный реактивный код подобного приложения. Можно и бенчмарк сделать, на предмет медленного DOM.

Покажите мне совершенный реактивный код подобного приложения
Вот это вы зря!

Не по теме поста, но на дисплее 1600x900 оказывается невозможно удалить заметку, потому что кнопка удаления не влезает, а горизонтальная прокрутка не работает

Ага, теперь я худо-бедно научился пользоваться этой прокруткой, но такое использование scroll-snap всё равно жутко неудобно

Извините за, возможно, неуместный вопрос. Но в чем проблема количества файлов в проекте, когда они удобно организованы?

Я очень ценю локальность. Когда весь код компонента перед глазами, и стили и разметка и логика. И желательно не более 200 строк на компонент. Когда-то такой паттерн явисты продвигали, и я с тех пор стараюсь следовать.

В этом коде перемешано создание UI и логика работы с данными. Это плохо, потому что не позволяет менять одну часть, не трогая другую.


Кроме того, а как вы тестируете компоненты написанные таким образом?

Положить объект в БД в исходном виде это не логика работы с данными, это часть интерфейса, имхо. Не представляю кейса когда придется менять одно, не трогая другое. В сложных формах это бы имело смысл.

Что конкретно вам не нравится?

Найболее отвратительно — полное отсутствие статики. Куча каких-то непонятных строк, которые вызывают строки и которые возвращают строки. Что это за кошмар?
APP.db.transaction("Objects").objectStore("Objects").openCursor(null,'prev')


А также «sep», «new», «msg», «obj-new-medias» и так далее — строки, строки, строки, строки. Все одинаковые, все ничего не говорят, все захардкоджены прям внутри кода. Некоторые ещё и повторяются несколько раз.

Как только я такое вижу — помечаю код как жуткое легаси с которым запрещено работать до полного и нормального переписывания.

Дело тут не столько в кодировании, сколько в отсутствии даже намёка на проектирование. Грамотное проектирование в принципе не допускает появление подобного кода. Фреймворк должен ограничивать такое кодирование определяя некоторые правила для написания сигнатур. В крайнем случае предлагать автоматизировать рутинные операции.
Проектирование как бы подразумевает декларативный подход. Если много синтаксического шума, то это явный и чёткий признак некачественного проекта. Это можно увидеть даже не понимая о чём собственно код. Насчёт коротких алиасов. Я — за! Но только в локальных нейсмпейсах. vintage знает о чём пишет.


Грамотное проектирование в принципе не допускает появление подобного кода
Ну это ведь тоже слишком абстрактно. Какого «такого». Я понимаю, что код плохой и так писать нельзя, но какие яркие признаки есть того, что код плохо

Только для меня — избыточность, линейность и императивность наиболее яркие признаки плохого ПТУ стайл кодирования. api github это хороший пример

Грамотное проектирование в принципе не допускает появление подобного кода
Ну это ведь тоже слишком абстрактно. Какого «такого». Я понимаю, что код плохой и так писать нельзя, но какие яркие признаки есть того, что код плохой?

Все короткие имена — локальные, внутри класса. Кроме собственно имен веб- компонентов, которые через дефис, на это я повлиять не могу — стандарт.

api github это хороший пример

Вот не сказал бы. Они вываливают клиенту 100500 полей, часть из которых дублируется. А потом прикрывают это дело довольно жёсткими лимитами.

плохого ПТУ стайл кодирования … это хороший пример
«Например, реактивная парадигма прекрасно выглядит, если наш стейт централизован (не случайно самый популярный стек это react / redux), а если он распределен по дереву компонентов (что архитектурно правильней)» — Вот всю жизнь у меня каждый компонент отвечал за то, что его волнует… В итоге большое приложение (800 вьюшек) абсолютно нормально и масштабируется и дорабатывается… как-кому не скажешь, первый вопрос «А почему стейт не глобальный и отдельный — так правильнее!» Почему правильнее… непонятно мне до сих пор
Да вроде с redux уже жестко разобрались, и на хабре статья была. В вакансиях он встречается, я так понял, в унаследованных проектах.
А с чем сейчас начинают новые проекты на Реакте?
Что-то я сомневаюсь, увы. Слишком сложный стек для редаксокодеров.
Ну для них то понятно, я говорю только про сильных разрабов, а не про дилетантов. Если React проект стартует сильный разраб, то понятно что он возьмет MobX, а если слабый, то понятно что Redux. Но это же не значит что сильные разрабы должны равняться на слабых и преднамеренно лишать себя крутых благ и стартовать проект на дерьмовом стеке)
Если WebApp проект стартует сильный разраб, то понятно что он возьмет $mol, а если слабый, то понятно что React. Но это же не значит что сильные разрабы должны равняться на слабых и преднамеренно лишать себя крутых благ и стартовать проект на дерьмовом стеке)

Ох уж этот парадокс Блаба.

Потому что effector слишком примитивный, к тому же иммутабильный и не реактивный (pub-sub в таком «тупом» виде не считается реактивностью в JS), отсюда и не удобный в применении. А MobX наоборот, выжимает из JS все соки, мутабильный и по настоящему реактивный, поэтому максимально удобен в применении. Какой смысл преднамеренно себя ограничивать и пользоваться тем, что заведомо обречено быть ошибкой выбора на этапе проектирования?

Первые впечатления на пэт проекте от него неплохие. Возможно, вы давно его пробовали?
Но спасибо за ваш ответ — попробую и mobX.

Ваше мнение насчёт context.provider? Во флаттере форсится почти как стандарт, почему в реакт нельзя использовать только его ?

Потому что стейт менеджмент реакта никчемный и крайне неудобный, связка с MobX выносит реакт на совершенно другой уровень, эту связку можно приравнять к другой технологии, он с ним наконец-то стал реактивным. React Context provider я использую только когда мне надо прокинуть родительский стейт(mobx разумеется) дочерним компонентам на разном уровне вложенности и не заниматься props drilling'ом.

Спасибо, понятно. Поэтому я и не люблю реакт, слишком много нюансов и противоречивых подходов. Посмотрите как чисто то же самое реализовано на флаттере, который некоторые считают форком реакта. Теперь на нем и вебню можно писать:
https://m.habr.com/ru/company/piter/blog/503074/

Писать вот такой код, нет спасибо) Я как нибудь покайфую лучше с React+MobX
class MyBloc {
  final _controller = StreamController<MyType>();  Stream<MyType> get stream => _controller.stream;
  StreamSink<MyType> get sink => _controller.sink;
  
  myMethod() {
    // ВАШ КОД
    sink.add(foo);
  }  dispose() {
    _controller.close();
  }
}
Пример виджета, потребляющего  BLoC:
@override
Widget build(BuildContext context) {
 return StreamBuilder<MyType>(
  stream: myBloc.stream,
  builder: (context, asyncSnapshot) {
    // ВАШ КОД
 });
}

Вот глянул ваш код — попробую реализовать на effector на днях.
Что бросилось в глаза —
export default 
— лучше не стоит, а то во время рефакторинга на это хорошо так напороться можно.
Из этого списка так и не понятно, на что можно напороться при рефакторинге? Если при рефакторинге вы используете текстовый поиск, вы также напоритесь и с именованными экспортами (имя можно поменять при импорте, или разные модули могут экспортировать одинаковые имена). А если пользуетесь семантическим поиском, то ему какая разница?
Какой поисковик? Нормальная IDE сама ищет, показывает и заменяет.
имя можно поменять при импорте, или разные модули могут экспортировать одинаковые имена

Вот только это происходит явно, чего не скажешь об export default
Нормальная IDE сама ищет, показывает и заменяет.

Что ей мешает искать дефолтные импорты?

Вот только это происходит явно, чего не скажешь об export default

Что вы подразумеваете под словом «явно»?
import Component from './Component';

Вполне явно импортирует с именем Component.
Вот именно, при дефолтном экспорте теряется явная связь с импортом.
Явно — именованый экспорт. Примеры имеются по ссылке, что выше скидал.
Напоретесь разок — поймете.
Но желаю вам никогда с таким не сталкиватся!)
Объясните, пожалуйста, что значит «явная связь»? Почему в моём примере она неявная? И почему это вообще важно?
По ссылке всё прочитал, но не понял, о каких проблемах с рефакторингом вы говорите.
Напоретесь разок — поймете.

Хотелось бы понять, на что я могу напороться до того, как это произойдёт.
Вот первая ссылка с гугла, которая раскрывает суть.
Не знаю, как лучше донести смысл, который я пытаюсь вложить в мои сообщения.
И по этой ссылке, также не объясняется, как дефолтный экспорт мешает рефакторингу.

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

Какая разница, как называется импортируемый модуль, если IDE ищет семантически? К тому же, именованный импорт тоже может называться по-разному.
Главная проблема в том, что дефолтный импорт не имеет «своего» имени. Обычный импорт можно переназвать, да, но это порождает след в виде «as». Переименование дефолтного импорта ничего не порождает. Перефразируя классику,
// true.js
<...>
export default true;

// app.js
import false from 'true';
import true from 'false';
// happy debugging!

И да, я безусловно согласен с вами, что IDE как раз с этим неплохо разберется. А вот люди читающие код — уже гораздо хуже.

Вам что-то хотя бы отдаленно похожее приходилось видеть?

Тогда чем это будет отличаться от
import { true as false } from 'true';
import { false as true } from 'false';
И по этой ссылке, также не объясняется, как дефолтный экспорт мешает рефакторингу.

Если вы через рефакторинг переименовываете класс в одном месте — он не переименовывается во всех остальных. Как результат переименование часто используемого класса становится ужасным испытанием.

Использование именованных экспортов не решает эту проблему полностью. Импорты с "as" тоже придется править руками. И на практике, скорее всего, их просто забудут исправить. Вместо решения, вы бежите от проблемы, создавая новые.


Полный путь к файлу создает уникальный скоуп для всего содержимого. Всё в файле должно именоваться относительно этого контекста. Тавтология не создаст глобально уникальных имен (если только вы не в несете полный путь в каждое имя).

Использование именованных экспортов не решает эту проблему полностью
Оно решает её достаточно

Импорты с «as» тоже придется править руками

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

Во-вторых, у нас не принято использовать as.

Вместо решения, вы бежите от проблемы, создавая новые.
Какие именно «новые» проблемы созданы? Это проблемы старые — с дефолтными импортами они остаются.

Тавтология не создаст глобально уникальных имен

А нам и не нужны глобально уникальные имена. Важно писать так, чтобы они были уникальные в местах, где используются. То есть не импортировать в файл два класса с одинаковыми названиями из разных мест.
Оно решает её достаточно

В таком случае её можно было и не решать вообще. Переименовывание и так достаточно редкий кейс. И, повторюсь, при вашем подходе, вы с большой вероятностью забудете поправить `as` импорты, потому что будете полагаться на IDE.

Ну, во-первых, таким импорты будут явно указаны,

Опять это слово «явно». Чем `import Name from` неявен? Тут явно написано, что объект, экспортируемый по дефолту, нужно импортировать с именем Name.

плюс в начале файла можно будет увидеть родное название.


И в дефолтном импорте можно увидеть название файла.

Какие именно «новые» проблемы созданы?

Отсутствие строгой системы в именовании.

Важно писать так, чтобы они были уникальные в местах, где используются.


Т.е. вы про каждый экспорт должны думать, а где и с чем вместе он будет использоваться? Экспорт — такое же локальное имя, как и всё остальное в файле. Он находится в том же смысловом контексте, и должен именоваться аналогично со всем остальным, без добавления контекста в имя. Иначе возникает неконсистентность в именах, которая всё равно не избавит от коллизий имён, а значит `as` придётся использовать, и все проблемы никуда не деваются. Вы не решили проблему, а замели пыль под кровать.
Ну да, контекст, на Flutter тоже рекомендован. Это как раз в духе того что я написал — в дерево элементов засунуть специальный элемент, назвать его контекст-провайдером, и использовать по ссылке. Я вместо этого использую веб-компонент с доступом по ID, или через события, если данные нужно снизу вверх поднять. Это уже не чистый декларатив, а смесь.
Главная проблема при работе с контекстом — это поиск shared state и общего предка. Как нам упростить эти шаги? Так как у нас реакт-дерево — это дерево, то у всех компонентов есть общий предок. Давайте хранить все shared state в root'овом компоненте.
Это слишком радикальный прыжок разума ) Поиск общего предка — это не проблема, это задача, если мы хотим слепить приложение из чужих компонентов, или хотим компоненты переиспользовать многократно, или хотим их менять в процессе эксплуатации, короче когда нам нужен локальный стейт с понятным временем жизни.
Это я скинул ответ d41d8cd98f к тому, что лучше использовать стейт менеджер. Почему — там расписано)
Вообще меня впечатлил effector и его философия мультисторов, где в принципе нет проблемы с локальным стейтом, да и общие предки в нем не нужны.

Вот ещё смотрю mobx — пока только изучаю, ничего касательно него сказать не могу.
Я буду мужественно грызть кактус использовать контекст. С флаттером получается идентичная архитектура, а я как раз его сейчас изучаю, делаю сравнение Flutter vs Ionic React на простой прикладной задаче.
Главное не забудьте, что когда вы неиронически начнете обсуждать правила именования пропсов в глобальной свалке контекста (чтоб выделить некие неймспейсы) — вы уже зашли слишком далеко :-)

Напомните, какие сейчас лучшие практики для серверного рендеринга приложения, использующего web components?

Нет таких, сейчас веб-компоненты не поддаются SSR. Есть некоторые наработки по гидратации в lit-html, есть @skatejs/ssr, но, насколько я знаю, это всё пока на ранних экспериментальных стадиях.


Сейчас всё упирается в императивность создания Shadow Root. Есть пропозал на его декларативность, но он пока не особо куда-то движется.

А какие реактивные технологии вы пробовали? Упомянутые реакт и редакс к реактивности не имеют никакого отношения.

Относитесь к redux-like стейту не как к централизованному хранилищу состояния компонентов, а так же, как к СУБД на бэкенде. Никто же в здравом уме не пишет sql-запросы в Presentation слое. Тот же подход сработает и здесь.

На мой взгляд, так себе подход. Сохраняя состояние приложения в DOMе вы смешиваете логику с представлением. Вот проблемы которые сразу приходят на ум:
1. Как сделать разный UI для разных девайсов? Например, для десктопа показать форму простыней, для мобильных ту же форму в виде визарда. В вашем случае придется переписывать не только вью, но и всю логику.
2. Как одним махом сохранить текущее состояние приложения? Например, при логауте сохранить при следующем логине вернуть пользователя в то состояние, где он нажал логаут.
3. Как делать переходы от экрана к экрану без перезагрузки страницы? Если логика зашита в DOM вам придется при каждом переходе восстанавливать нужное состояние.
4. Сложности при переносе наработок от одного проекта в другой. Довольно часто, одинаковая функциональность имеет очень разное представление от проекта к проекту.

Наконец-то сильные аргументы !


1) Отвязать DOM от HTML-верстки, не должно быть такого, что при смене диагонали у вас иерархия компонентов поползла. Такая отвязка даже на уровне CSS работает — флексбокс хотите в столбик, хотите в ряд. Визард vs простыня — согласен, придется полностью переписать. Но это и так почти все делают, тот же мобильный Хабр, насколько я понимаю, другой. Да и в визарде логикика переходов будет другая, значит и стейт. Может кто-то и делал две версии на одной кодовой базе, я так не умею ).


2) Только писать свой сериализатор, технически браузер позволяет весь DOM в переменную JS сохранить, а потом обратно восстановить в document, но во-первых переменную эту штатно не сериализовать, во вторых все равно слетают значения контролов формы, так что тут реактивность бесспорно выигрывает.


3) Скрывать и отображать дивы. У меня сделаны переходы и в стиле визарда (при создании новой карточки) и в стиле стека (при редактировании старой карточки). Зайдите и посмотрите как меняется location.hash при обеих типах навигации. Поэтому для возврата назад или закрытия модальной формы мне достаточно сказать history.go(-1), и все присходит автоматически — модальная часть DOM уничтожается, а линейная просто скрывается, отображая предыдущий DIV. Тут как раз большой плюс, что сохранено все, включая контролы и позиция скролинга. Да и навигация быстрее в разы получается.

1) Логика переходов не должна как-то влиять на данные. Т.е. у вас есть некий сет данных который вы отправляете на сервер. Как этот сет комплектуется, визардом за несколько шагов или простой формой за один раз, роли не играет. В любом случае, эта задача решается элементарно, если слой данных у вас живет отдельно. Там где CSSа недостаточно, просто, делаете 2 разных визуальных компонента и используете их по условию с одним и тем же набором данных, безо всяких плясок с бубном. Логика переходов запросто может быть внутри отдельного компонента.

2) Весь DOM в переменную JSON, это вы хватили)

3). Как быть если пользователь напрямую по ссылке зашел на какую-то промежуточную страницу и history.go(-1) никуда не ведет? Тут как раз, механика роутов которая есть во всех популярных библиотеках очень вам поможет.

Вы правы. Но у меня была цель именно обойтись ванилью. Из принципа. Иначе статья зачем )

Присоединяюсь к комментирующим — пост полон непродуманных утверждений.
— «тотально доминирует реактивность» — совсем не тотально, только в наиболее продвинутых с технической точки зрения проектах.
— «проблем вроде централизованного хранилища данных» — проблемой является по большей части как раз распределенное хранилище, разбросанное по разным компонентам и базам. Централизованное же предоставляет удобную возможность обращаться за актуальными данными из любого места, делать «слепки состояний», точно знать какими данными оперирует приложение и удобно рефакторить.
— «почему собственно данные должны храниться в объектах JS, а не напрямую в узлах DOM?» — концепт переноса операций с данными и хранилищ в JS — молодой, до этого состояние хранили как раз в дата-атрибутах и была масса фреймворков с подобной схемой работы. Однако медленность, необходимость постоянной сериализации-десериализации и работа по неудобному DOM-апи (вместо js-методов) неизбежно привели к выводу, что разрабатывая на JS, нужно там же и хранить данные, а в браузерное представление трансформировать скрытым реактивным механизмом как сайд-эффект. Присопособленность терпеть недостатки выдает в вас олдскул, так как раньше действительно технологии не позволяли подобного)
— «подобная реактивность вообще не часто востребована» — даже не могу найти примеров, где не востребовано подобное удобство. На работу можно доехать и на «запорожце» (вручную работая с DOM), но других людей (команду) возить стыдно.
— «функциональщина + виртуальный DOM — вещи не бесплатные» — не знаю что значит «функциональщина», но сравнить пару объектов явно дешевле работы с DOM.
— «компоненты, управляемые разными реактивными библиотеками» — если у вас есть такое в проекте, выбрасывайте, а не ищите способы скрестить ужа с ежом.

Примеры кода даже комментировать не хочется, в 2005 мне бы понравился — прямо как в учебниках, createElement, prepend-append, event-driven модель передачи событий, верстка в строках… Для первого проекта — самое то, но не верю, что серьезно топите за это все.

Спасибо за комментарий!
PS
Вернуться в олдскул меня побудила печаль с производительностью неудачных реактивных проектов. Императив оказался быстрее. Возможно я плохо готовил реакт, и нужно вернуться к снаряду ещё раз.

Попробуйте. Видимо, вам попадались проекты на Redux — я тоже недавно ушел из проекта, в котором на каждое вбивание символа в форму весь сайт с тысячами компонентов (виртуальная библиотека) ререндерился 17 раз, и это пару лет работало в продакшене, с неплохой посещаемостью. То есть с одной стороны перфоманс переоценен — если продукт действительно полезный и решает проблемы людей, то хоть вот так вот может работать. А с другой стороны — самый лучший проект — тот, который написан грамотно и заботливо, а не на хайповых технологиях, и 10-15-летние старые проекты на vanilla и сейчас дают фору кое-как слепленным из опенсорса монстрам (к слову, бандл позапрошлого проекта в несжатом виде был 97 мегабайт, и собирался соответствующее время, тоже реакт-редакс).

В этом смысле олдскульные практики экономить каждый килобайт и делать css-only-интерактив (модалки, слайдеры и тп) вместо js, да и еще nojs-версии, вызывают добрую ностальгию. Но подойдите к снаряду еще пару раз — я привел реальные крупные примеры, но каждый из них удалось сокращать раза в 3 и делать удобным в разработке и быстрым как раз благодаря современным практикам реактивности.
Это вообще вечный архитектурный спор за локальность против глобальности. Микросервисы против монолитов, композиция против наследования, локальный стейт против глобального и т.д. На бэкенде модно, чтобы каждый микросервис имел собственную БД, теперь и на фронте пошла тема за «микро-фронтенды». Хуже всего, когда коммерческий проект огромный и чужой, но нужно всунуть туда свою страничку. Пусть это обзовут кривым дизайном, но когда у меня однофайловый веб-компонент, со своими стилями и собственным сториджем, мне только нужно договориться об интерфейсе событий, и я за его судьбу спокоен — нет зацепленности, нет зависимостей, ошибкам негде жить. А так Вы правы конечно, на реактивное приложение нужен грамотный архитектор. У меня такого не было, вот и результат.
Ковырнул Ionic-React, демо-приложение. Все модно, на хуках, более ли менее красиво — но релизная сборка:
2.3 мегабайта мимифицированного JS, побитого на 100 файлов. Приложение просто делает фотки и кладет в базу, у меня функционал в три раза больше, а размер исходного JS — 27 килобайт. Если б на тайпскрипте сделал — было бы 15 килобайт.
Разница в 85 раз, Карл ! И это еще без редакса с мобиксом.
Для сравнения — на флаттере приложение 1.5 мегабайта одним файлом.
Я ничего не имею против реакта с иоником, и даже наверное буду продолжать на них писать, но это явно инструменты для богатых. Я понимаю, функциональный код проще тестировать, меньше ошибок и т.д. Но линукс написан на голом СИ, и до сих пор работает )
PS
Объем исходного кода одинаков — что на ионике, что на ванили, на ионике даже немного больше за счет длинных имен.
2.3 мегабайта — это очевидно за гранью разумного, в плане объема кода при небольшом функционале ванилла или Svelte выигрывают значительно. Но по мере усложнения приложения эта разница становится обратной, так что лендинги и простые сайты я продолжаю делать на нативном js, но для корпоративной разработки — только React+MobX.
Просто Ionic оборачивает все браузерные API (медиа-девайсы, местоположение, IndexedDB, LocalStorage и проч.) в свои собственные абстракции, чтобы на всех платформах было одинаково. Как пример — я через Ионик сохраняю файл, а под ковром он создает IndexedDB базу с именем Disk, и туда все файлы в виде блобов складывает. В итоге Flutter WEB получается выгодней, да и красивей у него интерфейс. Из фреймворков посматриваю на Stencil, очень близко к ванили, но при этом реактивно.
Stencil это вебкомпоненты, а вебкомпоненты сейчас — не самая беспроблемная штука. Когда разрулят вопросы с модульностью цсс и наконец-то зафиксируют стандарт — будет гораздо интереснее.

Пока что, на мой взгляд, если уж хочется протащить что-то в достаточно серьезный продукт (в «наколенное» и мелкое — вебкомпоненты скорее всего зайдут без проблем), то лучше брать цельное вендорское решение а-ля Svelte, чем опираться на несколько сырых стандартов сразу (webcomponents, shadow DOM), плюс еще и синтаксически кривого монстра JSX.

ЗЫ: Хотя конечно Svelte тоже собственный синтаксис изобретает, и это тоже такое себе. Короче, нет счастья.

К слову, вся библиотека jQuery уже меньше чем типичная сборка React-приложения. Люди хотят реактивности там где она не нужна, а тем кому она нужна но при этом хотят реализовать это в "Vanilla-way" — изобретают велосипеды. И всё это добро загружается не за секунду.

Состояние веб-приложения — это переменные JS, а DOM является лишь отображеним, которое нужно уметь «умно» пересоздавать. Идея хорошая, но почему собственно данные должны храниться в объектах JS, а не напрямую в узлах DOM

Потому что DOM для этого не предназначен, и хранить там можно только строки?

однако к сожалению функциональщина + виртуальный DOM — вещи не бесплатные

Бенчмарки предоставьте пожалуйста.

Функция querySelector() представляет собой великолепное API для адресации компонентов

А ничего что для ее использования надо сканировать документ на каждую операцию, что на порядки дороже прямой адресации по известной ссылке? А то что idшники должны быть UUID, иначе не избежать конфликтов?

Код из подобной лапши перестает быть поддерживаемым сразу, как только там становится больше одного дива.

Реактивный код можно вполне писать без блевотной редуксо-лапши, посмотрите например на тудушку на Scala.js github.com/Karasiq/scalajs-bootstrap/blob/master/test/frontend-v4/src/main/scala/com/karasiq/bootstrap4/test/frontend/TodoList.scala ( karasiq.github.io/scalajs-bootstrap/index-v4)
Хороший функциональный код легко понятен и правится даже через 20 лет, потому что имеет совершенно очевидный «flow» и лишен мусора из абстракций, наследования, фабрик фабрик фабрик и т.п., и при этом не ломается от изменения одной строчки или значения переменной как императивная лапша.

В dom можно хранить что угодно, за ссылку спасибо, посмотрю.

Может вам svelte попробовать? Компилируется в чистый js, компоненты есть и они сразу js+html+css, никакого оверхеда на shadow dom / event loop / etc.

Однозначно. Потому что с веб-компонентами не все хорошо например у FF. В этом конкретном проектике я на лису вообще не расчитывал, т.к. она не умеет красиво фотографировать, а выдирать фото из нефокусированного видео-потока это такое себе развлечение. Про Стройного слышу только положительные отзывы, спасибо.
Прочитал начало статьи и могу сказать, что статья ради статьи.
функциональщина + виртуальный DOM — вещи не бесплатные, они существенно нагружают и процессор и сборщик мусора. Иначе бы не придумали SSR.
автор это бред. ssr создан, чтобы быстро показывать контент в виде html+seo. Загрузка скриптов и их парсинг требует времени, особенно на мобильниках. Если вас не устраивает фунциональное программирование, глобальный store и виртуальный DOM — angular + mobx
Статья, как правильно уже сказали, ради статьи.
Идея хорошая, но почему собственно данные должны храниться в объектах JS, а не напрямую в узлах DOM?

Потому что JS, благодаря JIT-компиляции — довольно быстрый, а DOM — всегда медленный. Храня данные в JS, с ними можно успевать делать в десятки и сотни тысяч раз больше действий, не вызывая видимых просадок производительности относительно DOM.
Разумеется, если у вас страница «просто показывает данные», причём очень небольшое их количество за раз — храните всё в DOM, оно будет иметь право на жизнь. Важно только помнить, что такая ситуация не у всех и не везде.

Мне же нужна реактивность компонентов, а не реактивность HTML.

А мне вообще нужна реактивность моделей, потому что имея её — можно далее относительно несложно подключиться к любому ЖЦ любых компонентов, реакт это или не реакт (причём, кстати, к реакту подключаться наверное сложнее всего, спасибо его собственным загонам про виртуальный DOM).
Главная соль реактивности вообще не на уровне view, а именно на уровне model и controller.

Иначе бы не придумали SSR.

Главный драйвер SSR — это три магические буквы, среди которых есть буква S, буква E, и буква O. А не соображения производительности, это-то как раз вторично.
Идея писать на JS в ООП-стиле довольно интересна, но вот смешение в одну кучу JS-объектов и DOM выглядит очень сомнительно. Благодаря ковид-изоляции сам решил недавно поближе познакомиться с JS, попробовав сделать небольшой проект на JS и CSS без HTML. Проект представляет собой минималистичный Excel, поддерживающий операции +, -, *, / и ^ над числами и ячейками, сохраняющий документы в локальных файлах *.json с возможностью их открытия. Исходный код доступен по адресу https://github.com/leossnet/bizcalc.

В проекте все компоненты, как визуальные, так и для работы с данными, реализованы в виде классов. Визуальные компоненты расширяют либо HTMLElement, либо специализированные стандартные компоненты. При этом конструкторы визуальных компонентов содержат ссылку на корневой объект приложения, в котором в Map-кеше хранятся все визуальные компоненты.

Идея регистрации компонентов в кеше, заключается в том, что доступ к объектам осуществляется по ссылке, и если в качестве ключа в Map использовать имя переменной компонента, то доступ к любому компоненту из любого другого компонента можно получить практически мгновенно путем вызова this.#app.getComponent(compName) вместо использования медленного метода document.querySelector(appSelector).

Регистрация компонентов в приложении, которым является объект класса App, определенный в файле index.js, производится с помощью вызова следующего метода:

addComponent(componentName, component) {
this.#components.set(componentName, component);
}

В свою очередь внутренняя переменная #components определяется в конструкторе класса App как this.#components = new Map(), которая выполняет роль глобального пространства имен с предельно быстрым доступом к объектам по имени их переменных.

Еще одна оптимизация связана с тем, что самая быстрая обработка строк является на порядки медленнее самой неоптимизированной работы с объектами. Поэтому строки с формулами в момент ввода сразу разбиваются на токены, для каждого из который создается экземпляр класса Token, массив которых также сохраняется в Map-кеше, в котором ключами выступают имена ячеек, содержащих формулы.

В целом идея таких подобных оптимизаций заключается в том, чтобы максимально избегать работы со строками, а также сохранять в Map-кешах ссылки на объекты для быстрого доступа к ним. В общем, если что сказал не так, то не судите строго начинающего JS-писателя.
Баловался когда-то экселем, получив шикарный перфоманс. Но формулы писались сразу в синтаксисе JS, и переводились в код с помощью new Function(). По поводу хранения стейта, понимаете, плоский хэшмэп это шаг назад по сравнению с деревом. Вообще, парадигма MVC сильно переоценена, и сейчас наблюдается некоторый откат к толстым компонентам. Потому что разделяя модель и представление, вам приходится вручную управлять иерархией и временами жизни и вьюшек и моделей. Сложность предметной области удваивается — модели имеют свою иерархию и свой жизненный цикл, вьюшки — свою. В простых случаях можно все модели свалить в «вечный» хэшмап, а если их 1000? Нужно создавать фреймворк по управлению моделями. А потом его реактивно подружить с фреймворком по управлению вьюшками (условный Реакт).

Вот и возникла идея — а давайте будем размещать и модели и вьюшки в едином дереве компонентов с автоматической деструкцией. Получился паттерн контекст-провайдер, наиболее красиво реализованный во Flutter, ссылку на хорошую статью уже приводил habr.com/ru/company/piter/blog/503074
Но тогда получается, что моделька — это просто дополнительный узел в дереве виджетов, и грань между виджетом-моделью и виджетом-представлением сильно истончается. А следующим логическим шагом будет объединение этих 2-х виджетов в один «толстый» компонент, как это сделано в моем коде.

Ну то есть в индустрии существует вечный спор — на какие части нарезать изначальную сложность бизнес-задачи. Можно по технологическим признакам (хранилище — модель — контроллер — вью), можно по бизнес-ответственности (справочник клиентов, операция закупка, операция продажа), а больших приложениях всегда режут и так и так — горизонтально по технологиям, вертикально по функционалу, называется матричная схема. В относительно компактных проектах достаточно оставить только вертикальное деление, то есть толстые автономные однофайловые компоненты. Это уменьшает количество сущностей, файлов, и служебного кода. И делает приложение более живучим, когда каждый компонент содержит внутри логику собственного сториджа, реконнекта к другим компонентам, визуализации и проч.

Подобный подход (толстые автономные компоненты) в свое время был популярен на бэке, называется микросервисы. Каждый микросервис автономен, живуч, и имеет собственную БД. Отдельная БД на сервис, по сути инкапсуляция сториджа внутри сервиса — это выглядит странно (всегда было принято уровень хранения выделять в отдельный слой приложения), но тем не менее куча людей эти микросервисы форсят (я нет). Так же и толстые веб-компоненты имеют право на жизнь в определенных случаях. Причем, такие толстые model-view компоненты внутри могут быть сколь угодно сложными и реактивными, но извне выглядят как монолит.
Соглашусь, что архитектура приложения во многом определяется предпочтениями разработчика и особенностями предметной области. Тем не менее, кеширование компонентов в Map является самым быстрым механизмом доступа к ним, а чтобы избежать переполнения кеша, можно использовать инкапсуляцию, логически разбивая компоненты на группы, которые внутри такой группы много взаимодействуют либо имеют общую логику, а вне группы взаимодействуют только в рамках событийной модели.

С другой стороны, смешение в одном компоненте всевозможного функционала с какого-то момента резко ухудшает возможности масштабирования проекта. Даже если разработчик один. Просто в силу ограничений человеческого мозга. Одно дело, когда в одном классе присутствует код, решающий конкретную задачу. И совершенно другое дело, когда в одном классе присутствует самый разнообразный код. В этом случае как минимум нужно постоянно держать в голове, с какими функциями нужно работать сейчас, а на какие не нужно обращать внимания.

Да, инкапсуляция кода добавляет как минимум один дополнительный уровень абстракции. Если приложение небольшое, что эта абстракция на начальном этапе выглядит явно избыточной. Но в жизни аппетит приходит во время еды. И если изначальная идея приложения оказалась востребованной, то постепенно появляются все новые хотелки по расширению функционала. При изначально модульной архитектуре добавление нового функционала представляет собой просто рутину. В случае же монолита даже самая мелкая доработка представляет собой самый настоящий головняк с непредсказуемыми последствиями от внесения даже небольших изменений в код.

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

Ну и да, по поводу внешних библиотек. Лично я придерживаюсь разумного компромисса между полным контролем кода, когда пишется все свое, и использованием простых и надежных внешних библиотек. Этот компромисс исходит из затрат времени на длинном горизонте планирования. С одной стороны, на начальном этапе нужно потратить много времени на написание собственных библиотек, но затем можно практически не вспоминать про них. С другой стороны, быстрое подключение сторонних библиотек со временем будет требовать все большего времени на изучение появляющихся новых возможностей и ошибок в этих библиотеках с необходимостью последующей адаптации собственного кода.

Сейчас модно использовать специфичные методы компонент — хуки и через них осуществлять как запуск так и получать параметры. И view компоненты и компоненты модели имеют свои хуки, которые имплементируют интерфейс контроллера для работы с таргетным объектом. Например, в одном случае в результате работы специфичных хуков разных таргетных объектов на выходе получается рендер для вью, в других AST для модели. Интерфейс, реализуемый в хуках, как абстракция используется для взаимодействия функциональных компонент контроллера с таргетным объектом. Хуки — это слой контроллера, который доступен в М-V компонентах по ссылке. Таргетные объекты могут меняться, но интерфейс остаётся тот же самый. Расширение функционала идет за счет добавления M-V компонент со своими хуками и добавления компонент контроллера, с новыми интерфейсами, который нужно имплементировать через хуки. Есть также любители взаимодействия через events bus. Ничего из этого я не увидел в примере или всё сильно замаскировано:)

Хуки во флаттере появились только что, похоже Гугл сам не определился еще со своим продуктом.
PS
Однако, ваш комментарий крут )

Что значит реактивные технологии переоценены?

Реактивность — это не технология, это простенький паттерн, который юзали все и везде, до того как обхайпованные придумали ему название. Называть это технологией, всеравно что называть технологией 2+2=4
Всё зависит от поставленных целей и задач
И на Ассемблере сейчас пишут, и на языках высокого уровня
Если есть группа среднего уровня спецов — берется какой-нибудь фреймворк
Если один профессиональный программер — можно писать код ванильно и эффективно (а не олдскульно, как выше обозначили)
Какие проблемы?
Для больших расширяемых командных проектов фреймворки однозначно удобней.
Мощности машин позволяют терять 99% времени CPU и памяти именно на сопутствующих фреймворкных процессах, а не бизнес-логике приложения.
Когда ты едешь на автомобиле из точки А в точку Б, ты тоже тратишь 95% энергии топлива на по сути бесполезное перемещение груды железа, а не тебя.
Добавил бы следующее: писать код ванильно эффективнее, но дороже и дольше. Ваш пример с автомобилем — можно пешком, но долго и мучительно
можно писать код ванильно и эффективно (а не олдскульно, как выше обозначили)
Это как? В приведенном коде всего 2 проблемы — совмещение логики сториджа с логикой отображения в одно толстом компоненте (хотя последняя тенденция в том же Vue — делить не по технологиям а по ответственности, отсюда и однофайловые компоненты), и использование строк вместо статики. Для маленького приложения это норм. Логика работы с БД выносится на узел выше, по аналогии с Flutter Provider или реактовым useContext() за 10 минут, не ломая при этом всю архитектуру. При этом у меня получился несжатый бандл в 18.5 раз (!) меньше чем упакованный бандл стартового приложения React. Плюс при навигации страницы не рендерятся заново, поскольку целиком хранятся в DOM а не в State, а это означает лучшую отзывчивость.
PS
Я работал на действительно больших проектах, когда в БД полторы тысячи таблиц. Большинство коммерческих веб-приложений примерно в 10 раз меньше. И да, в том большом проекте не было декларативщины и реактивщины — вполне себе олдскульный императивный ООП.
PPSS
Я люблю фукнциональное программирование, но деньги приходится зарабатывать ООП, увы, потому что приходится работать на высоко-нагруженных проектах.
PPPSSS
Сейчас активно изучаю Flutter с прицелом в том числе на WEB-разработку, и бандл у него на маленьких приложениях где-то в 2 раза больше реактового. Кодирование абсолютно в стиле React )
Я всего лишь подчеркнул, что «эффективно» и «олдскульно» — это различные характеристики
Сейчас принято считать, если ты не используешь популярные фреймворки, то это олдскульно (устаревше), хотя, возможно, тебе просто нужна производительность (эффективность)
Да, я провалил один проект на модном фреймворке, теперь, обжегшись на молоке, приходится дуть на воду ). Когда пишешь на низком уровне — я полностью уверен в себе и результат предсказуем. С фреймвокамми не так — нужно постоянно усиленно грести по течению, в том же реакте появляются новые концепции, в результате чего старые становятся неактуальными. Возможно это кстати причина, по которой линуксоиды до сих пор пишут на Си и GTK+ вместо модных C++, Rust и Qt. Похоже, сейчас мы видим некую стабилизацию, и контекст с хуками наконец-то станут стандартной архитектурой.
Only those users with full accounts are able to leave comments. Log in, please.