Отказ от ответственности: я не следовал стандартной компонентной модели для Интернета, она даже близко не надежна для производства, название может меня немного ввести в заблуждение, а производительность меня не заботила… вообще!
Цель: написать Javascript-библиотеку, которая могла бы давать почти такой же результат (тот же? Правда? Немножко претенциозный?), что и Angular или React, а значит имеет:
- состояние: экземпляры, которые запускают новые рендеры при обновлении его значений;
- компоненты: основные объекты наследуются от одной и той же «компонентной структуры» и совместно используют функции рендеринга, конструкторы, деструкторы (не нативные для JS), управление событиями, сборщик мусора (связанный только с областью действия компонента, поскольку в JS уже есть сборщик мусора), синтаксический анализ HTML , и т.д. (= возможно, я добавлю сюда что-то позже);
- HTML-код «отделен» от JS-кода.
TL:DR: Фрагменты HTML создаются из каждого экземпляра компонента, вызываемого из узлов, расположенных ближе к корневому узлу; каждый раз, когда «какой-то HTML» не отображается, это означает, что по крайней мере один экземпляр компонента не нужен, поэтому мы отфильтровываем его из списка всех экземпляров компонента. Самые важные функции не являются методами класса:
1 — replaceComponentDeclaration: отвечает за построение DOM, вызывается из корня;
2 — parseForEvents: вызывается для отображения ВСЕХ событий, прикрепленных к тегам HTML внутри строки, возвращенной из (1);
3 — getAllEvents: вызывает (2), затем устанавливает addEventListener в теги HTML.
Состояние
В ходе этой работы у меня появились крутые инсайты, которыми я решил поделиться с теми, кому нечем заняться. Вы можете проверить полный исходный код здесь (вся библиотека представляет собой 300-строчный файл component.js; component.js — это всего лишь одно приложение). Начнем с управления состоянием. По сути, это класс, хранящий необработанные данные и вызывающий новый рендеринг (ранее установленный для компонента) при каждом его изменении.
class State { constructor(render, initialState) { this.render = render; this.state = initialState; } set = (cb) => { if (cb instanceof Function) { console.log("Old state: ", this.state); this.state = cb(this.state); console.log("New state: ", this.state); } else { this.state = { ...this.state, ...cb }; } this.render(); } get = (key) => { return this.state[key]; } }
Метод state.set может принимать как объект, так и функцию-редуктор в качестве аргумента.
Базовая структура компонента
Код ниже реализует текущие рабочие ресурсы для компонентов.
class Parent extends Component { static HTMLTagWrapper = "tr"; constructor(key, props){ super(key, props); this.state.set({ count: 0 }); } handleButtonClick = (key = "") => { // key === "times" this.state.set(state => ({ ...state, count: state.count+1 })); } renderHTML = () => { const contents = ` <td> ${this.props.name} </td> <td> ${this.props.factor} </td> <td> <button event-click="${this.id}/times">multiply by ${this.state.get("count")}</button> </td> <td> ${this.props.factor * this.state.get("count")} </td> `.replaceAll("\n", ""); return this.contents; } }
Как в Реакте:
- static HTMLTagWrapper: HTML-тег, отвечающий за перенос возврата this.renderHTML; его значение по умолчанию — div, и это статический атрибут, поэтому мы можем обратить на него внимание, не создавая экземпляр объекта только для этого.
- ключ: необходимо предоставить — в React только с разными экземплярами одного и того же компонента, являющимися родственными элементами в дереве DOM;
- конструктор: доступ к свойствам, определение начального состояния;
- методы класса: обработчики событий, обращающиеся к переменным состояния
Обработчики событий передаются сразу после каждого рендеринга, устанавливая атрибут с именем event-{{тип события}} и передавая строку в формате {{идентификатор компонента/ключ события}} как ценность. Если событие установлено в HTML компонента, мы ДОЛЖНЫ определить метод с именем handle{{Tag}}{{Event type}}, как и в случае с camel. правило. Обработчику будет присвоен ключ в качестве параметра, поэтому мы можем правильно работать со сценариями, в которых у нас есть более одного элемента, запускающего события с одинаковым тегом и типом.
Отрисовка содержимого
Во-первых, мне нужно прояснить мою тривиальную стратегию выполнения рендеринга: шаблонные строки. Вызов компонента с именем «Родитель» означает добавление самозакрывающегося тега с именем component.parent. Каждый компонент, дважды вызываемый одним и тем же родителем, должен иметь атрибут key, чтобы различать их. Все остальные атрибуты будут рассматриваться как свойства.
setHTML = () => { const rows = this.state.get("row"); const rowsHTML = rows.map((child, index) => { return ` <component.parent key=${index} name="${child.name}" factor=${child.factor} /> `; }).join("\n"); return; }
После первого шага обработки строки, на котором мы заменяем ‹component.* /› тегом-оболочкой (по умолчанию: div), получаем следующую строку ниже. Позже я планирую поддерживать функцию, подобную props.children. Атрибуту id присваивается значение UUID4 внутри конструктора компонентов.
` <div id=${this.id} key=${index} name="${child.name}" factor=${child.factor} ></div> `
Важно отметить, что самозакрывающиеся/незакрывающиеся теги могут быть выданы в зависимости от используемого вами парсера, поэтому я решил извлечь имя компонента и атрибуты с помощью regex и применить парсер после преобразования в нативные HTML-теги.
И т. д
Здесь в основном есть два типа компонентов: root и non-root. «Корневой тип» получает обратный вызов рендеринга из библиотеки.
class Root extends Lib.Component(Lib.render, true) {}
в то время как «не-корневой» тип (все остальные компоненты) получает обратный вызов рендеринга «корневого типа»
const Main = new Root("root"); // instantiate Root const Component = Lib.Component(Main.rootRender); // create alias for non-root class Parent extends Component {}
Все это имеет смысл, потому что я применил заводской шаблон в классе, который возвращает анонимный класс;
const InternalComponent = (render = null, isRoot = false) => class {}
Возможно, мои следующие шаги, связанные с этой библиотекой:
- повторно визуализировать только тот HTML, на который непосредственно влияет обновленное состояние
- относится к шагу выше: обрабатывать изменения в свойствах компонентов
- мощный сборщик мусора для отслеживания привязки элемента события — что-то вроде useEffect React, возвращающего обратный вызов, отвечающий за удаление прослушивателей событий
- "Машинопись"
- Библиотеки CSS — на данный момент в этом не было необходимости, так как мы можем использовать встроенный атрибут style
- разделение кода — хорошая возможность узнать больше о вебпаке
- тестирование — я написал шаблоны регулярных выражений с нуля, но я даже не помню, в каких случаях они выполняются или нет.