# План рефакторинга app.js на ES6 модули ## Текущее состояние - **Размер файла**: 1671 строка - **Проблемы**: - Монолитная структура затрудняет поддержку - Все функции в глобальной области видимости - Сложно тестировать отдельные компоненты - Повторное использование кода затруднено ## Целевая архитектура ``` static/ ├── index.html ├── styles.css ├── js/ │ ├── main.js # Точка входа, инициализация │ ├── config.js # Константы и конфигурация │ ├── state/ │ │ └── appState.js # Глобальное состояние приложения │ ├── services/ │ │ ├── api-client.js # Существующий API клиент (переместить) │ │ ├── auth.service.js # Аутентификация │ │ ├── settings.service.js # Управление настройками │ │ └── query.service.js # Запросы к RAG │ ├── ui/ │ │ ├── auth.ui.js # UI авторизации │ │ ├── settings.ui.js # Диалог настроек │ │ ├── query-builder.ui.js # Построитель запросов │ │ ├── answer-viewer.ui.js # Просмотр ответов │ │ ├── questions-list.ui.js # Список вопросов │ │ ├── annotations.ui.js # Аннотации │ │ └── loading.ui.js # Индикаторы загрузки │ ├── utils/ │ │ ├── dom.utils.js # DOM манипуляции │ │ ├── format.utils.js # Форматирование (время, текст) │ │ ├── file.utils.js # Работа с файлами │ │ └── validation.utils.js # Валидация данных │ └── data/ │ ├── storage.js # LocalStorage обертка │ └── defaults.js # Дефолтные настройки (из settings.js) └── settings.js # Удалить после рефакторинга ``` ## Этапы рефакторинга ### Этап 1: Подготовка (день 1) **Цель**: Настроить окружение для ES6 модулей #### 1.1. Обновить index.html ```html ``` #### 1.2. Создать структуру папок ```bash mkdir static/js mkdir static/js/state mkdir static/js/services mkdir static/js/ui mkdir static/js/utils mkdir static/js/data ``` #### 1.3. Настроить конфигурацию - Создать `js/config.js` с константами - Определить экспорты/импорты --- ### Этап 2: Утилиты и вспомогательные функции (день 1-2) **Цель**: Вынести независимые функции #### 2.1. `utils/format.utils.js` **Функции** (из app.js строки 150-188): - `generateUUID()` - `formatTime(seconds)` - `formatTimestamp(isoString)` - `isTableText(text)` - `parseTextTable(text)` - `escapeHtml(text)` **Экспорт**: ```javascript export { generateUUID, formatTime, formatTimestamp, isTableText, parseTextTable, escapeHtml } ``` #### 2.2. `utils/file.utils.js` **Функции** (строки 262-273, 984-1132): - `downloadJSON(data, filename)` - `loadFileAsJSON(file)` - новая функция для загрузки - `loadFileAsText(file)` - новая функция **Экспорт**: ```javascript export { downloadJSON, loadFileAsJSON, loadFileAsText } ``` #### 2.3. `utils/validation.utils.js` **Функции** (строки 823-860): - `validateJSON(jsonString)` - `validateLoginFormat(login)` **Экспорт**: ```javascript export { validateJSON, validateLoginFormat } ``` #### 2.4. `utils/dom.utils.js` **Функции**: - `showElement(id)` - `hideElement(id)` - `toggleElement(id)` - `setElementText(id, text)` - `showToast(message, type)` (строка 277-281) **Экспорт**: ```javascript export { showElement, hideElement, toggleElement, setElementText, showToast } ``` --- ### Этап 3: State Management (день 2) **Цель**: Централизовать управление состоянием #### 3.1. `state/appState.js` **Содержимое** (строки 10-42): ```javascript class AppState { constructor() { this.settings = { /* ... */ } this.currentEnvironment = 'ift' this.environments = { ift: { /* ... */ }, psi: { /* ... */ }, prod: { /* ... */ } } } // Геттеры getCurrentEnv() { /* ... */ } getCurrentEnvSettings() { /* ... */ } // Сеттеры setCurrentEnvironment(env) { /* ... */ } updateSettings(settings) { /* ... */ } // Persistence saveToLocalStorage() { /* ... */ } loadFromLocalStorage() { /* ... */ } } export default new AppState() ``` #### 3.2. `data/storage.js` **Функции**: - `saveEnvironmentData(env, data)` - `loadEnvironmentData(env)` - `clearEnvironmentData(env)` **Экспорт**: ```javascript export { saveEnvironmentData, loadEnvironmentData, clearEnvironmentData } ``` #### 3.3. `data/defaults.js` **Перенести из settings.js** (строки 1-70): ```javascript export const defaultSettings = { activeEnvironment: 'ift', environments: { /* ... */ } } ``` --- ### Этап 4: Services (день 3) **Цель**: Бизнес-логика и API взаимодействие #### 4.1. `services/api-client.js` **Действие**: Переместить существующий `api-client.js` в папку `services/` **Обновить**: ```javascript class BriefBenchAPI { // ... существующий код } export default new BriefBenchAPI() ``` #### 4.2. `services/auth.service.js` **Функции** (строки 60-140): - `checkAuth()` - `login(loginString)` - `logout()` - `isAuthenticated()` **Экспорт**: ```javascript import api from './api-client.js' export class AuthService { async checkAuth() { /* ... */ } async login(loginString) { /* ... */ } logout() { /* ... */ } isAuthenticated() { /* ... */ } } export default new AuthService() ``` #### 4.3. `services/settings.service.js` **Функции** (строки 290-357): - `loadSettingsFromServer()` - `saveSettingsToServer(settings)` - `extractEnvironmentSettings(envSettings)` - `resetToDefaults()` **Экспорт**: ```javascript import api from './api-client.js' import appState from '../state/appState.js' export class SettingsService { async loadFromServer() { /* ... */ } async saveToServer(settings) { /* ... */ } extractEnvSettings(envSettings) { /* ... */ } resetToDefaults() { /* ... */ } } export default new SettingsService() ``` #### 4.4. `services/query.service.js` **Функции** (строки 861-1063): - `buildRequestBody()` - `sendQuery(environment, apiMode, requestBody)` - `extractQuestions()` - `loadRequestFromFile()` - `loadResponseFromFile()` **Экспорт**: ```javascript import api from './api-client.js' import appState from '../state/appState.js' export class QueryService { buildRequestBody() { /* ... */ } async sendQuery(env, apiMode, body) { /* ... */ } extractQuestions() { /* ... */ } async loadRequestFromFile() { /* ... */ } async loadResponseFromFile() { /* ... */ } } export default new QueryService() ``` --- ### Этап 5: UI Components (день 4-5) **Цель**: Разделить UI логику по компонентам #### 5.1. `ui/auth.ui.js` **Функции** (строки 77-132): - `showLoginScreen()` - `hideLoginScreen()` - `setupLoginListeners()` - `handleLoginSubmit()` **Экспорт**: ```javascript import authService from '../services/auth.service.js' export class AuthUI { showLoginScreen() { /* ... */ } hideLoginScreen() { /* ... */ } setupListeners() { /* ... */ } async handleLoginSubmit() { /* ... */ } } export default new AuthUI() ``` #### 5.2. `ui/loading.ui.js` **Функции** (строки 1137-1145): - `showLoading(message)` - `hideLoading()` **Экспорт**: ```javascript export class LoadingUI { show(message) { /* ... */ } hide() { /* ... */ } } export default new LoadingUI() ``` #### 5.3. `ui/settings.ui.js` **Функции** (строки 362-813): - `openSettingsDialog()` - `closeSettingsDialog()` - `populateSettingsDialog()` - `readSettingsFromDialog()` - `toggleBackendSettings(show)` - `saveSettings()` - `resetSettings()` - `exportSettings()` - `importSettings()` **Экспорт**: ```javascript import settingsService from '../services/settings.service.js' import appState from '../state/appState.js' export class SettingsUI { open() { /* ... */ } close() { /* ... */ } populate() { /* ... */ } read() { /* ... */ } toggleBackendSettings(show) { /* ... */ } async save() { /* ... */ } async reset() { /* ... */ } export() { /* ... */ } async import() { /* ... */ } setupListeners() { /* ... */ } } export default new SettingsUI() ``` #### 5.4. `ui/query-builder.ui.js` **Функции** (строки 643-883): - `showQueryBuilder()` - `switchQueryMode(mode)` - `validateJSON()` - `setupQueryBuilderListeners()` **Экспорт**: ```javascript import queryService from '../services/query.service.js' export class QueryBuilderUI { show() { /* ... */ } switchMode(mode) { /* ... */ } validateJSON() { /* ... */ } setupListeners() { /* ... */ } async handleSendQuery() { /* ... */ } } export default new QueryBuilderUI() ``` #### 5.5. `ui/questions-list.ui.js` **Функции** (строки 1179-1273): - `renderQuestionsList()` - `selectAnswer(index)` - `updateQuestionsCount()` - `hasAnnotationsInDocs(docsSection)` - `pluralize(count, one, few, many)` **Экспорт**: ```javascript import appState from '../state/appState.js' export class QuestionsListUI { render() { /* ... */ } selectAnswer(index) { /* ... */ } updateCount() { /* ... */ } hasAnnotations(docsSection) { /* ... */ } setupListeners() { /* ... */ } } export default new QuestionsListUI() ``` #### 5.6. `ui/answer-viewer.ui.js` **Функции** (строки 1279-1443): - `renderAnswer(index)` - `renderAnswerBody(elementId, text)` - `renderDocuments(containerId, docs, ...)` - `toggleExpansion(id)` - `switchTab(tabButton, tabId)` **Экспорт**: ```javascript import appState from '../state/appState.js' import { formatTime, parseTextTable, escapeHtml } from '../utils/format.utils.js' export class AnswerViewerUI { render(index) { /* ... */ } renderBody(elementId, text) { /* ... */ } renderDocuments(containerId, docs, ...) { /* ... */ } toggleExpansion(id) { /* ... */ } switchTab(tabButton, tabId) { /* ... */ } setupListeners() { /* ... */ } } export default new AnswerViewerUI() ``` #### 5.7. `ui/annotations.ui.js` **Функции** (строки 1448-1615): - `initAnnotationForAnswer(index)` - `loadAnnotationsForAnswer(index)` - `loadSectionAnnotation(section, data)` - `loadDocumentAnnotations(section, subsection, docs)` - `setupAnnotationListeners()` - `updateCheckboxStyle(checkbox)` - `saveAnnotationsDraft()` **Экспорт**: ```javascript import appState from '../state/appState.js' import { saveEnvironmentData } from '../data/storage.js' export class AnnotationsUI { initForAnswer(index) { /* ... */ } loadForAnswer(index) { /* ... */ } loadSection(section, data) { /* ... */ } loadDocuments(section, subsection, docs) { /* ... */ } setupListeners() { /* ... */ } saveDraft() { /* ... */ } } export default new AnnotationsUI() ``` --- ### Этап 6: Main Entry Point (день 6) **Цель**: Создать точку входа и инициализацию #### 6.1. `js/main.js` ```javascript // Импорты import appState from './state/appState.js' import authService from './services/auth.service.js' import settingsService from './services/settings.service.js' import authUI from './ui/auth.ui.js' import settingsUI from './ui/settings.ui.js' import queryBuilderUI from './ui/query-builder.ui.js' import questionsListUI from './ui/questions-list.ui.js' import answerViewerUI from './ui/answer-viewer.ui.js' import annotationsUI from './ui/annotations.ui.js' // Инициализация приложения async function initApp() { // Load settings from server await settingsService.loadFromServer() appState.setCurrentEnvironment(appState.settings.activeEnvironment || 'ift') // Load saved data for each environment appState.loadFromLocalStorage() // Setup all UI listeners authUI.setupListeners() settingsUI.setupListeners() queryBuilderUI.setupListeners() questionsListUI.setupListeners() answerViewerUI.setupListeners() annotationsUI.setupListeners() // Setup environment tabs setupEnvironmentTabs() // Render initial state updateUI() } // Setup environment tabs function setupEnvironmentTabs() { const tabs = document.querySelectorAll('.env-tab') tabs.forEach(tab => { tab.addEventListener('click', () => { switchEnvironment(tab.dataset.env) }) }) } // Switch environment function switchEnvironment(env) { appState.setCurrentEnvironment(env) updateEnvironmentTabs() updateUI() } // Update environment tabs function updateEnvironmentTabs() { const tabs = document.querySelectorAll('.env-tab') tabs.forEach(tab => { if (tab.dataset.env === appState.currentEnvironment) { tab.classList.add('active') } else { tab.classList.remove('active') } }) } // Update UI based on current state function updateUI() { questionsListUI.render() const env = appState.getCurrentEnv() if (env.currentResponse && env.currentResponse.answers) { answerViewerUI.render(env.currentAnswerIndex || 0) } else { queryBuilderUI.show() } } // Entry point document.addEventListener('DOMContentLoaded', async () => { const isAuthenticated = await authService.checkAuth() if (isAuthenticated) { await initApp() } }) ``` #### 6.2. `js/config.js` ```javascript // API Configuration export const API_CONFIG = { baseURL: '/api/v1', timeout: 1800000 // 30 minutes } // UI Configuration export const UI_CONFIG = { defaultQueryMode: 'questions', defaultWithDocs: true, maxAnnotationLength: 5000 } // Storage Keys export const STORAGE_KEYS = { token: 'briefBenchToken', user: 'briefBenchUser', settings: 'briefBenchSettings', envData: (env) => `briefBenchData_${env}`, annotations: 'briefBenchAnnotationsDraft' } // Constants export const ENVIRONMENTS = ['ift', 'psi', 'prod'] export const API_MODES = ['bench', 'backend'] ``` --- ## Этап 7: Тестирование и оптимизация (день 7) ### 7.1. Ручное тестирование - ✅ Авторизация работает - ✅ Загрузка/сохранение настроек - ✅ Отправка запросов (bench/backend) - ✅ Отображение ответов - ✅ Аннотации сохраняются - ✅ Экспорт/импорт работает - ✅ Переключение окружений ### 7.2. Проверка производительности - Измерить время загрузки - Проверить размер бандла - Оптимизировать импорты ### 7.3. Очистка - Удалить старый `app.js` - Удалить `settings.js` - Обновить `.gitignore` если нужно --- ## Миграционная стратегия ### Вариант 1: Постепенная миграция (РЕКОМЕНДУЕТСЯ) **Подход**: Модули живут параллельно с монолитом 1. Создать папку `js/` с модулями 2. Оставить `app.js` работающим 3. Постепенно переносить функции 4. Тестировать после каждого этапа 5. Когда все готово - переключиться на `main.js` **Преимущества**: - Можно откатиться в любой момент - Меньше риска - Легче тестировать **index.html во время миграции**: ```html ``` ### Вариант 2: Быстрая миграция **Подход**: Переписать всё за раз **НЕ РЕКОМЕНДУЕТСЯ** из-за высокого риска багов --- ## Чек-лист миграции ### Подготовка - [ ] Создать ветку `feature/modularize-frontend` - [ ] Сделать backup текущего app.js - [ ] Создать структуру папок ### Этап 1: Утилиты - [ ] Создать `utils/format.utils.js` - [ ] Создать `utils/file.utils.js` - [ ] Создать `utils/validation.utils.js` - [ ] Создать `utils/dom.utils.js` - [ ] Тесты: проверить что функции работают ### Этап 2: State - [ ] Создать `state/appState.js` - [ ] Создать `data/storage.js` - [ ] Создать `data/defaults.js` - [ ] Тесты: проверить геттеры/сеттеры ### Этап 3: Services - [ ] Переместить `api-client.js` в `services/` - [ ] Создать `services/auth.service.js` - [ ] Создать `services/settings.service.js` - [ ] Создать `services/query.service.js` - [ ] Тесты: проверить API вызовы ### Этап 4: UI Components - [ ] Создать `ui/auth.ui.js` - [ ] Создать `ui/loading.ui.js` - [ ] Создать `ui/settings.ui.js` - [ ] Создать `ui/query-builder.ui.js` - [ ] Создать `ui/questions-list.ui.js` - [ ] Создать `ui/answer-viewer.ui.js` - [ ] Создать `ui/annotations.ui.js` - [ ] Тесты: проверить рендеринг ### Этап 5: Main - [ ] Создать `js/main.js` - [ ] Создать `js/config.js` - [ ] Обновить `index.html` - [ ] Тесты: полный цикл работы ### Этап 6: Очистка - [ ] Удалить старый `app.js` - [ ] Удалить `settings.js` - [ ] Обновить документацию - [ ] Code review --- ## Преимущества после рефакторинга ### Для разработки - ✅ **Модульность**: Каждый файл отвечает за свою область - ✅ **Тестируемость**: Легко покрыть модули unit-тестами - ✅ **Читаемость**: Проще найти нужную функцию - ✅ **Переиспользование**: Функции можно использовать в других проектах ### Для поддержки - ✅ **Изоляция**: Баг в одном модуле не затронет другие - ✅ **Масштабируемость**: Легко добавлять новые функции - ✅ **Документация**: Каждый модуль самодокументируемый - ✅ **Team work**: Разные разработчики могут работать над разными модулями ### Для производительности - ✅ **Tree shaking**: Неиспользуемый код не попадет в бандл - ✅ **Lazy loading**: Можно подгружать модули по требованию - ✅ **Кеширование**: Браузер может кешировать отдельные модули --- ## Риски и митигация ### Риск 1: Поломка существующего функционала **Митигация**: Постепенная миграция с тестированием после каждого этапа ### Риск 2: Увеличение времени загрузки (много файлов) **Митигация**: Использовать bundler (Vite/Webpack) для production ### Риск 3: Проблемы совместимости браузеров **Митигация**: ES6 модули поддерживаются всеми современными браузерами ### Риск 4: Сложность отладки **Митигация**: Source maps и понятная структура папок --- ## Следующие шаги 1. **Обсудить план** с командой 2. **Выбрать стратегию миграции** (постепенная/быстрая) 3. **Создать ветку** для рефакторинга 4. **Начать с Этапа 1** (утилиты) 5. **Тестировать** после каждого этапа 6. **Code review** перед мержем --- ## Временные оценки | Этап | Описание | Время | |------|----------|-------| | Этап 1 | Подготовка | 2 часа | | Этап 2 | Утилиты | 3 часа | | Этап 3 | State | 2 часа | | Этап 4 | Services | 4 часа | | Этап 5 | UI Components | 8 часов | | Этап 6 | Main Entry | 2 часа | | Этап 7 | Тестирование | 4 часа | | **ИТОГО** | | **~25 часов** (3-4 рабочих дня) | --- ## Пример использования после рефакторинга ```javascript // Было (app.js, строка 900): async function handleSendQuery() { const envSettings = getCurrentEnvSettings() const env = getCurrentEnv() const apiMode = envSettings.apiMode || 'bench' // ... 50 строк кода } // Стало (js/ui/query-builder.ui.js): import queryService from '../services/query.service.js' import appState from '../state/appState.js' import loadingUI from './loading.ui.js' export class QueryBuilderUI { async handleSendQuery() { const envSettings = appState.getCurrentEnvSettings() const apiMode = envSettings.apiMode || 'bench' loadingUI.show('Отправка запроса...') try { const result = await queryService.sendQuery( appState.currentEnvironment, apiMode, this.buildRequestBody() ) // Update state appState.getCurrentEnv().currentResponse = result.response // Re-render this.hide() answerViewerUI.render(0) } catch (error) { showToast(error.message, 'error') } finally { loadingUI.hide() } } } ``` **Преимущества**: - Понятно где искать функцию - Легко тестировать - Явные зависимости - Переиспользуемый код --- *Автор: Claude Sonnet 4.5* *Дата: 2025-12-25*