Я встречал много людей, легко и профессионально работающих с современными библиотеками и фреймворками, такими как Vue, React, Angular и другими, которым очень интересно, что происходит под капотом этих фантастических помощников.

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

Вот почему я решил показать очень простым и наивным способом, чтобы проиллюстрировать это, создав такое поведение libs с нуля. Я не хочу, чтобы вы думали, что нам приходится работать без библиотек или что они бесполезны. На самом деле, когда вы закончите, вы поймете всю работу, которую они делают, и они вам понравятся еще больше!

Я решил использовать Typescript для этого, потому что это очень хороший инструмент для написания кода внешнего интерфейса Vanilla. С его системой типизации и возможностями ООП, если у вас есть IDE, которая хорошо с этим справляется, вы получите отличный опыт кодирования, многие вещи предоставляются автозаполнением, гораздо больше, чем в JS…

Итак, начнем :

Установка

Вам просто понадобится установленный node.js, npm или yarn для установки некоторых пакетов, хорошая IDE, которая хорошо обрабатывает Typescript, VSCode — мой выбор, так как он делает это действительно хорошо, но другие с соответствующими плагинами тоже должны подойти, и современный браузер со встроенным dev-инструментом для проверки кода и консолью.

Имея это, давайте сначала создадим пустой проект, инициировав его: создайте где-нибудь каталог, зайдите в него и создайте файл package.json:

$ mkdir behind-hood
$ cd behind-hood
$ yarn init -y

(В этой статье я использую пряжу, но вы можете легко преобразовать ее в команды npm.)

Затем установите компилятор Typescript и node-sass, так как мы будем использовать его для стилизации. Мы не устанавливаем их как dev-зависимости, так как они понадобятся бегуну, если вы когда-нибудь сделаете какую-либо непрерывную интеграцию с этим кодом.

$ yarn add typescript node-sass

Теперь нам нужен упаковщик, чтобы связать и скомпилировать все файлы и компоненты, которые нам нужно будет создать. Поскольку нам не нужно настраивать конфигурацию, мы не будем использовать веб-пакет, потому что он настолько мощный, что требует много настроек. Вместо этого мы будем использовать пакет-связку (https://parceljs.org/), который предоставляет стандартную конфигурацию, которая работает в большинстве случаев. Итак, давайте установим его:

$ yarn add parcel-bundler

Затем нам нужно добавить несколько скриптов в файл package.json для быстрого запуска сборок и сервера разработки. Добавьте ключ «скрипты» и введите две команды:

«serve» соберет приложение и запустит сервер разработки с горячей перезагрузкой, чтобы помочь вам в процессе написания кода, а «build» соберет производственный пакет, доступный в каталоге .dist. Для более профессионально построенного проекта мы также должны на этом этапе создать линтер и настроить его в соответствии с нашими потребностями, но я не хочу загромождать здесь цель. Однако, если вы используете VSCode, он будет жаловаться, если он не установлен, поэтому создайте файл yarn add --dev eslint .

Затем мы добавим наш основной файл записи, который будет index.html в корне проекта:

Мы только что сгенерировали базовый файл шаблона HTML5 (набрав «!», если Emmet установлен в вашей IDE) и добавили три вещи:

Строка <link rel="stylesheet" href="./assets/style.sass"> указывает, где найти таблицу стилей. Вы можете видеть, что вы можете напрямую указать файл SASS, посылка-бандлер сделает то, что нужно.

Несколько основных тегов HTML в разделе body: один h1, чтобы показать, что вещи за пределами тега нашего приложения останутся нетронутыми, и один тег div с идентификатором app, куда будет вставлен наш код.

Вызов файла, который будет обрабатывать наш код <script src="./src/app.ts"></script>. Как и выше, мы напрямую указываем файл входа TS нашего приложения, Parcel сделает все необходимое.

Осталось добавить два файла, на которые мы только что ссылались: создайте каталоги src и assets в корне проекта, и вы создадите файл style.sass в активах со следующим внутри. Мы используем форму SASS с отступом, так как она довольно лаконична:

Затем создайте файл app.ts в каталоге src с одной строкой на данный момент:

В своем терминале запустите команду yarn serve (или npm run serve, если вы не используете пряжу), откройте браузер и перейдите по URL-адресу, указанному в выводе посылки, обычно http://localhost:1234 . Вы должны увидеть результат того, что мы сделали:

Как мы видим, HTML-код за пределами элемента #app сохранился, а написанная нами строка заполнила div #app. Вы можете оценить, как быстро мы построили проект, благодаря парцелле js…

На этом этапе вы можете добавить возможность управления версиями кода. Это легко сделать git init. Просто подумайте, если вы это сделаете, чтобы добавить файл, который нужно скрыть, для управления версиями бесполезных вещей, таких как node_modules, например, каталоги .dist и .cache, созданные парцеллой. Затем git add .и git commit -m “big bang”должны сделать вашу первоначальную фиксацию.

Настроить машинопись

Давайте сначала настроим Typescript так, как нам нужно. Создайте файл tsconfig.jsonfile в корне проекта и заполните его следующим:

Это означает, что мы будем строить код javascript как ES2015 (иначе мы не сможем получить доступ к геттерам и сеттерам в классах), мы будем использовать формат разрешения модуля ES2015 (импортировать и не требовать), мы будем использовать DOM lib, чтобы иметь все предполагаемые типы DOM, ES2019 lib, чтобы иметь возможность использовать новейшие функции языка, мы говорим, что будем генерировать исходные карты для отладки в режиме разработки, и мы будем использовать строгий режим, чтобы быть уверенным, что компилятор не пропустит ни один тип несоответствие, включая проверку нулей. Все остальные параметры обрабатываются Parcel JS.

Базовый компонент

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

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

Итак, давайте создадим базовый класс, чтобы определить базовое поведение компонента. Создайте каталог components в корне и файл Component.ts в этой папке:

Мы объявляем класс абстрактным, потому что этой базы недостаточно для создания экземпляра компонента, но она дает нужные нам базы:

  • свойство элемента типа HTMLElement, из которого будут получены все реальные элементы, которые мы создадим позже. Он также помечен как абстрактный, потому что в каждом случае тип будет дополнительно расширен (HTMLDivElement, HTMLHEadingElement и т. д.).
  • Метод рендеринга, который прикрепит компонент к его родителю, мы вернемся к этому позже. Вы можете видеть, что мы заставили его работать с чистым HTML-элементом, а также с другим компонентом, производным от Component.

Теперь мы готовы создать наш первый настоящий компонент. Будем делать кнопку. Давайте создадим новый файл в том же каталоге с именем «Button.ts» и напечатаем это:

Обратите внимание, что даже если мы используем export default , мы все равно называем классы, потому что это поможет VSCode IntelliSense автоматически генерировать импорт в начале файлов. Когда мы говорим о VSCode, он сразу жалуется: Non-abstract class 'Button' does not implement inherited abstract member 'element’ from class ‘Component'. ts(2515)

Это нормально, так как мы объявили element abstract, поэтому здесь его нужно переопределить. Давайте сделаем его HTMLButtonElement, так как этот тип предоставляет нам все методы, необходимые для управления кнопкой, и давайте также создадим его в конструкторе:

Здесь мы переобъявили элемент как HTMLButtonElement (что нормально, поскольку он наследуется от HTMLElement). В конструкторе делаем обязательный вызов конструктора унаследованного класса, даже если он ничего не делает, то элемент создается и заполняется его текстом.

Сейчас проверим. В app.ts сразу после объявления appElement добавьте const Button = new Button('My Button'). В VSCode, когда вы набираете кнопку, если вы выберете хорошее предложение автозаполнения, IDE должна автоматически добавить строку импорта в заголовок файла, а при вводе параметра у вас должна быть подсказка о том, что вводить, и его тип. Вы также удалите строку с записью документа и объявите функцию renderApp следующим образом:

Что делает renderApp, так это сначала очищает элемент #app, а затем визуализирует в него все компоненты, которые мы туда поместим, используя метод render , который мы сделали выше. Это предпосылка того, что называется виртуальным DOM, потому что мы можем подготовить целое HTML-дерево перед его отображением и заменить все это в документе за одну операцию, делая обновление абсолютно плавным, мы вернемся к этому позже. Теперь вы можете увидеть результат в браузере:

Чтобы проверить замену DOM, вы можете перейти к файлу index.html и ввести текст внутри тега <div id="app"></div>. Затем, когда вы обновите браузер, вы не сможете увидеть этот текст, так как он заменяется на renderApp всего за несколько миллисекунд. Вы можете представить себе очень сложную визуализацию всей страницы и мгновенно переключаться на нее каждый раз, когда что-то меняется. Это будет основой того, что мы увидим позже о реактивности.

Просто добавьте эти строки в style.sass, чтобы почувствовать себя немного менее 90-ми, и проверьте результат…

Добавление поведения к компоненту: обработчики событий

Хорошо, теперь мы довольны нашей кнопкой, но она ничего не делает… Мы должны указать, что делать, например, при нажатии на нее.

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

В файле Button.ts мы добавляем общедоступный член, который будет функцией, не принимающей параметров (потому что щелчок — это щелчок…) и ничего не возвращающей. Благодаря TS мы не сможем здесь назначить что-то еще, кроме функции с этой сигнатурой.

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

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

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

Теперь вернемся к app.ts и там мы определим, что делать, присвоив экземпляру кнопки значение onClick того, что нужно. Вот, например, мы открываем окно с предупреждением:

Вот очень важная вещь: функция обработчика ДОЛЖНА быть определена как функция стрелки, потому что позже нам нужно будет передать this, который должен быть лексическим. Если вы не можете использовать стрелочную функцию по какой-либо причине, вам придется, если вам нужно this внутри функции, обернуть ее в обработчик, который содержит другую функцию с apply для явного определения this.

Проверьте в браузере оповещение, появляющееся при нажатии кнопки.

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

Начав с одного и того же компонента Button, теперь у нас есть два, которые делают разные вещи без необходимости настраивать внутри Button.ts.

Компонент ввода текста

Разобравшись с основами компонентов, мы теперь создадим компонент ввода текста. Создайте файл src/components/TextInput.ts. Он будет немного больше:

Давайте посмотрим, что отличается от компонента Button:

Сначала в Button мы обработали одно событие. Здесь мы будем обрабатывать два события: input, которое происходит каждый раз, когда значение поля изменяется, т. е. при каждом введенном символе, и change, которое происходит реже, так как генерируется только тогда, когда пользователь «подтверждает» ввод, нажимая ввод или когда поле теряет фокус.

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

Измените app.ts следующим образом, чтобы проверить, что происходит:

Затем, при открытой консоли браузера, введите abc: при каждой нажатой клавише вы увидите событие ввода с содержимым поля. Затем нажмите клавишу Enter или Tab или щелкните в другом месте, чтобы поле потеряло фокус, после чего вы увидите событие изменения. Мы закончили наш второй компонент.

Давайте добавим немного стиля к style.sass, чтобы сделать его более сексуальным:

Сборка атомов для создания молекул

Допустим, теперь у нас есть два атома. Почему бы не собрать их в некую молекулу, элемент пользовательского интерфейса, объединяющий текстовое поле и кнопку? Для этого мы создадим новый компонент, который собирает два других. Создайте файл TextFieldButton.ts в каталоге компонентов со следующим содержимым:

В этом компоненте мы, как обычно, создаем экземпляр элемента, но он будет обертывать другие компоненты, которые мы прикрепим внутрь: один TextInput и одна Button. Из TextFieldButton мы хотим, чтобы событие change запускалось ТОЛЬКО при нажатии кнопки, но мы хотим всплывать без изменения события input. Вот почему мы назначаем пустую функцию this.textField.onChange. Но мы хотим, чтобы событие change вызывалось при нажатии кнопки: это просто, просто вызовите его со значением поля в this.button.onClick. Мы также передаем геттеры/сеттеры значений и заполнителей.

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

Как обычно, немного стиля для добавления к style.sass:

Затем мы можем вставить его внутрь app.ts, чтобы проверить результат:

При вводе в поле мы теперь можем видеть в консоли браузера, что события ввода происходят так, как ожидалось, и что событие изменения происходит только при нажатии кнопки «Отправить».

Компонент отчетности и реактивность

Теперь мы собираемся создать компонент, который будет реактивным образом отображать то, что мы напечатаем в нашем поле. Мы будем использовать один и тот же компонент, созданный дважды, чтобы реагировать на события ввода и события изменения. Давайте создадим файл Viewer.ts в каталоге компонентов:

Мы просто создаем новый div и поддерживаем две переменные: одну для текстовой метки и одну для самого содержимого. В сеттерах этих переменных мы вызываем метод update, который заполняет элемент-оболочку непосредственно HTML, с декодированными переменными. Затем нарисуем все как обычно в style.sass:

И, наконец, мы должны изменить app.ts, добавив туда два средства просмотра и адаптировав обработчики событий textFieldBtn в соответствии с нашими потребностями:

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

Яхоу! Мы закончили… Подумайте обо всех шагах, которые мы сделали, чтобы эта тривиальная вещь заработала, и представьте, сколько времени вам потребуется, чтобы сделать то же самое, например, с Vue.js? Вероятно, не более 2-3 минут… Я сказал вам, что теперь вам понравится ваша библиотека интерфейса!

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

Но выполнение такого рода упражнений может сильно помочь вам в понимании библиотеки и является одним из первых шагов к ее освоению. Когда вы захотите это сделать, следующим шагом будет клонирование репозитория GitHub вашей библиотеки, открытие его в вашей IDE и попытка понять, что происходит, например, в тот момент, когда ваш код создает new Vue({element: 'app'}).

Вы можете найти готовый код в этом репозитории GitHub: https://github.com/vertcitron/behind-hood
Извините, что не выполнил промежуточные шаги…

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

Спасибо за чтение…