Современный Frontend: проблемы и пути решения. Пишем React-like приложение со строгой типизацией без сборщиков +25




Всем привет! Меня зовут Петр Солопов, я руководитель отдела фронтенд-разработки в SuperJob. Думаю, многие из вас видели популярную серию картинок в интернете про фронтенд и бэкенд: на бекенде всегда какой-то монстр, а на фронте — все мило, летают бабочки. На мой взгляд, это не соответствует действительности и все не так радужно и безоблачно: чего только стоят настройка Webpack, тонна зависимостей, особенности фреймворков и многое другое. За подробностями под кат.

Что не так с React?

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

График скачиваний frontend фреймворков
График скачиваний frontend фреймворков

Есть много статей, и вот одна из них, в которой говорится об основных недостатках React:

  1. Из коробки он не работает в браузере из-за JSX, т.к. это проприетарный синтаксис, невалидный для браузера. А писать, не используя JSX, на React практически невозможно.

  2. Для «Hello, World» команда React предлагает инструмент create-react-app,  который загружает на вашу машину 2,5 миллиона строк кода зависимостей на JavaScript.

  3. Теперь React не просто библиотека для рендринга. В нем есть такие фичи, как server components, и другие.

Что не так с современным фронтендом?

Так как React — самый популярный фреймворк, то все его проблемы в той или иной степени специфичны для фронтенда в целом:

  1. Корпорации продвигают проприетарные вещи, а не развивают стандарты — «Hello, World» на любом популярном фреймворке не работает в браузере;

  2. Колоссальное количество зависимостей в приложениях, сложности настройки сборки.

Об этих проблемах также упоминается в статьях (например, 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 modules разными браузерами 
Поддержка ES modules разными браузерами 

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 и другое.

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

Используйте стандарты, пишите код, который исполняется в браузере, и все будет супер!




Комментарии (54):