3dec3ea720
Made-with: Cursor
731 lines
32 KiB
Markdown
731 lines
32 KiB
Markdown
# Учебник: Как устроен EduHelper
|
||
|
||
Это пошаговый учебник для начинающего программиста. Здесь подробно объяснено, как работает каждая часть проекта EduHelper — веб-приложения для обучения с ИИ-ассистентом.
|
||
|
||
---
|
||
|
||
## Оглавление
|
||
|
||
1. [Что такое веб-приложение и как оно устроено](#1-что-такое-веб-приложение)
|
||
2. [Архитектура: Frontend vs Backend](#2-архитектура-frontend-vs-backend)
|
||
3. [Backend: Express.js — наш сервер](#3-backend-expressjs)
|
||
4. [База данных: SQLite + Prisma](#4-база-данных-sqlite--prisma)
|
||
5. [Frontend: React + Vite](#5-frontend-react--vite)
|
||
6. [Компоненты UI: Shadcn подход](#6-компоненты-ui-shadcn)
|
||
7. [Маршрутизация (роутинг)](#7-маршрутизация)
|
||
8. [Как фронтенд общается с бэкендом](#8-как-фронтенд-общается-с-бэкендом)
|
||
9. [Интеграция с ИИ (DeepSeek)](#9-интеграция-с-ии-deepseek)
|
||
10. [Стриминг ответов (SSE)](#10-стриминг-ответов-sse)
|
||
|
||
---
|
||
|
||
## 1. Что такое веб-приложение
|
||
|
||
Веб-приложение — это программа, которая работает в браузере. В отличие от обычного сайта, она **интерактивна**: вы можете нажимать кнопки, заполнять формы, получать ответы — всё без перезагрузки страницы.
|
||
|
||
EduHelper — веб-приложение, которое позволяет:
|
||
- Задавать вопросы ИИ-ассистенту
|
||
- Генерировать учебники по любой теме
|
||
- Проходить тесты
|
||
- Получать ежедневные отчёты об обучении
|
||
|
||
### Как это работает в общих чертах
|
||
|
||
```
|
||
[Браузер (Chrome)] <--HTTP запросы--> [Сервер (Express)] <--SQL запросы--> [База данных (SQLite)]
|
||
|
|
||
v
|
||
[DeepSeek API] (ИИ в облаке)
|
||
```
|
||
|
||
1. Вы открываете сайт в браузере
|
||
2. Браузер показывает интерфейс (это **frontend**)
|
||
3. Когда нужны данные, браузер отправляет запрос на сервер (это **backend**)
|
||
4. Сервер достаёт данные из базы данных или обращается к ИИ
|
||
5. Сервер отправляет ответ обратно в браузер
|
||
6. Браузер обновляет интерфейс
|
||
|
||
---
|
||
|
||
## 2. Архитектура: Frontend vs Backend
|
||
|
||
В нашем проекте фронтенд и бэкенд **живут в разных папках** — это называется «разделённая архитектура»:
|
||
|
||
```
|
||
Edu_helper/
|
||
├── backend/ ← Серверная часть (обработка данных, работа с БД и ИИ)
|
||
├── frontend/ ← Клиентская часть (интерфейс, кнопки, формы)
|
||
└── TEXTBOOK.md ← Этот учебник
|
||
```
|
||
|
||
### Зачем разделять?
|
||
|
||
| Преимущество | Пояснение |
|
||
|---|---|
|
||
| **Понятность** | Серверный и клиентский код не перемешаны. Легче найти нужный файл |
|
||
| **Независимость** | Можно менять фронтенд, не трогая бэкенд, и наоборот |
|
||
| **Масштабирование** | В будущем можно запустить несколько бэкендов или поменять фронтенд на мобильное приложение |
|
||
| **Командная работа** | Один разработчик делает фронтенд, другой — бэкенд |
|
||
|
||
### Как они общаются?
|
||
|
||
Через **HTTP-запросы** (API). Фронтенд отправляет запрос на адрес вроде `http://localhost:3001/api/questions`, а бэкенд возвращает данные в формате JSON.
|
||
|
||
---
|
||
|
||
## 3. Backend: Express.js
|
||
|
||
### Что такое Express.js?
|
||
|
||
Express — это **фреймворк** (набор готовых инструментов) для создания серверов на Node.js. Он позволяет легко описать: «когда приходит запрос на такой-то адрес, делай вот это».
|
||
|
||
### Структура нашего бэкенда
|
||
|
||
```
|
||
backend/
|
||
├── src/
|
||
│ ├── index.ts ← Точка входа. Здесь создаётся и настраивается сервер
|
||
│ ├── routes/ ← Маршруты (какой запрос куда ведёт)
|
||
│ │ ├── settings.ts ← /api/settings — настройки приложения
|
||
│ │ ├── chat.ts ← /api/chat — чат с ИИ
|
||
│ │ ├── questions.ts ← /api/questions — ежедневные вопросы
|
||
│ │ ├── textbooks.ts ← /api/textbooks — генерация учебников
|
||
│ │ ├── tests.ts ← /api/tests — тестирование
|
||
│ │ └── reports.ts ← /api/reports — ежедневные отчёты
|
||
│ ├── lib/ ← Вспомогательные модули
|
||
│ │ ├── prisma.ts ← Подключение к базе данных
|
||
│ │ └── deepseek.ts ← Подключение к ИИ (DeepSeek)
|
||
│ └── middleware/ ← Промежуточные обработчики
|
||
│ └── errorHandler.ts ← Обработка ошибок
|
||
├── prisma/
|
||
│ └── schema.prisma ← Описание структуры базы данных
|
||
├── data/
|
||
│ └── edu_helper.db ← Файл базы данных SQLite
|
||
├── .env ← Переменные окружения (секретные настройки)
|
||
├── tsconfig.json ← Настройки TypeScript
|
||
└── package.json ← Зависимости проекта и команды запуска
|
||
```
|
||
|
||
### Как работает маршрут (Route)?
|
||
|
||
Маршрут — это правило: «если пришёл запрос такого-то типа на такой-то адрес, выполни такой-то код».
|
||
|
||
Пример из `routes/settings.ts`:
|
||
```typescript
|
||
router.get("/", async (req, res) => {
|
||
// GET /api/settings — вернуть все настройки
|
||
const settings = await prisma.setting.findMany();
|
||
res.json(settings); // отправить как JSON
|
||
});
|
||
|
||
router.put("/", async (req, res) => {
|
||
// PUT /api/settings — обновить настройки
|
||
const entries = req.body; // данные из запроса
|
||
// ... сохранить в БД ...
|
||
res.json({ success: true });
|
||
});
|
||
```
|
||
|
||
### Типы HTTP-запросов
|
||
|
||
| Метод | Для чего | Пример |
|
||
|-------|---------|--------|
|
||
| `GET` | Получить данные | Загрузить список вопросов |
|
||
| `POST` | Создать что-то новое | Сохранить новый вопрос |
|
||
| `PUT` | Обновить существующее | Изменить настройки |
|
||
| `DELETE` | Удалить | Очистить историю чата |
|
||
|
||
---
|
||
|
||
## 4. База данных: SQLite + Prisma
|
||
|
||
### Что такое база данных?
|
||
|
||
База данных — это **структурированное хранилище информации**. Представьте таблицу в Excel:
|
||
|
||
| id | text | answer | date |
|
||
|----|------|--------|------|
|
||
| 1 | Что такое React? | React — это... | 2026-03-27 |
|
||
| 2 | Как работает async? | Async позволяет... | 2026-03-27 |
|
||
|
||
Каждая **строка** — это одна запись (вопрос). Каждый **столбец** — это поле (свойство).
|
||
|
||
### SQLite
|
||
|
||
SQLite — это база данных, которая хранится в **одном файле** (у нас `data/edu_helper.db`). Не нужно ставить отдельную программу — всё работает «из коробки».
|
||
|
||
### Prisma
|
||
|
||
Prisma — это **ORM** (Object-Relational Mapping). Она позволяет работать с базой данных **на TypeScript** вместо SQL.
|
||
|
||
Без Prisma (чистый SQL):
|
||
```sql
|
||
SELECT * FROM Question WHERE date = '2026-03-27';
|
||
```
|
||
|
||
С Prisma (TypeScript):
|
||
```typescript
|
||
const questions = await prisma.question.findMany({
|
||
where: { date: '2026-03-27' }
|
||
});
|
||
```
|
||
|
||
### Схема базы данных (`schema.prisma`)
|
||
|
||
Схема описывает, какие таблицы и поля есть в нашей БД:
|
||
|
||
```prisma
|
||
model Question {
|
||
id Int @id @default(autoincrement()) // уникальный номер, растёт автоматически
|
||
text String // текст вопроса
|
||
answer String? // ответ (? = может быть пустым)
|
||
date String // дата в формате YYYY-MM-DD
|
||
createdAt DateTime @default(now()) // дата создания (автоматически)
|
||
}
|
||
```
|
||
|
||
Наши таблицы:
|
||
- **Setting** — настройки (API-ключ, промпты)
|
||
- **ChatMessage** — история чата
|
||
- **Question** — ежедневные вопросы и ответы
|
||
- **Textbook** — сгенерированные учебники
|
||
- **Test** — тесты (вопросы в формате JSON)
|
||
- **TestResult** — результаты прохождения тестов
|
||
- **Report** — ежедневные отчёты
|
||
|
||
---
|
||
|
||
## 5. Frontend: React + Vite
|
||
|
||
### Что такое React?
|
||
|
||
React — это библиотека для создания интерфейсов. Главная идея: интерфейс состоит из **компонентов** — переиспользуемых «кирпичиков».
|
||
|
||
Например, кнопка — это компонент. Карточка — компонент. Вся страница — тоже компонент, собранный из других.
|
||
|
||
### Что такое Vite?
|
||
|
||
Vite — это **сборщик** (build tool). Он берёт все ваши файлы TypeScript, React-компоненты, CSS — и собирает их в один пакет, который понимает браузер. Плюс он поддерживает **горячую перезагрузку** — вы сохраняете файл, и браузер обновляется мгновенно.
|
||
|
||
### Структура нашего фронтенда
|
||
|
||
```
|
||
frontend/src/
|
||
├── main.tsx ← Точка входа. Монтирует React-приложение в HTML
|
||
├── App.tsx ← Корневой компонент с маршрутизацией
|
||
├── index.css ← Глобальные стили и тема
|
||
├── pages/ ← Страницы приложения (по одной на раздел)
|
||
│ ├── HomePage.tsx ← Главная: приветствие + чат
|
||
│ ├── QuestionsPage.tsx ← Ежедневные вопросы
|
||
│ ├── TextbookPage.tsx ← Генерация учебника
|
||
│ ├── TestPage.tsx ← Тестирование
|
||
│ ├── ReportPage.tsx ← Ежедневный отчёт
|
||
│ └── SettingsPage.tsx ← Настройки (ключ, промпты)
|
||
├── components/ ← Переиспользуемые компоненты
|
||
│ ├── Layout.tsx ← Общий каркас: боковое меню + область контента
|
||
│ └── ui/ ← Базовые UI-компоненты (кнопки, инпуты, карточки)
|
||
│ ├── button.tsx
|
||
│ ├── input.tsx
|
||
│ ├── textarea.tsx
|
||
│ ├── card.tsx
|
||
│ └── label.tsx
|
||
└── lib/ ← Утилиты
|
||
└── utils.ts ← Вспомогательные функции (запросы к API, форматирование)
|
||
```
|
||
|
||
### Как работает React-компонент?
|
||
|
||
Компонент — это функция, которая возвращает **JSX** (разметку, похожую на HTML):
|
||
|
||
```tsx
|
||
function Greeting() {
|
||
const name = "Константин";
|
||
return <h1>Привет, {name}!</h1>;
|
||
}
|
||
```
|
||
|
||
В фигурных скобках `{}` можно писать JavaScript-выражения.
|
||
|
||
### Состояние (State)
|
||
|
||
Состояние — это данные, которые могут **меняться**. Когда состояние меняется, React перерисовывает компонент.
|
||
|
||
```tsx
|
||
const [count, setCount] = useState(0);
|
||
// count — текущее значение (изначально 0)
|
||
// setCount — функция для изменения
|
||
|
||
<button onClick={() => setCount(count + 1)}>
|
||
Нажали {count} раз
|
||
</button>
|
||
```
|
||
|
||
### Эффекты (useEffect)
|
||
|
||
`useEffect` выполняет код **после** рендера компонента. Используется для загрузки данных, подписок и т.д.
|
||
|
||
```tsx
|
||
useEffect(() => {
|
||
// Этот код выполнится один раз при первом рендере
|
||
fetch("/api/questions").then(res => res.json()).then(setQuestions);
|
||
}, []); // пустой массив = выполнить только один раз
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Компоненты UI: Shadcn подход
|
||
|
||
### Что такое Shadcn UI?
|
||
|
||
Shadcn UI — это **не библиотека**, а **коллекция компонентов**, которые вы копируете прямо в свой проект. Это значит:
|
||
- Вы полностью контролируете код каждого компонента
|
||
- Можно настроить внешний вид под свои нужды
|
||
- Нет «чёрного ящика» — всё прозрачно
|
||
|
||
### Как устроен компонент Button?
|
||
|
||
```tsx
|
||
const buttonVariants = cva(
|
||
"inline-flex items-center justify-center rounded-md text-sm font-medium",
|
||
{
|
||
variants: {
|
||
variant: {
|
||
default: "bg-primary text-primary-foreground", // синяя кнопка
|
||
outline: "border border-input bg-background", // кнопка с рамкой
|
||
ghost: "hover:bg-accent", // прозрачная кнопка
|
||
},
|
||
size: {
|
||
default: "h-10 px-4 py-2",
|
||
sm: "h-9 px-3",
|
||
lg: "h-11 px-8",
|
||
},
|
||
},
|
||
}
|
||
);
|
||
```
|
||
|
||
`cva` (class-variance-authority) — утилита для создания **вариантов стилей**. Вместо кучи `if/else` для классов CSS, мы декларативно описываем все варианты.
|
||
|
||
### Tailwind CSS
|
||
|
||
Tailwind — это CSS-фреймворк, где стили задаются через **классы** прямо в HTML:
|
||
|
||
```html
|
||
<!-- Обычный CSS -->
|
||
<div style="display: flex; padding: 16px; background: white; border-radius: 8px;">
|
||
|
||
<!-- Tailwind CSS -->
|
||
<div class="flex p-4 bg-white rounded-lg">
|
||
```
|
||
|
||
Каждый класс = одно CSS-свойство. Это быстрее писать и легче менять.
|
||
|
||
### Двухцветная тема
|
||
|
||
По требованию заказчика, дизайн в двух цветах (белый + акцентный). Наши цвета определены в `index.css`:
|
||
|
||
```css
|
||
@theme {
|
||
--color-background: #ffffff; /* белый фон */
|
||
--color-primary: #2563eb; /* синий — акцентный цвет */
|
||
--color-foreground: #0f172a; /* тёмный текст */
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Маршрутизация
|
||
|
||
### Что такое маршрутизация?
|
||
|
||
Когда вы нажимаете на ссылку «Вопросы», URL меняется на `/questions` и показывается нужная страница. Но **страница не перезагружается** — React Router просто меняет компонент.
|
||
|
||
```tsx
|
||
<Routes>
|
||
<Route element={<Layout />}> {/* общий каркас */}
|
||
<Route path="/" element={<HomePage />} />
|
||
<Route path="/questions" element={<QuestionsPage />} />
|
||
<Route path="/textbook" element={<TextbookPage />} />
|
||
<Route path="/test" element={<TestPage />} />
|
||
<Route path="/report" element={<ReportPage />} />
|
||
<Route path="/settings" element={<SettingsPage />} />
|
||
</Route>
|
||
</Routes>
|
||
```
|
||
|
||
`<Layout />` — это **обёртка**: боковое меню слева, а справа — `<Outlet />`, куда подставляется текущая страница.
|
||
|
||
---
|
||
|
||
## 8. Как фронтенд общается с бэкендом
|
||
|
||
### API-запросы
|
||
|
||
Фронтенд отправляет запросы к бэкенду с помощью функции `fetch`:
|
||
|
||
```typescript
|
||
// Утилита из lib/utils.ts
|
||
async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
|
||
const res = await fetch(`/api${path}`, {
|
||
headers: { "Content-Type": "application/json" },
|
||
...options,
|
||
});
|
||
return res.json();
|
||
}
|
||
|
||
// Использование
|
||
const questions = await apiFetch<Question[]>("/questions?date=2026-03-27");
|
||
```
|
||
|
||
### Прокси в Vite
|
||
|
||
Фронтенд работает на порту `5173`, бэкенд — на `3001`. Чтобы не было проблем с CORS, Vite проксирует запросы: всё, что начинается с `/api`, перенаправляется на бэкенд.
|
||
|
||
```typescript
|
||
// vite.config.ts
|
||
server: {
|
||
proxy: {
|
||
"/api": {
|
||
target: "http://localhost:3001",
|
||
changeOrigin: true,
|
||
},
|
||
},
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Интеграция с ИИ (DeepSeek)
|
||
|
||
### Что такое DeepSeek?
|
||
|
||
DeepSeek — это **языковая модель** (LLM), похожая на ChatGPT. Она принимает текст и генерирует ответ.
|
||
|
||
### Как мы подключаемся?
|
||
|
||
DeepSeek использует **OpenAI-совместимый API**. Это значит, мы можем использовать библиотеку `openai` от OpenAI, просто поменяв адрес сервера:
|
||
|
||
```typescript
|
||
import OpenAI from "openai";
|
||
|
||
const client = new OpenAI({
|
||
baseURL: "https://api.deepseek.com", // адрес DeepSeek вместо OpenAI
|
||
apiKey: "ваш-ключ",
|
||
});
|
||
```
|
||
|
||
### Промпты
|
||
|
||
**Промпт** (prompt) — это инструкция для ИИ. Мы используем разные промпты для разных задач:
|
||
|
||
- **prompt_answer** — для ответов на вопросы: «Ответь понятно, используй примеры»
|
||
- **prompt_textbook** — для учебника: «Объясняй просто, структурируй текст»
|
||
- **prompt_test** — для тестов: «Сгенерируй 10 вопросов с вариантами ответов»
|
||
- **prompt_report** — для отчёта: «Составь отчёт в таком-то формате»
|
||
|
||
Все промпты можно редактировать на странице «Настройки».
|
||
|
||
---
|
||
|
||
## 10. Стриминг ответов (SSE)
|
||
|
||
### Проблема
|
||
|
||
Когда ИИ генерирует длинный текст, ожидание может занять 10-30 секунд. Без стриминга пользователь видит пустой экран и думает, что приложение зависло.
|
||
|
||
### Решение: Server-Sent Events (SSE)
|
||
|
||
SSE — это технология, которая позволяет серверу **отправлять данные частями** по мере их генерации.
|
||
|
||
Как это работает:
|
||
|
||
1. Фронтенд отправляет запрос: «Ответь на вопрос»
|
||
2. Бэкенд начинает получать ответ от ИИ **по кусочкам**
|
||
3. Каждый кусочек сразу отправляется на фронтенд
|
||
4. Фронтенд показывает текст по мере поступления — как будто ИИ «печатает»
|
||
|
||
```
|
||
Сервер → Фронтенд:
|
||
data: {"content": "React"}
|
||
data: {"content": " — это"}
|
||
data: {"content": " библиотека"}
|
||
data: {"content": " для"}
|
||
data: {"content": " создания"}
|
||
data: {"content": " интерфейсов."}
|
||
data: [DONE]
|
||
```
|
||
|
||
На бэкенде:
|
||
```typescript
|
||
res.setHeader("Content-Type", "text/event-stream");
|
||
|
||
const stream = await client.chat.completions.create({
|
||
model: "deepseek-chat",
|
||
messages: [...],
|
||
stream: true, // ← включаем стриминг
|
||
});
|
||
|
||
for await (const chunk of stream) {
|
||
const content = chunk.choices[0]?.delta?.content || "";
|
||
res.write(`data: ${JSON.stringify({ content })}\n\n`);
|
||
}
|
||
```
|
||
|
||
На фронтенде:
|
||
```typescript
|
||
const reader = res.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
const text = decoder.decode(value);
|
||
// обработать полученные кусочки и обновить UI
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Словарь терминов
|
||
|
||
| Термин | Объяснение |
|
||
|--------|-----------|
|
||
| **API** | Application Programming Interface — способ общения между программами |
|
||
| **HTTP** | Протокол передачи данных в вебе (GET, POST, PUT, DELETE) |
|
||
| **JSON** | Формат данных: `{"name": "Константин", "age": 25}` |
|
||
| **REST** | Стиль проектирования API — каждый ресурс имеет свой URL |
|
||
| **ORM** | Object-Relational Mapping — работа с БД через объекты вместо SQL |
|
||
| **SSE** | Server-Sent Events — односторонняя отправка данных от сервера к клиенту |
|
||
| **LLM** | Large Language Model — большая языковая модель (ИИ) |
|
||
| **Промпт** | Текстовая инструкция для ИИ |
|
||
| **Компонент** | Переиспользуемый блок интерфейса в React |
|
||
| **Состояние** | Данные компонента, при изменении которых он перерисовывается |
|
||
| **Маршрут** | Связь между URL и страницей/обработчиком |
|
||
|
||
---
|
||
|
||
## 11. Дизайн: CSS-анимации и визуальные эффекты
|
||
|
||
### Почему дизайн важен?
|
||
|
||
Интерфейс — это первое, что видит пользователь. Если приложение выглядит современно и приятно, им хочется пользоваться. Claude, ChatGPT и другие ИИ-продукты ставят высокую планку дизайна.
|
||
|
||
### CSS-анимации
|
||
|
||
Анимации делают интерфейс «живым». В CSS анимация описывается через `@keyframes`:
|
||
|
||
```css
|
||
@keyframes fade-in {
|
||
from { opacity: 0; transform: translateY(8px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.animate-fade-in {
|
||
animation: fade-in 0.4s ease-out both;
|
||
}
|
||
```
|
||
|
||
Что происходит:
|
||
1. `from` — начальное состояние (элемент невидим и смещён вниз на 8px)
|
||
2. `to` — конечное состояние (полностью виден, на своём месте)
|
||
3. `0.4s` — длительность анимации
|
||
4. `ease-out` — плавное замедление в конце
|
||
5. `both` — применить конечное состояние после анимации
|
||
|
||
### Glow-эффект (свечение)
|
||
|
||
Свечение создаётся через `box-shadow` с цветом акцента:
|
||
|
||
```css
|
||
.glow-card:hover {
|
||
box-shadow:
|
||
0 0 0 1px rgba(79, 70, 229, 0.08), /* тонкая рамка */
|
||
0 4px 20px rgba(79, 70, 229, 0.08), /* мягкое свечение */
|
||
0 1px 3px rgba(0, 0, 0, 0.04); /* лёгкая тень */
|
||
transform: translateY(-1px); /* лёгкий подъём */
|
||
}
|
||
```
|
||
|
||
### Glassmorphism (стеклянный эффект)
|
||
|
||
Популярный тренд — полупрозрачный фон с размытием:
|
||
|
||
```css
|
||
.glass {
|
||
background: rgba(255, 255, 255, 0.85); /* полупрозрачный белый */
|
||
backdrop-filter: blur(12px); /* размытие фона за элементом */
|
||
}
|
||
```
|
||
|
||
### Пульсирующее свечение
|
||
|
||
Для привлечения внимания (например, иконка логотипа):
|
||
|
||
```css
|
||
@keyframes pulse-glow {
|
||
0%, 100% { box-shadow: 0 0 15px rgba(79, 70, 229, 0.15); }
|
||
50% { box-shadow: 0 0 30px rgba(79, 70, 229, 0.25); }
|
||
}
|
||
```
|
||
|
||
Свечение плавно усиливается и ослабевает — создаёт ощущение «дыхания».
|
||
|
||
### Принципы хорошей анимации
|
||
|
||
1. **Быстро** — не более 0.3-0.5 секунды (длиннее раздражает)
|
||
2. **Плавно** — используйте `ease-out` или `cubic-bezier`
|
||
3. **Ненавязчиво** — анимация не должна мешать работе
|
||
4. **Осмысленно** — анимируйте только то, что привлекает внимание к действию
|
||
|
||
---
|
||
|
||
## 12. Electron: веб-приложение как десктоп-программа
|
||
|
||
### Что такое Electron?
|
||
|
||
Electron — это фреймворк, который позволяет запускать веб-приложение **как обычную программу** на Windows, macOS и Linux. Технически, он встраивает браузер Chromium внутрь окна приложения.
|
||
|
||
Пример приложений на Electron: VS Code, Discord, Slack, Notion.
|
||
|
||
### Как это работает
|
||
|
||
```
|
||
┌──────────────────────────────────┐
|
||
│ Окно Electron (Chromium) │
|
||
│ ┌────────────────────────────┐ │
|
||
│ │ Наш React-фронтенд │ │
|
||
│ │ (http://localhost:5173) │ │
|
||
│ └────────────────────────────┘ │
|
||
│ │
|
||
│ Node.js (запускает бэкенд) │
|
||
└──────────────────────────────────┘
|
||
```
|
||
|
||
1. Electron запускает **бэкенд** (Express-сервер) как дочерний процесс
|
||
2. Открывает **окно** и загружает в него фронтенд
|
||
3. Пользователь видит обычное приложение, без адресной строки браузера
|
||
|
||
### Главный файл Electron (`electron/main.js`)
|
||
|
||
```javascript
|
||
const { app, BrowserWindow } = require("electron");
|
||
|
||
function createWindow() {
|
||
const win = new BrowserWindow({
|
||
width: 1200,
|
||
height: 800,
|
||
titleBarStyle: "hiddenInset", // macOS-стиль: кнопки светофора встроены
|
||
});
|
||
|
||
win.loadURL("http://localhost:5173");
|
||
}
|
||
|
||
app.whenReady().then(createWindow);
|
||
```
|
||
|
||
### Команды запуска
|
||
|
||
```bash
|
||
# Как десктоп-программу (Electron + бэкенд + фронтенд):
|
||
npm run dev
|
||
|
||
# Как обычное веб-приложение в браузере:
|
||
npm run dev:web
|
||
```
|
||
|
||
---
|
||
|
||
## Словарь терминов
|
||
|
||
| Термин | Объяснение |
|
||
|--------|-----------|
|
||
| **API** | Application Programming Interface — способ общения между программами |
|
||
| **HTTP** | Протокол передачи данных в вебе (GET, POST, PUT, DELETE) |
|
||
| **JSON** | Формат данных: `{"name": "Константин", "age": 25}` |
|
||
| **REST** | Стиль проектирования API — каждый ресурс имеет свой URL |
|
||
| **ORM** | Object-Relational Mapping — работа с БД через объекты вместо SQL |
|
||
| **SSE** | Server-Sent Events — односторонняя отправка данных от сервера к клиенту |
|
||
| **LLM** | Large Language Model — большая языковая модель (ИИ) |
|
||
| **Промпт** | Текстовая инструкция для ИИ |
|
||
| **Компонент** | Переиспользуемый блок интерфейса в React |
|
||
| **Состояние** | Данные компонента, при изменении которых он перерисовывается |
|
||
| **Маршрут** | Связь между URL и страницей/обработчиком |
|
||
| **Electron** | Фреймворк для создания десктоп-приложений из веб-технологий |
|
||
| **Glassmorphism** | Дизайн-тренд: полупрозрачные элементы с размытием фона |
|
||
| **@keyframes** | CSS-конструкция для описания шагов анимации |
|
||
|
||
---
|
||
|
||
## 13. Архив и поиск: работа с накопленными данными
|
||
|
||
### Зачем нужен архив?
|
||
|
||
Со временем в приложении накапливаются данные: вопросы, учебники, отчёты. Без архива старые данные «теряются» — их не видно на текущей странице. Архив собирает **всё** в одном месте.
|
||
|
||
### Группировка по датам
|
||
|
||
Данные в архиве организованы по дням. Для этого мы берём массив объектов и группируем:
|
||
|
||
```typescript
|
||
function groupByDate(items) {
|
||
const groups = {};
|
||
for (const item of items) {
|
||
const date = item.createdAt.split("T")[0]; // "2026-03-27T15:30:00" → "2026-03-27"
|
||
if (!groups[date]) groups[date] = [];
|
||
groups[date].push(item);
|
||
}
|
||
return groups;
|
||
// Результат: { "2026-03-27": [...], "2026-03-26": [...] }
|
||
}
|
||
```
|
||
|
||
### Поиск (фильтрация)
|
||
|
||
Поиск на фронтенде — это **фильтрация** массива по введённому тексту:
|
||
|
||
```typescript
|
||
const filtered = questions.filter(
|
||
(q) => q.text.toLowerCase().includes(search.toLowerCase())
|
||
);
|
||
```
|
||
|
||
`includes()` проверяет, содержит ли строка подстроку. `toLowerCase()` делает поиск нечувствительным к регистру.
|
||
|
||
### Табы (вкладки)
|
||
|
||
Табы — это состояние, определяющее какой контент показывать:
|
||
|
||
```typescript
|
||
const [tab, setTab] = useState("questions"); // "questions" | "textbooks" | "reports"
|
||
|
||
// В зависимости от tab показываем разные списки
|
||
{tab === "questions" && <QuestionsList />}
|
||
{tab === "textbooks" && <TextbooksList />}
|
||
```
|
||
|
||
---
|
||
|
||
## 14. Умное управление полями ввода
|
||
|
||
### Задача
|
||
|
||
По ТЗ нужно заполнить **минимум 5 вопросов в день**. Но если 3 уже сохранены — показывать 5 полей глупо. Нужно показать только 2 оставшихся (плюс кнопку «Ещё»).
|
||
|
||
### Решение
|
||
|
||
```typescript
|
||
function updateFieldCount(savedCount: number) {
|
||
const needed = Math.max(1, 5 - savedCount);
|
||
// Если сохранено 0 → 5 полей
|
||
// Если сохранено 3 → 2 поля
|
||
// Если сохранено 5+ → 1 поле (всегда можно добавить ещё)
|
||
setFields(Array(needed).fill(""));
|
||
}
|
||
```
|
||
|
||
`Math.max(1, ...)` гарантирует, что **хотя бы одно поле** всегда доступно.
|
||
|
||
---
|
||
|
||
*Учебник обновлён. Новые разделы: архив с поиском, умное управление полями.*
|