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

JavaScript for Babies

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

Мне нужно признаться, что я бэкендер и даже не знаю что меня соподвигло к написанию микрофреймворка на JavaScript. Но если говорить откровенно, это желание изучить JS.

На самом деле есть хорошая мотивация создавать проекты в виде модулей/компонентов. В представлении бэкенд-программистов данные выглядят в лучшем случае как JSON объекты, они должны быть сформированы в нужную структуру и отправлены куда надо, а там делайте с ними, что хотите. На фронтенде в самом примитивном варианте приходится выбирать по ID нужные HTML-элементы и обновлять их атрибуты, а также изменять текстовые узлы. Облегчают жизнь JavaScript-фреймворки.

Однажды я написал свой PHP-Slim-framework, который далек от оригинального, но мне он реально помогает в PHP-проектах. Сегодня мне хочется рассказать о том как я представил истоки разработки ReactJS. Я написал один файл в 135 строчек кода назвал его bots.js и если его подключить и написать компонент подобно как в React, то можно даже что-то увидеть в браузере. Назвал я его ReactKids.

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

Структура HTML стандартная:

<!doctype html>
<html>
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
		<title>HelloReactKids</title>
		<link href="style.css" rel="stylesheet">
	</head>
	<body>
		<div id="root"></div>
		<script src="js/bots.js"></script>
		<script src="js/pict.js"></script>
		<script src="js/navbar.js"></script>
		<script src="js/label.js"></script>
		<script src="js/button.js"></script>
		<script src="js/app.js"></script>
	</body>
</html>

Для приложения указываем id=root и подключаем bots.js, затем подключаем компоненты (или сами их пишем) и в app.js все это запускаем.

Компонент в ReactKids выглядит так:

function Button(attr) {
	// attribute components default values
	if(!attr.labelButton) attr.labelButton = "Click Me"
	
	return elem(
		"button",
		{
			padding: "0.65rem",
			marginTop: "0.4rem",
			color: "gray",
			border: "1px solid gray",
			borderRadius: "0.5rem",
			background: "#fff",
			fontSize: "large",
			cursor: "pointer",
		},
		{
			id: "btn1",
			click: btn1Click,
		}, 
		attr.labelButton
	)
}

function btn1Click(e) {
	console.log("Clicked!")
	setAttr(Label({labelContent: "i-i-i!!!"}), 0.6)
}

Компонентом в нашем случае может быть только функция в которой params называется attr.
Вот здесь следует уделить внимание для чего этот attr может пригодиться. Ну во-первых, те, кто знаком с реактом знают, что по ним можно «спускать» данные дочерним компонентам. То есть компонент возвращает компонент, который возвращает компонент, и так до компонента у которого нет дочерних. Однако их также используют и в качестве упаковок для данных приходящих от сервера. Запросы на бэкенд по большей части отправляются из функций обрабатывающих события, связанные с взаимодействием с пользовательским интерфейсом.

Когда сервер в ответ присылает JSON (обычно текстом) его нужно превратить в JS-объект и куда-то деть. Вот для этого и требуется params в React и attr в нашей детской реализации.
В attr можно запихивать весь JSON объект, полученный от сервера, а можно только лучшие нужные данные и, возможно, дополненные другими необходимыми.

Дальше следуем логике взрослого React — в начале функции обрабатываем объект attr и выполняем другие хозяйственные дела. После чего нужно возвратить результат вызова функции elem(), реализация которой находится в bots.js. В параметры вызова передается:

  1. Имя тега.
  2. Объект со стилями (в формате JS)
  3. Атрибуты для тега.
  4. Текст, другой тег или компонент (дочерний) или ничего не передается.

Посмотрим на app.js:

var attr = {
	labelContent: "Hello React Kids",
	labelButton: "This button",
}

rend(document.getElementById("root"), App(attr))

function App(attr) {
	return elem(
		"div", 
		{
			fontFamily: "segoe ui",
			color: "gray",
			textAlign: "center",
		},
		{
			id: "app",
		},
		[
			Navbar(attr),
			Pict(attr),
			Label(attr),
			Button(attr),
		]
	)
}

Тут тоже ничего необычного. Вот то же самое посложнее:

function App(attr) {
	var cpic1 = CirclePict({id: "img1", src: "./img/img1.jpg", height: "200px"})
	var cpic2 = CirclePict({id: "img1", src: "./img/img2.jpg", height: "200px"})
	var cpic3 = CirclePict({id: "img1", src: "./img/img3.jpg", height: "200px"})
	
	var txt1 = "Кто придумал детское шампанское. Бывает ли от него похмелье и чем отличается от газировки.";
	var txt2 = "Кто такой Дед Мороз, зачем ему рассказывают стихи и какова максимальная стоимость его подарка.";
	
	return elem(
		"div", 
		{
			fontFamily: "segoe ui",
			color: "gray",
		},
		{
			id: "app",
		},
		[
			Pict({id: "logo", src: "./img/logo.png", height: "90%"}),
			Text({id: "info", text: "you number", direction: "right"}),
			Label(attr),
			Outer({id: "outer1", content: [cpic1, cpic2, cpic3]}),
			Text({id: "txt1", text: txt1, width: "450px"}),
			Button(attr),
			Label({id: "lbl2", labelContent: "Дед Мороз по вызову"}),
			Text({id: "txt2", text: txt2, width: "650px", direction: "center"}),
			RoundPict({id: "well", src: "./img/well.jpg", height: "280px", width: "550"})
		]
	)
}

Как видно мы вложили в компонент Outer 3 компонента CirclePict.

Дети, конечно, заметили отсутствие JSX. На самом деле он придуман ленивыми программистами и просто облегчает то, что мы пишем. В итоге теги JSX все равно превращаются в JavaScript.

Теперь нужно посмотреть как вот это вот реализовано в bots.js. Фреймворк состоит из целых 3-х функций, собственно elem() и setAttr() первая для создания, вторая для обновления состояния компонента и rend() для отображения в app.js.

function elem(elt, style, attr, item) {
	/*element */
	if(elt) {
		// создается узел, как и теги в браузере 
		var el = document.createElement(elt);
	} else {
		console.log("elt fail")
		return 
	}
	/* style */
	if(style) {
		if(typeof(style) == "object") {
			for(var itm in style) {
				el.style[itm] = style[itm]
			}
		} else {
			console.log("style is not object type")
		}
	} else {
		console.log("style fail")
	}
	/* attr */
	if(attr) {
		if(typeof(attr) == "object") {
			for(var itm in attr) {
				if(typeof(attr[itm]) == "function") {
					el.addEventListener(itm, attr[itm])
				} else {
					// standart
					el[itm] = attr[itm]
				}
			}
		} else {
			console.log("attr is not object type")
		}
	} else {
		console.log("attr fail (add ID for element)")
	}
	/* item */
	if(item) {
		if(typeof(item) == "string") {
			var text = document.createTextNode(item)
			el.appendChild(text)
		} else if(typeof(item) == "object") {
			if(Array.isArray(item)) {
				if(item.length < 1) {
					console.log("not items in array")
					return
				}
				item.map(function(itm) {
					el.appendChild(itm)
				})
			} else {
				el.appendChild(item)
			}
		} else {
			console.log("text is not string or object type")
		}
	} else {
		console.log("text fail")
	}
	return el
}

Функция обрабатывает переданные ей параметры в той же последовательности:

  1. Создание компонента в дереве документа.
  2. Добавление ему стилей.
  3. Атрибутов.
  4. Добавление дочернего элемента в виде текстового либо другого элемента.

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

Вот в этой функции обработки события мы и вызываем setAttr(), передавая ей сам объект с обновленным attr. Тут одно но — для каждого создаваемого элемента в attr необходимо указывать id иначе он не будет обновляться через setAttr. Она по id его находит в DOM.

Что касается setAttr() — тут все хуже чем в React, хотя для понимания принципов ее достаточно (ну или почти достаточно).

function setAttr(update, slow) {
	if(slow) {
		var replace = document.getElementById(update.id)
		var opamax = 0.99
		var opaint = 0.01
		var outslow = setInterval(function() {
			opamax = opamax - opaint
			if(opamax <= 0) {
				clearInterval(outslow)
				update.style.opacity = opamax
				replace.parentNode.replaceChild(update, replace)
				var inslow = setInterval(function() {
					opamax = opamax + opaint
					update.style.opacity = opamax
					if(opamax >= 1) {
						clearInterval(inslow)
					}
				}, slow)
			}
			replace.style.opacity = opamax
		}, slow)
	} else {
		var replace = document.getElementById(update.id)
		replace.parentNode.replaceChild(update, replace)
	}
}

Как видно здесь только манипуляции с деревом документа и еще эффект затухания, чтобы хотя бы смотрелось и код был похож на функцию, а не helloworld.

Самая крутая в нашем детском фреймворке это функция рендеринга:

function rend(root, elem) {
	root.appendChild(elem)
}

Мною было замечено, что начинающим программистам трудно начать изучение таких штук как React чисто психологически. Увидев сотни мегабайт библиотек и миллионы строк кода приходится впадать в депрессию и отыскивать себе нечто другое. В частности переходят на Vue. Конечно это тоже хороший фреймворк, но еще лучше понимать оба подхода к разработке фронтенда.

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