Одна методика изучения позволила мне понять устройство многих сложных программных систем и сводится к тому, что нужно переписать изучаемый проект так, чтобы получился какой нибудь неведомый зверь, который хотя бы чуть-чуть шевелился и был в чем-то схож со своим прообразом.
Мне нужно признаться, что я бэкендер и даже не знаю что меня соподвигло к написанию микрофреймворка на 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. В параметры вызова передается:
- Имя тега.
- Объект со стилями (в формате JS)
- Атрибуты для тега.
- Текст, другой тег или компонент (дочерний) или ничего не передается.
Посмотрим на 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
}
Функция обрабатывает переданные ей параметры в той же последовательности:
- Создание компонента в дереве документа.
- Добавление ему стилей.
- Атрибутов.
- Добавление дочернего элемента в виде текстового либо другого элемента.
При обработке атрибутов также проверяем их тип, если в качестве значения получена функция, то предполагается что это событие и вешаем на него прослушку. Поэтому остается только объявить и реализовать указанную в качестве события функцию.
Вот в этой функции обработки события мы и вызываем 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. Да прибудет с нами сила!
staticlab
Казалось бы, причём тут Реакт?
rusldv Автор
На самом деле реакт в начале своего существования был так же небольшим. Но в данном случае это действительно другой код. Идея в том, что логика компонентов имеет определенные схожие характеристики, такие как возврат из функции объекта компонента и изменение состояния (в данном случае реализовано примитивно).
waltter
Пару лет назад реакт был похож на то, что приведено у автора.
Я потратил несколько часов на его изучение, мне показалось сложно и я ушел в angularjs. Вернувшись в реакт через пару лет, я быстро прошел курс на их сайте, а на следующий день пошел на хакатон разрабатывать на нем приложение, что не составило абсолютно никакого труда.
staticlab
Основной смысл Реакта был и остаётся всё же автоматическая реконсиляция DOM для отражения состояния приложения, а не тупая замена createElement.
И да, пару лет назад вполне уже были babel, webpack и jsx. Нужно заглядывать в прошлое глубже — лет на 5 назад.
rusldv Автор
Согласен. Я поэтому и обратил внимание, что не так все хорошо с обновлением состояния. Однако для одностраничного сайта мне даже так подошло. Конечно сложный проект на нем не написать. Ну или переписывать setAttr и много всего еще. Однако я и хотел в статье отразить простую идею не вникающую в принципы реальной архитектуры реакта.
В действительности я планирую писать статьи о Golang, там интересная история. Мне выделили определенное финансирование на разработку целого блокчейна для управления жизненным циклом изделия) Поэтому буду писать как оно у меня движется, а комментарии тут очень помогут))
rusldv Автор
У меня похожая история, только наоборот) Сперва angular изучал, даже не знал о существовании реакта. Это где-то летом 2015-го или 16-го было. Написал на нем что-то типа интернет-магазина и потом мне он надоел. А реакт я изучил только весной прошлого года. А сейчас сижу юзаю svelte. Хотя все же основная моя сфера это Go, C и PHP. php это вообще с чего я начинал свой путь)