Всем привет! Меня зовут Петр Солопов, я руководитель отдела фронтенд-разработки в SuperJob. Думаю, многие из вас видели популярную серию картинок в интернете про фронтенд и бэкенд: на бекенде всегда какой-то монстр, а на фронте — все мило, летают бабочки. На мой взгляд, это не соответствует действительности и все не так радужно и безоблачно: чего только стоят настройка Webpack, тона зависимостей, особенности фреймворков и многое другое. За подробностями под кат.
Что не так с React?
React де-факто стал стандартом при разработке интерфейсов в вебе. В этом можно убедиться, посмотрев на график скачиваний популярных фреймворков с npm:
Есть много статей, и вот одна из них, в которой говорится об основных недостатках React:
Из коробки он не работает в браузере из-за JSX, т.к. это проприетарный синтаксис, невалидный для браузера. А писать, не используя JSX, на React практически невозможно.
Для «Hello World» команда React предлагает инструмент create-react-app, который загружает на вашу машину 2,5 миллиона строк кода зависимостей на JavaScript.
Теперь React не просто библиотека для рендринга. В нем есть такие фичи, как server components, и другие.
Что не так с современным фронтендом?
Так как React — самый популярный фреймворк, то все его проблемы в той или иной степени специфичны для фронтенда в целом:
Корпорации продвигают проприетарные вещи, а не развивают стандарты — «Hello World» на любом популярном фреймворке не работает в браузере;
Колоссальное количество зависимостей в приложениях, сложности настройки сборки.
Об этих проблемах также упоминается в статьях (например, I don't want to do front-end anymore), которые становятся довольно популярными в сообществе.
It was easy to get started, too — you just created the files and refreshed the page.
Как это можно исправить
React и другие популярные фреймворки были выпущены в районе 2013 года. Тогда даже не было ES2015. Давайте посмотрим, какие на текущий момент есть инструменты, которые смогут решить вышеописанные проблемы. Первый инструмент, о котором хочется сказать, это нативные модули для JavaScript — ES-modules.
ES-modules
ES-модули приносят в JavaScript официальную унифицированную модульную систему. Однако, чтобы прийти к этому, потребовалось около 10 лет работы по стандартизации. Сегодня почти все основные браузеры из коробки поддерживают ES-модули, как и Node.js, начиная с 12-й версии.
ES-модули работают нативно в браузере у 93% пользователей. Поэтому их можно и нужно использовать в продакшене. Если вам нужно поддерживать браузеры, которые этого не умеют, например IE11, для них можно делать специальную сборку.
HTM
Следующий инструмент, который может помочь нам, это HTM. Удобная библиотека, с помощью которой можно писать JSX-like-синтаксис с помощью tagged templates, который будет работать прямо в браузере. HTM может работать как с React, так и Preact.
Unpkg
В примерах кода будет использован ресурс unpkg.com. Это просто CDN для npm. Его можно использовать для быстрой проверки гипотез, не скачивая себе на компьютер никаких зависимостей.
Hello world
Давайте с помощью всего вышеперечисленного напишем простейшее приложение, которое работает прямо в браузере. В примерах будет использован Preact, но это будет работать и с React’ом тоже. Создаем компонент и рендрим его на DOM-ноде:
<!-- index.html -->
<body>
<div id="app"></div>
<script type="module" src="main.js"></script>
</body>
// main.js
import {
h,
render
} from "https://unpkg.com/preact@10.5.13/dist/preact.module.js";
import htm from "https://unpkg.com/htm@3.0.4/dist/htm.module.js";
const html = htm.bind(h);
const App = ({ name }) => {
return html`<div>Hello ${name}</div>`;
};
function renderApp() {
const element = document.getElementById("app");
render(html`<${App} name="world" />`, element);
}
renderApp();
Для запуска не понадобилось сборщиков и даже локальной установки зависимостей. Приложение похоже на обычное приложение на React, с которым многие знакомы. Но тут есть что улучшить — например, добавить статическую типизацию.
Статическая типизация
Большие веб-приложения уже не пишутся на голом JavaScript. Используя статическую типизацию, например TypeScript или Flow, можно избежать многих ошибок и сделать код более поддерживаемый.
В примерах будем использовать TypeScript — это своеобразная надстройка над JS, которая позволяет писать типы в коде. Чтобы TypeScript работал в браузере, можно использовать специальный синтаксис и писать все типы в JSDocs. Получаем работающую статическую проверку кода, при этом код все еще работает прямо в браузере:
// main.js
import {
html,
render,
} from "https://unpkg.com/htm@3.0.2/preact/standalone.module.js";
/** @type {import('preact').FunctionalComponent<{name: string}>} */
const App = ({ name }) => {
return html`<div>Hello ${name}</div>`;
};
function renderApp() {
const element = document.getElementById("app");
if (!element) throw new Error("element is not found");
render(html`<${App} name="world" />`, element);
}
renderApp();
Управление импортами
С выходом Chrome 89 (и в Deno 1.8) мы получили нативное использование Import maps — механизма, который позволяет получить контроль над поведением JS-импортов.
Чем Import maps может быть полезен? К примеру, мы хотим использовать библиотеку «preact-router», которая внутри зависит от библиотеки «preact». Когда JS-интерпретатор дойдет до импорта «preact» в «preact-router», он упадет с ошибкой, так как не знает, что такое «preact» и откуда его брать. С помощью Import maps мы можем указать в html-файле, откуда брать зависимость, и JS-интерпретатор в рантайме успешно отработает.
<!-- index.html -->
<script type="importmap">
{
"imports": {
"preact": "https://unpkg.com/preact@10.5.13/dist/preact.module.js",
"htm": "https://unpkg.com/htm@3.0.4/dist/htm.module.js",
"htm/preact": "https://unpkg.com/htm@3.0.4/preact/index.module.js",
"preact-router": "https://unpkg.com/preact-router@3.2.1/dist/preact-router.es.js"
}
}
</script>
// main.js
import { html, render } from "htm/preact";
import { Router, Route } from "preact-router";
/** @type {import('preact').FunctionComponent<{ name: string }>} */
const Page = ({ name }) => {
return html`<div>${name} page</div>`;
};
function App() {
return html`
<${Router}>
<${Route} default component=${() => html`<${Page} name="Home" />`} />
<${Route} path="/about" component=${() => html`<${Page} name="About" />`} />
</Router>
`;
}
function renderApp() {
const element = document.getElementById("app");
if (!element) throw new Error("element is not found");
render(html`<${App} />`, element);
}
Подводные камни
У описанного выше подхода ограничение: зависимости должны быть ES-модулями. Но ввиду широкой поддержки ES-модулей, скорей всего в будущем, не собирать библиотеки под ES-модули будет моветоном.
Есть еще проблема: проверка типов не работает в tagged templates. Проблема решаемая, и уже есть инструменты, которое это умеют в lit (аналог htm), например lit-analyzer. Также есть issue в гитхаб в репозитории TypeScript, связанное с этим.
Заключение
В итоге удалось построить приложение, которое работает прямо в браузере. Вы можете самостоятельно посмотреть, как это работает, используя transpilation-free-starter-kit. В шаблоне также предусмотрена сборка для продакшена с поддержкой старых браузеров, установка зависимостей из npm
и другое.
Это не серебряная пуля, а один из способов писать веб-приложения, используя последние стандарты. На мой взгляд, такой подход хорошо подойдет как для быстрой проверки гипотез, так и для приложений среднего размера, которые не требуют большого количества зависимостей.
Используйте стандарты, пишите код, который исполняется в браузере, и все будет супер!