Files
pyisu/v2.md
2026-03-13 14:39:43 +08:00

1111 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Руководство по работе с Vditor
## 6. Отображение контента без редактора
### 6.1 Рендеринг HTML с поддержкой KaTeX
```javascript
// Функция для отображения контента без редактора
function renderContentWithoutEditor(content, containerId, options = {}) {
const container = document.getElementById(containerId);
if (!container) {
console.error(`Контейнер ${containerId} не найден`);
return;
}
const config = {
enableMath: true,
enableSyntaxHighlight: true,
theme: 'light',
...options
};
// Конвертация Markdown в HTML если нужно
let htmlContent;
if (content.markdown) {
htmlContent = convertMarkdownToHTML(content.markdown);
} else if (content.html) {
htmlContent = content.html;
} else {
htmlContent = convertMarkdownToHTML(content);
}
// Создание структуры для отображения
container.innerHTML = `
<div class=\\\"rendered-content ${config.theme}-theme\\\">
<div class=\\\"content-body\\\">
${htmlContent}
</div>
</div>
`;
// Инициализация KaTeX для рендеринга формул
if (config.enableMath && typeof katex !== 'undefined') {
renderLatexFormulas(container);
}
// Подсветка синтаксиса
if (config.enableSyntaxHighlight && typeof hljs !== 'undefined') {
hljs.highlightAll();
}
// Добавление стилей если их нет
addContentStyles();
}
// Рендеринг LaTeX формул в контейнере
function renderLatexFormulas(container) {
if (!container || typeof katex === 'undefined') return;
// Рендеринг display формул ($$...$$)
container.querySelectorAll('.language-math.display, .katex-display').forEach(element => {
try {
const formula = element.textContent.trim();
katex.render(formula, element, {
displayMode: true,
throwOnError: false,
errorColor: '#cc0000'
});
} catch (error) {
console.error('Ошибка рендеринга display формулы:', error);
element.innerHTML = `<div class=\\\"latex-error\\\">Ошибка формулы: ${element.textContent}</div>`;
}
});
// Рендеринг inline формул ($...$)
container.querySelectorAll('.language-math:not(.display), .katex').forEach(element => {
try {
const formula = element.textContent.trim();
katex.render(formula, element, {
displayMode: false,
throwOnError: false,
errorColor: '#cc0000'
});
} catch (error) {
console.error('Ошибка рендеринга inline формулы:', error);
element.innerHTML = `<span class=\\\"latex-error\\\">${element.textContent}</span>`;
}
});
// Обработка формул в тексте (если они не обернуты в теги)
const textNodes = Array.from(container.childNodes).filter(node =>
node.nodeType === Node.TEXT_NODE &&
(node.textContent.includes('$') || node.textContent.includes('\\\\\\\\['))
);
textNodes.forEach(node => {
const wrapper = document.createElement('span');
wrapper.innerHTML = node.textContent
.replace(/\\\\$\\\\$(.*?)\\\\$\\\\$/g, (match, formula) => {
try {
return katex.renderToString(formula, { displayMode: true });
} catch (e) {
return `<div class=\\\"latex-error\\\">Ошибка: ${formula}</div>`;
}
})
.replace(/\\\\$(.*?)\\\\$/g, (match, formula) => {
try {
return katex.renderToString(formula, { displayMode: false });
} catch (e) {
return `<span class=\\\"latex-error\\\">${formula}</span>`;
}
});
node.parentNode.replaceChild(wrapper, node);
});
}
// Добавление необходимых стилей
function addContentStyles() {
if (document.getElementById('content-render-styles')) return;
const styles = `
<style id=\\\"content-render-styles\\\">
.rendered-content {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
line-height: 1.6;
color: #333;
}
.rendered-content.light-theme {
background: #fff;
color: #333;
}
.rendered-content.dark-theme {
background: #1a1a1a;
color: #e0e0e0;
}
.content-body {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.content-body h1,
.content-body h2,
.content-body h3,
.content-body h4 {
margin-top: 1.5em;
margin-bottom: 0.5em;
font-weight: 600;
line-height: 1.25;
}
.content-body h1 { font-size: 2em; border-bottom: 2px solid #eee; padding-bottom: 0.3em; }
.content-body h2 { font-size: 1.5em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
.content-body h3 { font-size: 1.25em; }
.content-body h4 { font-size: 1em; }
.content-body p {
margin: 1em 0;
}
.content-body ul,
.content-body ol {
padding-left: 2em;
margin: 1em 0;
}
.content-body li {
margin: 0.5em 0;
}
.content-body blockquote {
margin: 1em 0;
padding: 0.5em 1em;
border-left: 4px solid #ddd;
background: #f9f9f9;
color: #666;
}
.content-body pre {
background: #f6f8fa;
border-radius: 6px;
padding: 16px;
overflow: auto;
margin: 1em 0;
}
.content-body code {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
padding: 0.2em 0.4em;
background: rgba(175, 184, 193, 0.2);
border-radius: 3px;
font-size: 0.9em;
}
.content-body pre code {
background: transparent;
padding: 0;
}
.content-body table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}
.content-body th,
.content-body td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.content-body th {
background: #f6f8fa;
font-weight: 600;
}
.content-body tr:nth-child(even) {
background: #f9f9f9;
}
.latex-error {
color: #d73a49;
background: #ffebee;
padding: 4px 8px;
border-radius: 3px;
font-family: monospace;
}
.katex-display {
overflow-x: auto;
overflow-y: hidden;
padding: 10px 0;
}
.katex {
font-size: 1.1em;
}
</style>
`;
document.head.insertAdjacentHTML('beforeend', styles);
}
```
### 6.2 Пример использования
```javascript
// Пример 1: Отображение сохраненного контента
async function displaySavedContent(contentId) {
try {
// Загрузка контента из базы данных
const response = await fetch(`/api/content/${contentId}`);
const content = await response.json();
// Отображение в контейнере
renderContentWithoutEditor(content, 'content-container', {
theme: 'light',
enableMath: true,
enableSyntaxHighlight: true
});
} catch (error) {
console.error('Ошибка загрузки контента:', error);
document.getElementById('content-container').innerHTML = `
<div class=\\\"error-message\\\">
<h3>Ошибка загрузки контента</h3>
<p>${error.message}</p>
</div>
`;
}
}
// Пример 2: Предпросмотр перед сохранением
function previewContent() {
if (!vditorInstance) return;
const content = {
markdown: vditorInstance.getValue(),
html: vditorInstance.getHTML()
};
// Открытие предпросмотра в модальном окне
const modal = document.createElement('div');
modal.className = 'preview-modal';
modal.innerHTML = `
<div class=\\\"modal-content\\\">
<div class=\\\"modal-header\\\">
<h3>Предпросмотр</h3>
<button class=\\\"close-btn\\\">&times;</button>
</div>
<div class=\\\"modal-body\\\" id=\\\"preview-container\\\"></div>
</div>
`;
document.body.appendChild(modal);
// Отображение контента
renderContentWithoutEditor(content, 'preview-container', {
theme: 'light',
enableMath: true
});
// Закрытие модального окна
modal.querySelector('.close-btn').addEventListener('click', () => {
modal.remove();
});
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
}
});
}
// Пример 3: Экспорт в PDF/печать
function exportToPDF(content, filename = 'document') {
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>${filename}</title>
<meta charset=\\\"UTF-8\\\">
<link rel=\\\"stylesheet\\\" href=\\\"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css\\\">
<link rel=\\\"stylesheet\\\" href=\\\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css\\\">
<style>
@media print {
body {
font-family: 'Times New Roman', Times, serif;
line-height: 1.5;
font-size: 12pt;
color: #000;
}
h1, h2, h3, h4 {
page-break-after: avoid;
}
pre, blockquote, table {
page-break-inside: avoid;
}
.katex-display {
page-break-inside: avoid;
}
}
body {
max-width: 210mm;
margin: 20mm auto;
padding: 0 20mm;
}
.katex { font-size: 1em; }
</style>
</head>
<body>
${content.html || convertMarkdownToHTML(content.markdown || content)}
</body>
</html>
`);
printWindow.document.close();
// Автоматическая печать после загрузки
printWindow.onload = function() {
printWindow.print();
};
}
```
### 6.3 Оптимизация производительности
```javascript
// Ленивая загрузка KaTeX и Highlight.js
let katexLoaded = false;
let hljsLoaded = false;
async function loadMathRenderer() {
if (katexLoaded) return;
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js';
script.onload = () => {
katexLoaded = true;
resolve();
};
script.onerror = reject;
document.head.appendChild(script);
const css = document.createElement('link');
css.rel = 'stylesheet';
css.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css';
document.head.appendChild(css);
});
}
async function loadSyntaxHighlighter() {
if (hljsLoaded) return;
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js';
script.onload = () => {
hljsLoaded = true;
resolve();
};
script.onerror = reject;
document.head.appendChild(script);
const css = document.createElement('link');
css.rel = 'stylesheet';
css.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css';
document.head.appendChild(css);
});
}
// Оптимизированный рендеринг с ленивой загрузкой
async function renderContentOptimized(content, containerId) {
const container = document.getElementById(containerId);
// Показываем спиннер загрузки
container.innerHTML = '<div class=\\\"loading-spinner\\\">Загрузка...</div>';
try {
// Параллельная загрузка зависимостей
await Promise.all([
loadMathRenderer(),
loadSyntaxHighlighter()
]);
// Рендеринг контента
renderContentWithoutEditor(content, containerId, {
enableMath: true,
enableSyntaxHighlight: true
});
} catch (error) {
console.error('Ошибка загрузки зависимостей:', error);
container.innerHTML = `
<div class=\\\"error\\\">
<h3>Ошибка отображения контента</h3>
<p>${error.message}</p>
<button onclick=\\\"renderContentOptimized(content, '${containerId}')\\\">
Повторить попытку
</button>
</div>
`;
}
}
```
## 7. Хранение в базе данных
### 7.1 Структура таблиц для хранения контента
```sql
-- Таблица для хранения курсов/статей
CREATE TABLE content (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
description TEXT,
markdown_content TEXT NOT NULL,
html_content TEXT,
metadata JSONB DEFAULT '{}',
author_id UUID REFERENCES users(id),
status VARCHAR(50) DEFAULT 'draft',
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
published_at TIMESTAMP WITH TIME ZONE,
version INTEGER DEFAULT 1
);
-- Индексы для оптимизации поиска
CREATE INDEX idx_content_author ON content(author_id);
CREATE INDEX idx_content_status ON content(status);
CREATE INDEX idx_content_created ON content(created_at);
CREATE INDEX idx_content_title ON content(title);
CREATE INDEX idx_content_metadata ON content USING gin(metadata);
-- Таблица для версий контента (история изменений)
CREATE TABLE content_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content_id UUID REFERENCES content(id) ON DELETE CASCADE,
markdown_content TEXT NOT NULL,
html_content TEXT,
version INTEGER NOT NULL,
change_description TEXT,
author_id UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Индекс для быстрого поиска версий
CREATE INDEX idx_content_versions_content ON content_versions(content_id);
CREATE INDEX idx_content_versions_version ON content_versions(version);
-- Таблица для извлеченных формул (для поиска и индексации)
CREATE TABLE content_formulas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content_id UUID REFERENCES content(id) ON DELETE CASCADE,
formula_text TEXT NOT NULL,
formula_type VARCHAR(20), -- 'inline' или 'display'
position INTEGER,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Индекс для поиска формул
CREATE INDEX idx_formulas_content ON content_formulas(content_id);
CREATE INDEX idx_formulas_text ON content_formulas USING gin(formula_text gin_trgm_ops);
```
### 7.2 Модели данных для Node.js/Express
```javascript
// models/Content.js
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Content = sequelize.define('Content', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
title: {
type: DataTypes.STRING(255),
allowNull: false,
validate: {
notEmpty: true
}
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
markdownContent: {
type: DataTypes.TEXT,
allowNull: false,
field: 'markdown_content'
},
htmlContent: {
type: DataTypes.TEXT,
allowNull: true,
field: 'html_content'
},
metadata: {
type: DataTypes.JSONB,
defaultValue: {},
get() {
const rawValue = this.getDataValue('metadata');
return rawValue ? rawValue : {};
}
},
status: {
type: DataTypes.ENUM('draft', 'published', 'archived'),
defaultValue: 'draft'
},
version: {
type: DataTypes.INTEGER,
defaultValue: 1
},
publishedAt: {
type: DataTypes.DATE,
allowNull: true,
field: 'published_at'
}
}, {
tableName: 'content',
timestamps: true,
underscored: true,
hooks: {
beforeSave: async (content) => {
// Автоматическая генерация HTML из Markdown
if (content.changed('markdownContent') && content.markdownContent) {
content.htmlContent = await convertMarkdownToHTML(content.markdownContent);
// Извлечение и сохранение формул
await extractAndSaveFormulas(content);
}
// Обновление версии при изменении контента
if (content.changed('markdownContent')) {
content.version += 1;
}
// Обновление времени публикации
if (content.changed('status') && content.status === 'published') {
content.publishedAt = new Date();
}
},
afterSave: async (content) => {
// Сохранение версии в историю
if (content.changed('markdownContent')) {
await sequelize.models.ContentVersion.create({
contentId: content.id,
markdownContent: content.markdownContent,
htmlContent: content.htmlContent,
version: content.version,
authorId: content.authorId
});
}
}
}
});
Content.associate = (models) => {
Content.belongsTo(models.User, {
foreignKey: 'author_id',
as: 'author'
});
Content.hasMany(models.ContentVersion, {
foreignKey: 'content_id',
as: 'versions'
});
Content.hasMany(models.ContentFormula, {
foreignKey: 'content_id',
as: 'formulas'
});
};
return Content;
};
// models/ContentVersion.js
module.exports = (sequelize) => {
const ContentVersion = sequelize.define('ContentVersion', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
markdownContent: {
type: DataTypes.TEXT,
allowNull: false,
field: 'markdown_content'
},
htmlContent: {
type: DataTypes.TEXT,
allowNull: true,
field: 'html_content'
},
version: {
type: DataTypes.INTEGER,
allowNull: false
},
changeDescription: {
type: DataTypes.TEXT,
allowNull: true,
field: 'change_description'
}
}, {
tableName: 'content_versions',
timestamps: true,
underscored: true
});
ContentVersion.associate = (models) => {
ContentVersion.belongsTo(models.Content, {
foreignKey: 'content_id',
as: 'content'
});
ContentVersion.belongsTo(models.User, {
foreignKey: 'author_id',
as: 'author'
});
};
return ContentVersion;
};
// Вспомогательные функции
async function convertMarkdownToHTML(markdown) {
// Используем Lute или marked.js для конвертации
// с поддержкой KaTeX
// ...
}
async function extractAndSaveFormulas(content) {
const formulas = extractLatexFormulas(content.markdownContent);
// Удаляем старые формулы
await sequelize.models.ContentFormula.destroy({
where: { contentId: content.id }
});
// Сохраняем новые формулы
const formulaPromises = [];
formulas.inline.forEach((formula, index) => {
formulaPromises.push(
sequelize.models.ContentFormula.create({
contentId: content.id,
formulaText: formula.formula,
formulaType: 'inline',
position: formula.position
})
);
});
formulas.display.forEach((formula, index) => {
formulaPromises.push(
sequelize.models.ContentFormula.create({
contentId: content.id,
formulaText: formula.formula,
formulaType: 'display',
position: formula.position
})
);
});
await Promise.all(formulaPromises);
}
```
### 7.3 API endpoints для работы с контентом
```javascript
// routes/content.js
const express = require('express');
const router = express.Router();
const { Content, ContentVersion } = require('../models');
const auth = require('../middleware/auth');
// Получение списка контента
router.get('/', auth, async (req, res) => {
try {
const { page = 1, limit = 20, status, search } = req.query;
const offset = (page - 1) * limit;
const where = {};
if (status) {
where.status = status;
}
if (search) {
where[Op.or] = [
{ title: { [Op.iLike]: `%${search}%` } },
{ description: { [Op.iLike]: `%${search}%` } }
];
}
const { count, rows } = await Content.findAndCountAll({
where,
limit: parseInt(limit),
offset: parseInt(offset),
order: [['created_at', 'DESC']],
include: [{
model: User,
as: 'author',
attributes: ['id', 'name', 'email']
}]
});
res.json({
data: rows,
pagination: {
total: count,
page: parseInt(page),
limit: parseInt(limit),
pages: Math.ceil(count / limit)
}
});
} catch (error) {
console.error('Ошибка получения контента:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Создание нового контента
router.post('/', auth, async (req, res) => {
try {
const { title, description, markdownContent, metadata } = req.body;
const content = await Content.create({
title,
description,
markdownContent,
metadata: metadata || {},
authorId: req.user.id
});
res.status(201).json({
message: 'Контент создан',
data: content
});
} catch (error) {
console.error('Ошибка создания контента:', error);
res.status(400).json({ error: error.message });
}
});
// Получение контента по ID
router.get('/:id', async (req, res) => {
try {
const content = await Content.findByPk(req.params.id, {
include: [{
model: User,
as: 'author',
attributes: ['id', 'name', 'email']
}]
});
if (!content) {
return res.status(404).json({ error: 'Контент не найден' });
}
res.json({
data: content
});
} catch (error) {
console.error('Ошибка получения контента:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Обновление контента
router.put('/:id', auth, async (req, res) => {
try {
const { title, description, markdownContent, metadata } = req.body;
const content = await Content.findByPk(req.params.id);
if (!content) {
return res.status(404).json({ error: 'Контент не найден' });
}
// Проверка прав доступа
if (content.authorId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Доступ запрещен' });
}
// Обновление полей
await content.update({
title: title || content.title,
description: description !== undefined ? description : content.description,
markdownContent: markdownContent || content.markdownContent,
metadata: metadata || content.metadata
});
res.json({
message: 'Контент обновлен',
data: content
});
} catch (error) {
console.error('Ошибка обновления контента:', error);
res.status(400).json({ error: error.message });
}
});
// Публикация контента
router.post('/:id/publish', auth, async (req, res) => {
try {
const content = await Content.findByPk(req.params.id);
if (!content) {
return res.status(404).json({ error: 'Контент не найден' });
}
if (content.authorId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Доступ запрещен' });
}
await content.update({
status: 'published',
publishedAt: new Date()
});
res.json({
message: 'Контент опубликован',
data: content
});
} catch (error) {
console.error('Ошибка публикации:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Получение версий контента
router.get('/:id/versions', auth, async (req, res) => {
try {
const versions = await ContentVersion.findAll({
where: { contentId: req.params.id },
order: [['version', 'DESC']],
include: [{
model: User,
as: 'author',
attributes: ['id', 'name', 'email']
}]
});
res.json({
data: versions
});
} catch (error) {
console.error('Ошибка получения версий:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Восстановление версии
router.post('/:id/restore/:versionId', auth, async (req, res) => {
try {
const version = await ContentVersion.findByPk(req.params.versionId, {
include: [{
model: Content,
as: 'content',
where: { id: req.params.id }
}]
});
if (!version) {
return res.status(404).json({ error: 'Версия не найдена' });
}
const content = version.content;
if (content.authorId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Доступ запрещен' });
}
// Восстановление контента из версии
await content.update({
markdownContent: version.markdownContent,
htmlContent: version.htmlContent,
version: content.version + 1
});
res.json({
message: 'Версия восстановлена',
data: content
});
} catch (error) {
console.error('Ошибка восстановления:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
module.exports = router;
```
### 7.4 Оптимизация хранения больших документов
```javascript
// Стратегии хранения больших документов
// 1. Компрессия контента
async function compressContent(content) {
// Используем gzip для сжатия больших текстов
const compressed = await new Promise((resolve, reject) => {
zlib.gzip(content, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
return compressed.toString('base64');
}
async function decompressContent(compressed) {
const buffer = Buffer.from(compressed, 'base64');
return new Promise((resolve, reject) => {
zlib.gunzip(buffer, (err, result) => {
if (err) reject(err);
else resolve(result.toString());
});
});
}
// 2. Разделение контента на части
async function splitLargeContent(content, maxChunkSize = 100000) {
if (content.length <= maxChunkSize) {
return [content];
}
const chunks = [];
let currentChunk = '';
const lines = content.split('\\
');
for (const line of lines) {
if ((currentChunk + line + '\\
').length > maxChunkSize) {
chunks.push(currentChunk);
currentChunk = line + '\\
';
} else {
currentChunk += line + '\\
';
}
}
if (currentChunk) {
chunks.push(currentChunk);
}
return chunks;
}
// 3. Ленивая загрузка частей контента
class LazyContentLoader {
constructor(contentId) {
this.contentId = contentId;
this.chunks = [];
this.loadedChunks = new Set();
}
async getChunk(index) {
if (this.loadedChunks.has(index)) {
return this.chunks[index];
}
// Загрузка чанка из базы данных
const response = await fetch(`/api/content/${this.contentId}/chunks/${index}`);
const chunk = await response.json();
this.chunks[index] = chunk;
this.loadedChunks.add(index);
return chunk;
}
async *getContent() {
// Получаем метаданные о количестве чанков
const metadata = await fetch(`/api/content/${this.contentId}/metadata`);
const { totalChunks } = await metadata.json();
for (let i = 0; i < totalChunks; i++) {
yield await this.getChunk(i);
}
}
}
// 4. Кэширование в Redis
const redis = require('redis');
const client = redis.createClient();
async function getContentWithCache(contentId) {
const cacheKey = `content:${contentId}`;
// Попытка получить из кэша
const cached = await client.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Загрузка из базы данных
const content = await Content.findByPk(contentId);
if (!content) {
return null;
}
// Сохранение в кэш на 1 час
await client.setex(cacheKey, 3600, JSON.stringify(content));
return content;
}
// 5. Индексация для полнотекстового поиска
async function indexContentForSearch(content) {
// Извлечение текста без Markdown разметки
const plainText = content.markdownContent
.replace(/#+\\\\s*/g, '') // Удаляем заголовки
.replace(/\\\\*\\\\*?(.*?)\\\\*\\\\*?/g, '$1') // Удаляем жирный/курсив
.replace(/`(.*?)`/g, '$1') // Удаляем inline код
.replace(/\\\\[(.*?)\\\\]\\\\(.*?\\\\)/g, '$1') // Удаляем ссылки
.replace(/\\\\$\\\\$?(.*?)\\\\$?\\\\$/g, '') // Удаляем формулы
.replace(/```[\\\\s\\\\S]*?```/g, '') // Удаляем блоки кода
.replace(/[^\\\\w\\\\sа-яА-ЯёЁ]/g, ' ') // Оставляем только буквы и пробелы
.replace(/\\\\s+/g, ' ') // Убираем лишние пробелы
.trim();
// Сохранение в поисковый индекс (Elasticsearch или PostgreSQL tsvector)
await saveToSearchIndex(content.id, plainText, {
title: content.title,
description: content.description,
author: content.authorId,
status: content.status,
publishedAt: content.publishedAt
});
}
```
## 8. Примеры использования
### 8.1 Полный пример: Система управления курсами
```javascript
// course-system.js
class CourseSystem {
constructor() {
this.vditorInstance = null;
this.currentCourse = null;
this.draftManager = null;
}
// Инициализация системы
async initialize() {
// Загрузка Vditor
await this.loadVditor();
// Инициализация менеджера черновиков
this.draftManager = new DraftManager(this.vditorInstance, 'course_draft');
this.draftManager.enableAutoSave(30000); // Автосохранение каждые 30 секунд
// Настройка обработчиков событий
this.setupEventHandlers();
// Загрузка сохраненного черновика
const draft = this.draftManager.loadDraft();
if (draft) {
this.showDraftNotification(draft);
}
console.log('Система управления курсами инициализирована');
}
// Загрузка Vditor
async loadVditor() {
return new Promise((resolve, reject) => {
if (typeof Vditor !== 'undefined') {
this.initVditorEditor();
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://unpkg.com/vditor@3.10.4/dist/index.min.js';
script.onload = () => {
this.initVditorEditor();
resolve();
};
script.onerror = reject;
document.head.appendChild(script);
});
}
// Инициализация редактора
initVditorEditor() {
this.vditorInstance = new Vditor('vditor-container', {
height: 600,
placeholder: 'Начните создавать курс...',
theme: 'classic',
lang: 'ru_RU',
toolbar: [
'emoji', 'headings', 'bold', 'italic',.