23 KiB
План рефакторинга 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
<!-- Заменить обычные скрипты на модули -->
<script type="module" src="js/main.js"></script>
1.2. Создать структуру папок
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)
Экспорт:
export {
generateUUID,
formatTime,
formatTimestamp,
isTableText,
parseTextTable,
escapeHtml
}
2.2. utils/file.utils.js
Функции (строки 262-273, 984-1132):
downloadJSON(data, filename)loadFileAsJSON(file)- новая функция для загрузкиloadFileAsText(file)- новая функция
Экспорт:
export {
downloadJSON,
loadFileAsJSON,
loadFileAsText
}
2.3. utils/validation.utils.js
Функции (строки 823-860):
validateJSON(jsonString)validateLoginFormat(login)
Экспорт:
export {
validateJSON,
validateLoginFormat
}
2.4. utils/dom.utils.js
Функции:
showElement(id)hideElement(id)toggleElement(id)setElementText(id, text)showToast(message, type)(строка 277-281)
Экспорт:
export {
showElement,
hideElement,
toggleElement,
setElementText,
showToast
}
Этап 3: State Management (день 2)
Цель: Централизовать управление состоянием
3.1. state/appState.js
Содержимое (строки 10-42):
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)
Экспорт:
export {
saveEnvironmentData,
loadEnvironmentData,
clearEnvironmentData
}
3.3. data/defaults.js
Перенести из settings.js (строки 1-70):
export const defaultSettings = {
activeEnvironment: 'ift',
environments: { /* ... */ }
}
Этап 4: Services (день 3)
Цель: Бизнес-логика и API взаимодействие
4.1. services/api-client.js
Действие: Переместить существующий api-client.js в папку services/
Обновить:
class BriefBenchAPI {
// ... существующий код
}
export default new BriefBenchAPI()
4.2. services/auth.service.js
Функции (строки 60-140):
checkAuth()login(loginString)logout()isAuthenticated()
Экспорт:
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()
Экспорт:
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()
Экспорт:
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()
Экспорт:
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()
Экспорт:
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()
Экспорт:
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()
Экспорт:
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)
Экспорт:
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)
Экспорт:
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()
Экспорт:
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
// Импорты
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
// 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: Постепенная миграция (РЕКОМЕНДУЕТСЯ)
Подход: Модули живут параллельно с монолитом
- Создать папку
js/с модулями - Оставить
app.jsработающим - Постепенно переносить функции
- Тестировать после каждого этапа
- Когда все готово - переключиться на
main.js
Преимущества:
- Можно откатиться в любой момент
- Меньше риска
- Легче тестировать
index.html во время миграции:
<!-- Старый код (работает) -->
<script src="settings.js"></script>
<script src="api-client.js"></script>
<script src="app.js"></script>
<!-- Новый код (тестируется) -->
<!-- <script type="module" src="js/main.js"></script> -->
Вариант 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 (утилиты)
- Тестировать после каждого этапа
- 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 рабочих дня) |
Пример использования после рефакторинга
// Было (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