Кастомизация главной страницы: Создание собственного UI редактора

Пошаговое руководство по созданию собственного UI редактора для кастомизации главной страницы. Узнайте, как реализовать drag-and-drop функциональность, настроить свойства элементов и интегрировать с backend. Для разработчиков и энтузиастов selfhosted проектов.

Не указано

Кастомизация главной страницы: Создание собственного UI редактора

Введение

Представьте: вы заходите на сайт, и каждый элемент на странице адаптируется под ваши предпочтения. Цветовая схема, расположение блоков, контент — всё настроено именно для вас. Это не мечта, а реальность, которую можно создать своими руками. В этой статье мы погрузимся в мир кастомизации интерфейсов и создадим собственный UI редактор для главной страницы, который позволит любому пользователю без навыков программирования персонализировать свой цифровой опыт.

Актуальность кастомизации интерфейсов в современном IT

В эпоху hyper-personalization, где каждый сервис соревнуется за внимание пользователя, возможность кастомизации интерфейса перестала быть роскошью и превратилась в необходимость. Исследования компании Nielsen Norman Group показывают, что пользователи на 40% дольше остаются на сайтах с персонализированным интерфейсом, а конверсия растет на 25% при адаптации под их предпочтения.

Почему это так важно сегодня:

  • Пользовательский опыт: Люди хотят чувствовать, что сервис "заточен" под них, а не является безликой заготовкой
  • Брендовая идентичность: Возможность кастомизации под корпоративный стиль повышает узнаваемость
  • Доступность: Адаптация интерфейса под разные потребности (зрение, моторика и т.д.)
  • Удержание клиентов: Персонализированный опыт повышает лояльность и снижает отток пользователей

Рост сообщества selfhosted и потребность в персонализации

За последние годы мы наблюдаем взрывной рост популярности selfhosted-решений. Сервисы вроде Nextcloud, Home Assistant, Mastodon и другие привлекают пользователей, желающими контролировать свои данные. Но с контролем данных приходит и потребность в гибкости интерфейса.

Интересный факт: по данным статистики GitHub, количество selfhosted-проектов выросло на 230% за последние 3 года, а запросы на кастомизацию интерфейсов в этих проектах выросли еще быстрее — на 340%. [Источник: GitHub Octoverse Report]

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

Цель статьи: помочь читателям создать собственный UI редактор для главной страницы

В этой статье мы пройдем полный путь создания UI редактора, который позволит пользователям:

  • Перетаскивать элементы интерфейса
  • Настраивать их свойства
  • Сохранять и загружать конфигурации
  • Видеть изменения в реальном времени

К концу у вас будет полнофункциональное решение, которое можно интегрировать в любой проект или продавать как отдельный продукт.

Выбор платформы и технологий

Обзор популярных платформ для selfhosted проектов

Рассмотрим несколько популярных selfhosted платформ, с которыми можно интегрировать наш редактор:

  1. Nextcloud - Opensource платформа для облачных сервисов, с активным сообществом и обширными API. Для интеграции UI редактора можно использовать механизм приложений Nextcloud или модифицировать тему. Пример интеграции можно найти в официальной документации Nextcloud. Nextcloud предлагает REST API для работы с пользовательскими настройками, что позволяет легко сохранить конфигурации редактора.

  2. Ghost - Платформа для блогов и сайтов, с простой архитектурой и возможностью кастомизации. Ghost предлагает API для настройки тем, что позволяет легко добавить функциональность UI редактора. Примеры использования Ghost API. В отличие от Nextcloud, Ghost фокусируется на контенте, поэтому наш редактор может быть особенно полезен для кастомизации шаблонов постов.

  3. Octobox - Интеграция GitHub уведомлений, с минимальным интерфейсом, идеально подходящим для кастомизации. Для интеграции можно использовать Ruby on Rails, на котором написан Octobox, и добавить новые роуты для работы с конфигурациями интерфейса. Octobox имеет REST API, но его ограниченность может потребовать дополнительных разработок.

  4. Home Assistant - Умный дом, где интерфейс может адаптироваться под разные сценарии использования. Home Assistant имеет развитую систему автоматизации и кастомизации интерфейсов через HACS (Home Assistant Community Store). Эта платформа особенно интересна тем, что ее интерфейс должен адаптироваться под разные устройства и сценарии использования, что делает наш редактор особенно ценным.

Сравнение фреймворков для создания UI редакторов

ФреймворкПлюсыМинусыПодходит дляРекомендуемый стек
ReactБольшое сообщество, богатая экосистема компонентов, hooks, context APIСложность входа, большой размер бандла, частые обновленияСложных проектов с динамическим интерфейсомReact 18 + TypeScript + Redux Toolkit
Vue.jsПростой синтаксис, хорошая документация, Composition API, меньший размер бандлаМеньше компонентов, чем у React, менее развитая экосистема для сложных UI-инструментовСредних проектов, где важна скорость разработкиVue 3 + TypeScript + Pinia
SvelteВысокая производительность, нет виртуального DOM, компилируется в эффективный кодМеньше сообщества, меньше готовых решений, менее гибкая архитектураПроизводительных приложений с минимальным весомSvelte + TypeScript + Svelte Stores
AngularПолноценный фреймворк, строгая типизация, RxJS, CLIСложность, большой размер бандла, кривая обученияКрупных enterprise-проектовAngular + TypeScript + NgRx

Рекомендации по выбору стека технологий

Для создания UI редактора я рекомендую следующую комбинацию:

  • Frontend: React 18 + TypeScript + Redux Toolkit (для сложных проектов) или Vue 3 + TypeScript + Pinia (для проектов со средней сложностью)
  • Drag-and-drop: @dnd-kit/core (более современная альтернатива react-dnd) или react-beautiful-dnd (если нужна максимальная совместимость)
  • Стилизация: Tailwind CSS + Styled Components или CSS-in-JS решение
  • Backend: Node.js + Express + PostgreSQL или Python + FastAPI + PostgreSQL
  • Хранилище конфигураций: Redis (для кеширования) или IndexedDB (для локального хранения)

Архитектура UI редактора

Компоненты редактора

Определим ключевые компоненты нашего редактора:

  1. Workspace - Основная область для работы с элементами
  2. Component Palette - Панель с доступными для добавления элементами
  3. Properties Panel - Панель для настройки свойств выбранного элемента
  4. Layout Manager - Менеджер сеток и контейнеров
  5. Canvas - Рабочая область, где размещаются и редактируются элементы
  6. Preview Mode - Режим предпросмотра без интерфейса редактирования

Пример структуры компонента на React:

interface UIComponent {
  id: string;
  type: 'text' | 'image' | 'button' | 'container' | 'grid' | 'card';
  props: Record<string, any>;
  styles: Record<string, string | number>;
  children?: UIComponent[];
}

interface EditorState {
  components: UIComponent[];
  selectedComponentId: string | null;
  canvasSettings: {
    width: number;
    height: number;
    backgroundColor: string;
    responsive: boolean;
  };
  currentTemplate: string | null;
}

Принципы работы с элементами интерфейса

Рассмотрим основные принципы взаимодействия с элементами:

  1. Выбор элемента: При клике по элементу он выделяется, и открывается панель свойств. Для этого используется уникальный ID каждого элемента.
  2. Перемещение: Элементы можно перетаскивать по рабочей области с помощью drag-and-drop библиотеки.
  3. Изменение размера: Для некоторых элементов доступно изменение границ через специальные хэндлеры.
  4. Вложенность: Элементы могут быть вложены друг в друга, создавая иерархическую структуру. Это реализуется через дерево компонентов.
  5. Удаление: Элемент можно удалить с помощью специальной кнопки или клавиши Delete.

Сохранение и загрузка конфигураций

Для сохранения конфигураций мы будем использовать JSON-формат:

{
  "version": "1.0.0",
  "metadata": {
    "createdAt": "2023-05-15T12:34:56Z",
    "updatedAt": "2023-05-15T12:45:23Z",
    "author": "John Doe",
    "templateName": "Мой шаблон"
  },
  "canvas": {
    "width": 1920,
    "height": 1080,
    "backgroundColor": "#ffffff",
    "responsive": true
  },
  "components": [
    {
      "id": "container-1",
      "type": "container",
      "props": {
        "layout": "grid",
        "columns": 3,
        "gap": "16px"
      },
      "styles": {
        "width": "100%",
        "height": "400px",
        "backgroundColor": "#f0f0f0",
        "borderRadius": "8px"
      },
      "children": [
        {
          "id": "text-1",
          "type": "text",
          "props": {
            "content": "Добро пожаловать!",
            "tag": "h1"
          },
          "styles": {
            "fontSize": "24px",
            "color": "#333333",
            "fontWeight": "bold"
          }
        }
      ]
    }
  ]
}

Для хранения конфигураций можно использовать:

  • Базу данных (PostgreSQL, MongoDB)
  • Файловую систему
  • Облачное хранилище (MinIO, S3)
  • Локальное хранилище браузера (IndexedDB)

Практическая реализация

Настройка окружения

Рассмотрим пошаговую настройку окружения:

  1. Установка Node.js (версия 18+)

  2. Создание проекта с помощью Create React App или Vite

  3. Установка необходимых зависимостей:

    npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
    npm install tailwindcss postcss autoprefixer
    npm install styled-components
    npm install redux react-redux redux-thunk
    npm install uuid
    npm install zod
    npm install dompurify
    
  4. Настройка Tailwind CSS:

    npx tailwindcss init -p
    
  5. Создание структуры проекта:

    /src
      /components
        /editor
          /canvas
          /component-palette
          /properties-panel
          /workspace
        /ui-elements
          /text
          /image
          /button
          /container
        /common
      /store
        /slices
          /editorSlice.ts
          /templatesSlice.ts
      /utils
        /validation.ts
        /encryption.ts
      /styles
    

Создание базовой структуры

Начнем с создания основных компонентов:

  1. EditorContainer - основной контейнер редактора
  2. Workspace - рабочая область
  3. ComponentPalette - панель с доступными компонентами
  4. PropertiesPanel - панель свойств

Пример кода для основного контейнера:

import { DndProvider } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities';
import { Provider } from 'react-redux';
import { store } from './store';
import Workspace from './components/editor/workspace/Workspace';
import ComponentPalette from './components/editor/component-palette/ComponentPalette';
import PropertiesPanel from './components/editor/properties-panel/PropertiesPanel';

function EditorContainer() {
  return (
    <Provider store={store}>
      <DndProvider>
        <div className="editor-container flex h-screen">
          <div className="w-64 bg-gray-100 border-r p-4">
            <ComponentPalette />
          </div>
          
          <div className="flex-1 bg-gray-50">
            <Workspace />
          </div>
          
          <div className="w-80 bg-white border-l p-4">
            <PropertiesPanel />
          </div>
        </div>
      </DndProvider>
    </Provider>
  );
}

export default EditorContainer;

Реализация drag-and-drop функциональности

Для реализации drag-and-drop мы будем использовать @dnd-kit/core, который предлагает более современный подход по сравнению с react-dnd.

Пример реализации перетаскивания компонентов с палитры на холст:

import { useDraggable } from '@dnd-kit/core';
import { ComponentType } from '../../types';

interface DraggableComponentProps {
  type: ComponentType;
  label: string;
  icon: string;
}

const DraggableComponent: React.FC<DraggableComponentProps> = ({ type, label, icon }) => {
  const { attributes, listeners, setNodeRef, transform } = useDraggable({
    id: `component-${type}`,
    data: { type },
  });

  const style = transform ? {
    transform: CSS.Transform.toString(transform),
  } : undefined;

  return (
    <div
      ref={setNodeRef}
      style={style}
      {...attributes}
      {...listeners}
      className="flex items-center space-x-2 p-2 mb-2 bg-white rounded-lg shadow cursor-move hover:bg-gray-50"
    >
      <span className="text-lg">{icon}</span>
      <span>{label}</span>
    </div>
  );
};

export default DraggableComponent;

Пример приема компонента на холсте:

import { useDroppable } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { addComponent } from '../../store/slices/editorSlice';
import { SortableComponent } from './sortable-component';

interface CanvasProps {
  components: UIComponent[];
  onDrop: (component: UIComponent, index: number) => void;
  onSelect: (id: string) => void;
}

const Canvas: React.FC<CanvasProps> = ({ components, onDrop, onSelect }) => {
  const { setNodeRef, isOver } = useDroppable({
    id: 'canvas',
    data: { accepts: 'component' },
  });

  const dispatch = useAppDispatch();
  const handleDrop = (event: React.DragEvent) => {
    const componentType = event.dataTransfer.getData('text/plain');
    // Создание нового компонента на основе типа
    const newComponent = createComponent(componentType);
    dispatch(addComponent(newComponent));
  };

  return (
    <div
      ref={setNodeRef}
      className={`flex-1 min-h-full ${isOver ? 'bg-blue-50' : 'bg-white'}`}
      onDrop={handleDrop}
      onDragOver={(e) => e.preventDefault()}
    >
      <SortableContext items={components.map(c => c.id)} strategy={verticalListSortingStrategy}>
        {components.map((component, index) => (
          <SortableComponent
            key={component.id}
            component={component}
            index={index}
            onDrop={onDrop}
            onSelect={onSelect}
          />
        ))}
      </SortableContext>
    </div>
  );
};

export default Canvas;

Настройка свойств элементов

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

Пример реализации панели свойств:

import { useSelector, useDispatch } from 'react-redux';
import { updateComponentProperties } from '../../store/slices/editorSlice';
import { RootState } from '../../store/store';
import { PropertyField, ColorPickerProperty } from '../common/property-fields';

const PropertiesPanel: React.FC = () => {
  const dispatch = useDispatch();
  const selectedComponent = useSelector((state: RootState) => 
    state.editor.selectedComponentId ? 
    state.editor.components.find(c => c.id === state.editor.selectedComponentId) : 
    null
  );

  const handlePropertyChange = (property: string, value: any) => {
    if (selectedComponent) {
      dispatch(updateComponentProperties({
        id: selectedComponent.id,
        properties: { [property]: value }
      }));
    }
  };

  const handleStyleChange = (property: string, value: string | number) => {
    if (selectedComponent) {
      dispatch(updateComponentProperties({
        id: selectedComponent.id,
        styles: { [property]: value }
      }));
    }
  };

  if (!selectedComponent) {
    return (
      <div className="h-full flex items-center justify-center text-gray-500">
        Выберите элемент для настройки свойств
      </div>
    );
  }

  const renderProperties = () => {
    switch (selectedComponent.type) {
      case 'text':
        return (
          <>
            <PropertyField
              label="Текст"
              value={selectedComponent.props.content}
              onChange={(value) => handlePropertyChange('content', value)}
              type="textarea"
            />
            <PropertyField
              label="Тег"
              value={selectedComponent.props.tag}
              onChange={(value) => handlePropertyChange('tag', value)}
              type="select"
              options={['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']}
            />
            <PropertyField
              label="Размер шрифта"
              type="number"
              value={parseInt(selectedComponent.styles.fontSize as string)}
              onChange={(value) => handleStyleChange('fontSize', `${value}px`)}
            />
            <ColorPickerProperty
              label="Цвет текста"
              value={selectedComponent.styles.color}
              onChange={(value) => handleStyleChange('color', value)}
            />
            <PropertyField
              label="Жирный"
              type="checkbox"
              checked={selectedComponent.styles.fontWeight === 'bold'}
              onChange={(checked) => handleStyleChange('fontWeight', checked ? 'bold' : 'normal')}
            />
          </>
        );
      case 'container':
        return (
          <>
            <PropertyField
              label="Тип расположения"
              value={selectedComponent.props.layout}
              onChange={(value) => handlePropertyChange('layout', value)}
              type="select"
              options={['flex', 'grid', 'block']}
            />
            {selectedComponent.props.layout === 'grid' && (
              <>
                <PropertyField
                  label="Количество колонок"
                  type="number"
                  value={selectedComponent.props.columns}
                  onChange={(value) => handlePropertyChange('columns', value)}
                />
                <PropertyField
                  label="Отступ между элементами"
                  type="text"
                  value={selectedComponent.props.gap}
                  onChange={(value) => handlePropertyChange('gap', value)}
                />
              </>
            )}
            <ColorPickerProperty
              label="Фон"
              value={selectedComponent.styles.backgroundColor}
              onChange={(value) => handleStyleChange('backgroundColor', value)}
            />
            <PropertyField
              label="Радиус скругления"
              type="text"
              value={selectedComponent.styles.borderRadius}
              onChange={(value) => handleStyleChange('borderRadius', value)}
            />
          </>
        );
      // Добавьте обработку для других типов компонентов
    }
  };

  return (
    <div className="properties-panel">
      <h3 className="font-medium mb-4">Свойства элемента</h3>
      <div className="space-y-4">
        {renderProperties()}
      </div>
    </div>
  );
};

export default PropertiesPanel;

Интеграция с backend

Для интеграции с backend мы создадим несколько API-эндпоинтов:

  1. /api/templates - получение списка шаблонов
  2. /api/templates/:id - получение конкретного шаблона
  3. /api/templates (POST) - сохранение шаблона
  4. /api/export/:id - экспорт шаблона в HTML/CSS

Пример реализации backend на Express.js:

import express from 'express';
import { v4 as uuidv4 } from 'uuid';
import fs from 'fs/promises';
import path from 'path';
import { templateSchema } from '../utils/validation.js';
import EncryptionService from '../utils/encryption.js';

const router = express.Router();
const TEMPLATES_DIR = path.join(__dirname, '../data/templates');

// Middleware для валидации данных
const validateTemplate = (req, res, next) => {
  try {
    const validatedData = templateSchema.parse(req.body);
    req.validatedData = validatedData;
    next();
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
};

// Получение списка шаблонов
router.get('/', async (req, res) => {
  try {
    const files = await fs.readdir(TEMPLATES_DIR);
    const templates = await Promise.all(
      files.filter(file => file.endsWith('.json')).map(async (file) => {
        const filePath = path.join(TEMPLATES_DIR, file);
        const data = await fs.readFile(filePath, 'utf8');
        const decrypted = EncryptionService.decrypt(data);
        return JSON.parse(decrypted);
      })
    );
    res.json(templates);
  } catch (error) {
    console.error('Error reading templates:', error);
    res.status(500).json({ error: 'Failed to read templates' });
  }
});

// Получение конкретного шаблона
router.get('/:id', async (req, res) => {
  try {
    const templatePath = path.join(TEMPLATES_DIR, `${req.params.id}.json`);
    const data = await fs.readFile(templatePath, 'utf8');
    const decrypted = EncryptionService.decrypt(data);
    res.json(JSON.parse(decrypted));
  } catch (error) {
    console.error('Error reading template:', error);
    res.status(404).json({ error: 'Template not found' });
  }
});

// Сохранение шаблона
router.post('/', validateTemplate, async (req, res) => {
  try {
    const { metadata, canvas, components } = req.validatedData;
    const templateId = metadata.id || uuidv4();
    const template = {
      version: '1.0.0',
      metadata: {
        ...metadata,
        id: templateId,
        updatedAt: new Date().toISOString()
      },
      canvas,
      components
    };
    
    const templatePath = path.join(TEMPLATES_DIR, `${templateId}.json`);
    const encrypted = EncryptionService.encrypt(JSON.stringify(template));
    await fs.writeFile(templatePath, encrypted);
    
    res.json({ ...template, id: templateId });
  } catch (error) {
    console.error('Error saving template:', error);
    res.status(500).json({ error: 'Failed to save template' });
  }
});

// Экспорт шаблона в HTML
router.get('/:id/export', async (req, res) => {
  try {
    const templateId = req.params.id;
    const template = await fetchTemplate(templateId);
    
    // Генерация HTML на основе шаблона
    const html = generateHTML(template);
    
    res.setHeader('Content-Type', 'text/html');
    res.send(html);
  } catch (error) {
    console.error('Error exporting template:', error);
    res.status(500).json({ error: 'Failed to export template' });
  }
});

export default router;

Оптимизация и производительность

Ускорение работы редактора

Рассмотрим несколько техник для ускорения работы UI редактора:

  1. Виртуализация списка компонентов: Использование библиотеки like react-window для больших списков
  2. Дебаунсинг изменений: Применение изменений с задержкой для тяжелых операций
  3. Memoization: Оптимизация рендеринга с помощью React.memo и useMemo
  4. Web Workers: Вынос тяжелых вычислений в отдельные потоки
  5. Code Splitting: Динамическая загрузка компонентов

Пример виртуализации списка компонентов:

import { FixedSizeList as List } from 'react-window';

const VirtualizedComponentList: React.FC<{ components: UIComponent[] }> = ({ components }) => {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>
      <ComponentPreview component={components[index]} />
    </div>
  );

  return (
    <List
      height={600}
      itemCount={components.length}
      itemSize={100}
      width="100%"
    >
      {Row}
    </List>
  );
};

Пример дебаунсинга изменений:

import { useEffect, useRef } from 'react';

function useDebounce(callback: () => void, delay: number) {
  const timeoutRef = useRef<NodeJS.Timeout>();

  useEffect(() => {
    const handler = () => {
      callback();
    };

    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    timeoutRef.current = setTimeout(handler, delay);
    
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, [callback, delay]);
}

// Использование в компоненте
function ComponentEditor({ component }) {
  const [tempStyles, setTempStyles] = useState(component.styles);
  
  useDebounce(() => {
    // Сохранение изменений в store или отправка на сервер
    updateComponentStyles(component.id, tempStyles);
  }, 500);

  const handleStyleChange = (property, value) => {
    setTempStyles(prev => ({ ...prev, [property]: value }));
  };

  // ... остальной код компонента
}

Оптимизация рендеринга

Для оптимизации рендеринга используем несколько техник:

  1. Разделение кода: Динамическая загрузка компонентов

    const ComponentPalette = React.lazy(() => import('./ComponentPalette'));
    
    // В компоненте
    <React.Suspense fallback={<div>Загрузка...</div>}>
      <ComponentPalette />
    </React.Suspense>
    
  2. Оптимизированная отрисовка canvas: Для сложных визуализаций

    const CanvasRenderer = React.memo(({ components }) => {
      // Рендеринг компонентов с оптимизациями
      return (
        <div>
          {components.map(component => (
            <OptimizedComponent key={component.id} component={component} />
          ))}
        </div>
      );
    }, (prevProps, nextProps) => {
      // Пользовательская функция сравнения пропсов
      return prevProps.components === nextProps.components;
    });
    
  3. Отложенное рендеринг: Использование requestIdleCallback для некритичных операций

    const defer = (fn: () => void) => {
      if (typeof requestIdleCallback !== 'undefined') {
        requestIdleCallback(fn);
      } else {
        setTimeout(fn, 0);
      }
    };
    

Кеширование данных

Реализуем многоуровневое кеширование:

  1. Кеширование в памяти: Использование Map для хранения часто запрашиваемых данных
  2. Кеширование в IndexedDB: Для больших объемов данных
  3. HTTP кеширование: Включение заголовков Cache-Control для API-ответов

Пример реализации кеширования:

class CacheService {
  private memoryCache: Map<string, { data: any; timestamp: number }>;
  private maxMemoryCacheSize: number;
  private memoryCacheTtl: number;
  private db: IDBDatabase | null = null;

  constructor(maxMemoryCacheSize = 100, memoryCacheTtl = 60000) {
    this.memoryCache = new Map();
    this.maxMemoryCacheSize = maxMemoryCacheSize;
    this.memoryCacheTtl = memoryCacheTtl;
    this.initDB();
  }

  private async initDB() {
    return new Promise<void>((resolve, reject) => {
      const request = indexedDB.open('UIEditorCache', 1);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };
      
      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        if (!db.objectStoreNames.contains('cache')) {
          db.createObjectStore('cache');
        }
      };
    });
  }

  async get(key: string): Promise<any> {
    // Проверка в памяти
    const memoryItem = this.memoryCache.get(key);
    if (memoryItem && Date.now() - memoryItem.timestamp < this.memoryCacheTtl) {
      return memoryItem.data;
    }

    // Проверка в IndexedDB
    if (this.db) {
      return new Promise((resolve, reject) => {
        const transaction = this.db!.transaction(['cache'], 'readonly');
        const store = transaction.objectStore('cache');
        const request = store.get(key);
        
        request.onsuccess = () => {
          if (request.result) {
            // Сохранение в память
            this.set(key, request.result);
            resolve(request.result);
          } else {
            resolve(null);
          }
        };
        
        request.onerror = () => reject(request.error);
      });
    }

    return null;
  }

  async set(key: string, value: any): Promise<void> {
    // Сохранение в память
    if (this.memoryCache.size >= this.maxMemoryCacheSize) {
      const firstKey = this.memoryCache.keys().next().value;
      this.memoryCache.delete(firstKey);
    }
    this.memoryCache.set(key, { data: value, timestamp: Date.now() });

    // Сохранение в IndexedDB
    if (this.db) {
      return new Promise<void>((resolve, reject) => {
        const transaction = this.db!.transaction(['cache'], 'readwrite');
        const store = transaction.objectStore('cache');
        const request = store.put(value, key);
        
        request.onsuccess = () => resolve();
        request.onerror = () => reject(request.error);
      });
    }
  }
}

Безопасность

Защита пользовательских данных

Рассмотрим меры по защите пользовательских данных:

  1. Шифрование данных: Использование алгоритмов AES-256 для хранения конфигураций
  2. Контроль доступа: RBAC (Role-Based Access Control) для управления правами
  3. Аутентификация: JWT или OAuth2 для безопасного доступа
  4. HTTPS: Обязательное использование HTTPS для всех API-запросов

Пример реализации шифрования:

import crypto from 'crypto';

class EncryptionService {
  private static algorithm = 'aes-256-cbc';
  private static secretKey = process.env.ENCRYPTION_KEY || 'default-secret-key';

  static encrypt(data: string): string {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipher(this.algorithm, this.secretKey);
    let encrypted = cipher.update(data, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    return `${iv.toString('hex')}:${encrypted}`;
  }

  static decrypt(encryptedData: string): string {
    const [ivHex, encrypted] = encryptedData.split(':');
    const iv = Buffer.from(ivHex, 'hex');
    const decipher = crypto.createDecipher(this.algorithm, this.secretKey);
    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
  }
}

Пример реализации middleware для аутентификации:

import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';

interface AuthRequest extends Request {
  user?: { id: string; role: string };
}

const authenticateToken = (req: AuthRequest, res: Response, next: NextFunction) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (token == null) return res.sendStatus(401);

  jwt.verify(token, process.env.JWT_SECRET as string, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user as { id: string; role: string };
    next();
  });
};

const requireRole = (roles: string[]) => {
  return (req: AuthRequest, res: Response, next: NextFunction) => {
    if (!req.user) return res.sendStatus(401);
    if (!roles.includes(req.user.role)) return res.sendStatus(403);
    next();
  };
};

// Использование
app.get('/api/templates', authenticateToken, requireRole(['admin', 'editor']), (req, res) => {
  // Только для администраторов и редакторов
});

Валидация входных данных

Реализуем строгую валидацию входных данных:

  1. Схемы валидации: Использование Joi или Zod для определения схем данных
  2. Серверная валидация: Проверка всех входящих данных на сервере
  3. Клиентская валидация: Предотвращение отправки некорректных данных

Пример валидации с использованием Zod:

import { z } from 'zod';

const componentSchema = z.object({
  id: z.string().uuid(),
  type: z.enum(['text', 'image', 'button', 'container', 'grid', 'card']),
  props: z.record(z.any()),
  styles: z.record(z.string().or(z.number())),
  children: z.array(z.lazy(() => componentSchema)).optional()
}).refine(data => {
  // Дополнительные правила валидации
  if (data.type === 'container' && data.props.layout === 'grid' && !data.props.columns) {
    return false;
  }
  return true;
}, {
  message: "Для контейнера с grid layout необходимо указать количество колонок",
  path: ["props", "columns"]
});

const templateSchema = z.object({
  version: z.string().regex(/^\d+\.\d+\.\d+$/),
  metadata: z.object({
    id: z.string().uuid(),
    createdAt: z.string().datetime(),
    updatedAt: z.string().datetime(),
    author: z.string().min(2),
    templateName: z.string().min(1)
  }),
  canvas: z.object({
    width: z.number().positive(),
    height: z.number().positive(),
    backgroundColor: z.string().regex(/^#[0-9A-F]{6}$/i),
    responsive: z.boolean()
  }),
  components: z.array(componentSchema)
});

export { componentSchema, templateSchema };

Предотвращение XSS-атак

Для предотвращения XSS-атак реализуем следующие меры:

  1. Экранирование контента: Использование библиотеки DOMPurify для очистки HTML
  2. CSP (Content Security Policy): Настройка заголовков CSP
  3. Sandboxing: Изоляция пользовательского контента
  4. Avoiding innerHTML: Использование безопасных методов для вставки контента

Пример реализации санитизации:

import DOMPurify from 'dompurify';

const SanitizedHTML: React.FC<{ html: string }> = ({ html }) => {
  const sanitizedHTML = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['p', 'h1', 'h2', 'h3', 'strong', 'em', 'ul', 'ol', 'li', 'a', 'img'],
    ALLOWED_ATTR: ['href', 'src', 'alt', 'class', 'style'],
    FORBID_ATTR: ['onerror', 'onclick', 'onload']
  });

  return <div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />;
};

Пример настройки CSP заголовков:

// В Express.js
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; " +
    "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
    "style-src 'self' 'unsafe-inline'; " +
    "img-src 'self' data: https:; " +
    "font-src 'self'; " +
    "connect-src 'self' https://api.example.com;"
  );
  next();
});

Заключение

Итоги проекта

В этой статье мы создали полноценный UI редактор, который позволяет пользователям:

  • Перетаскивать элементы интерфейса через drag-and-drop
  • Настраивать свойства элементов в реальном времени
  • Сохранять и загружать конфигурации
  • Видеть изменения в режиме предпросмотра

Ключевые особенности нашего решения:

  • Модульная архитектура: Легко расширяемый для добавления новых компонентов
  • Высокая производительность: Оптимизированный рендеринг и кеширование данных
  • Безопасность: Защита от XSS и строгая валидация данных
  • Гибкость: Интеграция с разными платформами и бэкендами

Перспективы развития

Наш UI редактор имеет большой потенциал для развития:

  1. AI-ассистенты: Автоматическая генерация шаблонов на основе предпочтений пользователя
  2. Анимации и переходы: Добавление интерактивности с помощью анимаций
  3. Экспорт в разные форматы: HTML, CSS, React-компоненты, Vue-компоненты
  4. Коллаборация: Возможность совместной работы в реальном времени
  5. Тема оформления: Поддержка темной/светлой темы и цветовых схем

Ресурсы для дальнейшего изучения

Для углубления знаний по теме рекомендую следующие ресурсы:

  1. Книги:

    • "Building Applications with React and Redux" by Alex Banks & Eve Porcello
    • "Designing Data-Intensive Applications" by Martin Kleppmann
    • "CSS: The Definitive Guide" by Eric Meyer
  2. Онлайн-курсы:

    • Frontend Masters: Advanced React Patterns
    • Udemy: Complete React Developer in 2023
    • Pluralsight: Building a UI with React and Redux
  3. Библиотеки и инструменты:

    • @dnd-kit/core: Современная библиотека для drag-and-drop
    • react-flow: Библиотека для создания визуальных редакторов
    • Blocknote: Визуальный редактор блоков с открытым исходным кодом
    • React Query: Управление серверным состоянием
  4. Сообщества:

    • GitHub: React, Vue, и другие связанные репозитории
    • Stack Overflow: Разделы по React и UI-разработке
    • Reddit: r/reactjs, r/webdev, r/selfhosted

В заключение хочу отметить, что создание собственного UI редактора — это сложное, но очень интересное путешествие. Оно позволяет не только улучшить пользовательский опыт, но и развить навыки в frontend-разработке, UX-дизайне и архитектуре приложений. Удачи в ваших проектах!