javascript

Почти Ванильный Фронтэнд

  • суббота, 5 апреля 2025 г. в 00:00:08
https://habr.com/ru/articles/897454/

Почти — потому что используется всего две функции из библиотеки:

  1. Создать элемент DOM

  2. Обновить элемент DOM

Эта библиотека упрощает использование нативных функций DOM, таких как createElement и replaceChild. Библиотека Fusor направлена на то, чтобы сделать эти функции проще и компактнее.

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

Оглавление

Установка

npm install @fusorjs/dom

Или:

Создание и Обновление DOM

import {getElement, update} from '@fusorjs/dom';
import {section, div} from '@fusorjs/dom/html';

let count = 0;

const block = section(
  {class: () => (count % 2 ? 'odd' : 'even')},

  div('Seconds ', () => count, ' elapsed'),
  div('Minutes ', () => Math.floor(count / 60), ' elapsed'),
);

document.body.append(getElement(block));

setInterval(() => {
  count++;
  update(block);
}, 1000);

> запустить пример

Только крохотные части у block дерева DOM обновляются, если они отличаются от текущих значений.

Поддержка JSX

import {getElement, update} from '@fusorjs/dom';

let count = 0;

const block = (
  <section class={() => (count % 2 ? 'odd' : 'even')}>
    <div>Seconds {() => count} elapsed</div>
    <div>Minutes {() => Math.floor(count / 60)} elapsed</div>
  </section>
);

document.body.append(getElement(block));

setInterval(() => {
  count++;
  update(block);
}, 1000);

> запустить пример

Синтаксис Установки Параметров

import {getElement, update} from '@fusorjs/dom';
import {section} from '@fusorjs/dom/html';

let count = 0;

const block = section(
  {
    id: 'set attribute or property automatically',
    title_a: 'set attribute',
    style_p: 'set property',

    focus_e: () => 'set bubbling event handler',
    blur_e_capture_once: () => 'set capturing event handler once',

    // update dynamic values in this DOM node:
    click_e_update: () => count++, // same as
    click_e: () => {count++; update(block);}, // same as
    click_e: (event, self) => {count++; update(self);},

    class: count % 2 ? 'odd' : 'even', // static
    class: () => (count % 2 ? 'odd' : 'even'), // dynamic
  },

  'Static child ', count, ' never changes.',
  'Dynamic child ', () => count, ' is wrapped in a function.',
);

document.body.append(getElement(block));

> запустить пример

Компонент с Состоянием

import {getElement} from '@fusorjs/dom';
import {button, div} from '@fusorjs/dom/html';

const ClickCounter = (count = 0) =>
  button({click_e_update: () => count++}, 'Clicked ', () => count, ' times');

const App = () => div(ClickCounter(), ClickCounter(22), ClickCounter(333));

document.body.append(getElement(App()));

> запустить пример

Версия JSX

import {getElement} from '@fusorjs/dom';

const ClickCounter = ({count = 0}) => (
  <button click_e_update={() => count++}>Clicked {() => count} times</button>
);

const App = () => (
  <div>
    <ClickCounter />
    <ClickCounter count={22} />
    <ClickCounter count={333} />
  </div>
);

document.body.append(getElement(<App />));

> запустить пример

Компоненты в обеих версиях совместимы.

Тот же Компонент Разными Способами

import {button} from '@fusorjs/dom/html';

const ClickCounter = (count = 0) => {
  const self = button(
    {click_e: () => {count++; update(self);}},
    'Clicked ', () => count, ' times',
  );

  return self;
};

const ClickCounter = (count = 0) =>
  button(
    {click_e: (event, self) => {count++; update(self);}},
    'Clicked ', () => count, ' times',
  );

const ClickCounter = (count = 0) =>
  button(
    {click_e_update: () => count++},
    'Clicked ', () => count, ' times',
  );

> запустить пример

Контролируемый Компонент Ввода

Или Controlled Input в терминах React.

import {getElement} from '@fusorjs/dom';
import {input, div} from '@fusorjs/dom/html';

const UppercaseInput = (value = '') =>
  input({
    value: () => value.toUpperCase(),
    input_e_update: (event) => (value = event.target.value),
  });

document.body.append(
  getElement(
    div(UppercaseInput(), UppercaseInput('two'), UppercaseInput('three')),
  ),
);

> запустить пример

Точное Обновление DOM

import {getElement, update} from '@fusorjs/dom';
import {section, div} from '@fusorjs/dom/html';

let count = 0;

const seconds = div('Seconds ', () => count, ' elapsed');

const block = section(
  seconds,
  div('Minutes ', () => Math.floor(count / 60), ' elapsed'),
);

document.body.append(getElement(block));

setInterval(() => {
  count++;
  update(seconds); // not minutes
}, 1000);

> запустить пример

Это обновит только секунды, а не минуты.

Остановка Рекурсии Обновления

import {getElement, update} from '@fusorjs/dom';
import {section, div} from '@fusorjs/dom/html';

let count = 0;

const seconds = div('Seconds ', () => count, ' elapsed');

const block = section(
  () => seconds, // wrapped in a function to escape
  div('Minutes ', () => Math.floor(count / 60), ' elapsed'),
);

document.body.append(getElement(block));

setInterval(() => {
  count++;
  update(block);
}, 1000);

> запустить пример

Это обновит только минуты, но не секунды.

Только компоненты (seconds, block) обновляются рекурсивно. () => seconds — это функция, а не компонент.

Каждая функция из @fusorjs/dom/html возвращает компонент, если он содержит динамические значения. То же самое относится и к определениям JSX.

Жизненный Цикл Компонента

  1. Создать компонент

  2. Подключить к DOM

  3. Обновить DOM

  4. Отключить от DOM

import {getElement, update} from '@fusorjs/dom';
import {div} from '@fusorjs/dom/html';

const IntervalCounter = (count = 0) => {
  console.log('1. Create component');

  return div(
    {
      mount: (self) => {
        console.log('2. Connect to DOM');

        const timerId = setInterval(() => {
          count++;
          update(self);
          console.log('3. Update DOM');
        }, 1000);

        const unmount = () => {
          clearInterval(timerId);
          console.log('4. Disconnect from DOM');
        };

        return unmount;
      },
    },

    'Since mounted ', () => count, ` seconds elapsed`,
  );
};

const instance = IntervalCounter(); // 1. Create component
const element = getElement(instance);

document.body.append(element); // 2. Connect to DOM
setTimeout(() => element.remove(), 15000); // 4. Disconnect from DOM

> запустить пример

> запустить SVG Analog Clock

Автоматическое/Реактивное Обновление

Автоматические/реактивные обновления в больших фреймворках — это не что иное, как реализация паттерна Observable. Это включает в себя State в React, Signals в Solid, Redux, MobX и многие другие. В Fusor вы можете использовать любую из этих библиотек.

Здесь мы обсуждаем универсальное решение:

Библиотека Роутинга

import {update} from '@fusorjs/dom';
import {Observable} from 'Any/Observable/Signal/Redux/Mobx...';

// Modern routing handling
const observable = new Observable();
const read = () => location.hash.substring(1); // omit "#"
let route = read();
window.addEventListener(
  'popstate',
  () => {
    const next = read();
    if (route === next) return;
    route = next;
    observable.notify();
  },
  false,
);

// Fusor integration
export const getRoute = () => route;
export const mountRoute = (self) => {
  const callback = () => update(self);
  observable.subscribe(callback);
  return () => observable.unsubscribe(callback);
};

Реактивный Компонент

Переключение компонентов, когда выбран текущий маршрут.

import {span, a} from '@fusorjs/dom/html';
import {getRoute, mountRoute} from './router';

export const RouteLink = (title, route) =>
  span({mount: mountRoute}, () =>
    getRoute() === route
      ? title // when selected
      : a({href: `#${route}`}, title),
  );

Динамическое Создание и Обновление DOM

import {getElement} from '@fusorjs/dom';
import {ul, li} from '@fusorjs/dom/html';
import {RouteLink} from './RouteLink';

const block = ul(
  [...Array(10)].map((v, i) =>
    li(RouteLink(`${i + 1}. Section`, `url-to-${i + 1}-section`)),
  ),
);

document.body.append(getElement(block));

> запустить пример

Кеширование и Мемоизация

Тяжелый компонент создается только один раз.

import {div, br} from '@fusorjs/dom/html';

let isVisible = true; // can change

const block = div(
  (
    (cache = HeavyComponent()) =>
    () =>
      isVisible && cache
  )(),

  br(),

  () => RecreatedEveryUpdate(),
);

> запустить пример

Обработка Исключений

import {section, p} from '@fusorjs/dom/html';

const Value = (value) => {
  if (value === undefined) throw new Error(`provide a value`);

  return p(value);
};

const block = section(
  p('Before'),

  (() => {
    try {
      return [
        Value(1),
        Value(), // will throw
        Value(3),
      ];
    } catch (error) {
      if (error instanceof Error) return p('Exception: ', error.message);

      return p('Exception: unknown');
    }
  })(),

  p('After'),
);

> запустить пример

Заключение

Теперь вы знаете все, что нужно для начала разработки современных фронтенд-приложений с Fusor.

Насколько мне известно, разработка с Fusor — это самый лаконичный, гибкий, легковесный и производительный способ создания фронтенд-приложений.