
Vue.js Компоненти для перетворення мови в мову: створення реактивних реальних аудіо-додатків у режимі реального часу
https://ift.tt/SoLsvnT
Вступ: чому Vue.js Компоненти змінили все
Коли я вперше взявся за створення фронтенду сервісу реального часу перекладу мови в мову, я вже реалізував версію на React із використанням хуків. «Наскільки іншим може бути Vue?» — подумав я. Виявилося, що Composition API Vue пропонує свіжий й елегантний підхід до управління з’єднаннями WebSocket, аудіо-потоками та реактивним станом, що змусив мене переглянути структуру реальних застосунків.
У цій статті я поділюся уроками, які отримав, будуючи Vue.js композабли для реального часу аудіо сервісів перекладу, викликами, з якими зіткнувся, і паттернами, що виникли під час керування реактивними з’єднаннями WebSocket та відтворенням аудіо. Чи ви приходите з React, Angular, або є досвідченим користувачем Vue — тут знайдеться щось корисне для всіх, хто будує реальновчасові аудіо-додатки.
Виклик: потокове аудіо в реальному часі у браузері
Перш ніж переходити до коду, розглянемо, що саме ми будуємо:
– Управління з’єднанням WebSocket: встановлення та підтримка постійного з’єднання з бекендом FastAPI
– Захоплення аудіо: доступ до мікрофона користувача та передача аудіо-блоків
– Реактивний стан: синхронізація UI з станом з’єднання, результатами транскрипції та помилками
– Управління життєвим циклом: коректне очищення ресурсів при відмонтуванні компонентів
– Обробка помилок: коректне оброблення відмови у мережі, відхилення дозволів та помилок API
Краса Composition API Vue полягає в тому, що ми можемо інкапсулювати кожну з цих проблем у повторно використовуючіся, тестовані композабли.
Урок 1: Композабли — це більше, ніж React хуки з іншим синтаксисом
Якщо ви перейшли з React, спочатку думав, що композабли — це просто хуки з іншою назвою. Я помилявся. Ось у чому їхня відмінність:
Vue Композабли мають прямі реактивні посилання
// React Hook — потрібен useState
const [isConnected, setIsConnected] = useState(false);
// Vue Композаб — пряме реактивне посилання
const isConnected = ref(false);
Це може здаватися дрібницею, але має глибокі наслідки:
– Немає залежних масивів: реактивність Vue відстежує залежності автоматично
– Немає застарілих закриттів: ви завжди працюєте з поточним значенням
– Простіша розумова модель: просто мутуйте властивість .value
Життєвий цикл більш явний
// React потребує useEffect із чисткою
useEffect(() => {
// Налаштування
return () => {
// Очистка
};
}, []);
// Vue має спеціалізовані хуки життєвого циклу
onUnmounted(() => {
// Очистка
});
Урок 2: Створення WebSocket Композаблі
Дозвольте показати ядро композаблі useTranscription, яке я побудував. Це обробляє все зв’язування з нашим FastAPI бекендом через WebSocket.
Базова структура
// composables/useTranscription.ts
import { ref, onUnmounted } from ‘vue’;
import { v4 as uuidv4 } from ‘uuid’;
export function useTranscription(apiUrl = ‘ws://localhost:8000’) {
// Реактивний стан
const ws = ref
const sessionId = ref
const isConnected = ref(false);
const isTranscribing = ref(false);
const partialTranscript = ref(”);
const finalTranscript = ref(”);
const error = ref
// Методи будуть тут…
// Очистка при розмонтуванні
onUnmounted(() => {
disconnect();
});
return {
// Стан
isConnected,
isTranscribing,
partialTranscript,
finalTranscript,
error,
sessionId,
// Методи
connect,
startTranscription,
sendAudioChunk,
stopTranscription,
disconnect,
};
}
Ключове рішення дизайну: обіцянкове з’єднання
Одна з помилок, яку я спочатку зробив — зробити з’єднання синхронним. Коли UI намагався розпочати запис одразу після з’єднання, WebSocket ще не був готовий. Ось як я це вирішив:
const connect = async (): Promise
return new Promise((resolve, reject) => {
try {
sessionId.value = uuidv4();
const url = `${apiUrl}/transcribe/${sessionId.value}`;
ws.value = new WebSocket(url);
ws.value.onopen = () => {
console.log(‘WebSocket connected’);
};
ws.value.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.event === ‘connected’) {
isConnected.value = true;
resolve(); // ✅ Розв’язати, коли справді готово
}
// Обробка інших повідомлень…
};
ws.value.onerror = (err) => {
error.value = ‘WebSocket connection error’;
reject(err);
};
// Таймаут через 10 секунд
setTimeout(() => {
if (!isConnected.value) {
reject(new Error(‘Connection timeout’));
}
}, 10000);
} catch (err) {
reject(err);
}
});
};
Чому це важливо: UI тепер може надійно чекати з’єднання
// У вашому компоненті
const handleConnect = async () => {
try {
await connect(); // ✅ Чекає фактичного з’єднання
await startTranscription(); // ✅ Безпечно розпочати зараз
} catch (err) {
console.error(‘Connection failed:’, err);
}
};
Урок 3: Реактивна обробка повідомлень
Краса реактивності Vue сяє під час обробки повідомлень WebSocket. Так я структурую обробку повідомлень:
const handleTranscript = (message: TranscriptResult) => {
if (message.type === ‘partial’) {
partialTranscript.value = message.transcript || ”;
} else if (message.type === ‘final’) {
finalTranscript.value += (message.transcript || ”) + ‘ ‘;
partialTranscript.value = ”; // Очистити частковий
}
};
У шаблоні це просто працює:
Ніяких ручних оновлень, немає forceUpdate, немає колбеків setState. Реактивність Vue обробляє все.
Урок 4: Композабля захоплення аудіо
Друга важлива композабля — обробка мікрофона. Тут браузерні API зустрічаються з реактивністю Vue:
// composables/useAudioCapture.ts
import { ref, onUnmounted } from ‘vue’;
export function useAudioCapture() {
const isRecording = ref(false);
const mediaRecorder = ref
const audioStream = ref
const startRecording = async (
onAudioData: (data: Blob) => void
): Promise
try {
// Запит мікрофона
audioStream.value = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: 1, // моно
sampleRate: 16000, // 16 кГц для мови
echoCancellation: true,
noiseSuppression: true,
},
});
mediaRecorder.value = new MediaRecorder(audioStream.value, {
mimeType: ‘audio/webm’,
audioBitsPerSecond: 16000,
});
// Потоки аудіо-часток
mediaRecorder.value.ondataavailable = (event) => {
if (event.data.size > 0) {
onAudioData(event.data); // Виклик для відправки через WebSocket
}
};
// Захоплення відбувається кожні 100мс
mediaRecorder.value.start(100);
isRecording.value = true;
} catch (err) {
console.error(‘Microphone access denied:’, err);
throw err;
}
};
const stopRecording = () => {
if (mediaRecorder.value && isRecording.value) {
mediaRecorder.value.stop();
isRecording.value = false;
}
// Важливо: зупинити всі треки, щоб звільнити мікрофон
if (audioStream.value) {
audioStream.value.getTracks().forEach((track) => track.stop());
audioStream.value = null;
}
};
// Автоматичне очищення
onUnmounted(() => {
stopRecording();
});
return {
isRecording,
startRecording,
stopRecording,
};
}
Критичний урок: очищення ресурсів
Я навчився цього болем: невтамоване зупинення медіатреків залишає мікрофон активним після розмонтування компонента. Користувачі бачать страшний індикатор «мікрофон активний» у браузері, і вони лякаються.
Завжди зупиняйте медіа-треки:
audioStream.value.getTracks().forEach((track) => track.stop());
Урок 5: Поєднання у компоненті
Ось як красиво ці композабли працюють разом:
Урок 6: Composition API проти Options API
Може виникнути питання: «Чому використовувати Composition API замість Options API?»
Ось що я виявив:
Переваги Composition API:
– Краща організація коду: пов’язані логіки залишаються разом
– Легше тестувати: композабли — це просто функції
– Підтримка TypeScript: висновок працює чудово
– Перевикористання: легко ділитися логікою між компонентами
Options API був кращим для:
– Малих, простих компонентів: іноді data() та methods ясніші
– Вивчення Vue: структура більш явна
– Командна знайомість: якщо ваша команда добре знає Options API
Для реальних застосунків із складним управлінням станом Composition API — явний переможець.
Урок 7: Обробка помилок та крайових випадків
Реальні застосунки потребують надійної обробки помилок. Ось крайові варіанти, з якими я стикався:
1. Користувач відмовляється у доступі до мікрофона
const handleStart = async () => {
try {
await startRecording(onAudioData);
} catch (err) {
if (err.name === ‘NotAllowedError’) {
error.value = ‘Дозвіл на використання мікрофона відмовлено. Дозвольте доступ.’;
} else {
error.value = ‘Не вдалося почати запис’;
}
}
};
2. Втрачено з’єднання WebSocket
ws.value.onclose = (event) => {
isConnected.value = false;
isTranscribing.value = false;
if (!event.wasClean) {
error.value = ‘З’єднання втрачено. Оновіть сторінку та спробуйте знову.’;
}
};
3. Браузерна сумісність
const checkBrowserSupport = () => {
if (!(‘MediaRecorder’ in window)) {
throw new Error(‘MediaRecorder API не підтримується’);
}
if (!(‘WebSocket’ in window)) {
throw new Error(‘WebSocket API не підтримується’);
}
};
Урок 8: Оптимізація продуктивності
Реал-тайм застосунки вимагають відмінної продуктивності. Ось що працювало:
1. Згладжування оновлень UI для часткових транскриптів
import { ref, watch } from ‘vue’;
import { debounce } from ‘lodash-es’;
const debouncedPartial = ref(”);
watch(partialTranscript, debounce((newValue) => {
debouncedPartial.value = newValue;
}, 50));
2. Використання shallowRef для великих об’єктів
import { shallowRef } from ‘vue’;
// Не робити кожну властивість реактивною
const allTranscripts = shallowRef
3. Ліниве завантаження аудіо-компонентів
Урок 9: Тестування композаблі
Composition API спрощує тестування. Ось як я тестую WebSocket композаблі:
import { describe, it, expect, vi } from ‘vitest’;
import { useTranscription } from ‘../useTranscription’;
describe(‘useTranscription’, () => {
it(‘має підключитися до WebSocket’, async () => {
const { connect, isConnected } = useTranscription();
await connect();
expect(isConnected.value).toBe(true);
});
it(‘має обробляти повідомлення транскрипції’, async () => {
const { connect, partialTranscript } = useTranscription();
await connect();
// Імітувати повідомлення WebSocket
const message = {
event: ‘transcript’,
type: ‘partial’,
transcript: ‘Hello world’,
};
// Випустити повідомлення…
expect(partialTranscript.value).toBe(‘Hello world’);
});
});
Урок 10: Що б я зробив інакше
Підіб’ємо підсумок: що б я змінив:
1. Додати логіку повторного підключення раніше
Замість того щоб провалюватися через помилки з’єднання, реалізуйте експоненційне відступлення:
const connectWithRetry = async (maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
await connect();
return;
} catch (err) {
if (i === maxRetries - 1) throw err;
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, i) * 1000)
);
}
}
};
2. Одразу використовувати TypeScript
Типобезпека виявила багато помилок під час рефакторингу.
3. Реалізувати Audio Queueing
Для перекладу в режимі реального часу потрібно було б чергував аудіо-вивід, щоб запобігати перекриванню відтворення.
4. Додати Логіку стану з’єднання
Керування станами з’єднання (підключення, підключено, відключення, відсутність з’єднання, помилка) полегшило б логіку.
Порівняння: Vue композабли проти React хуків
Після реалізації обох підходів, чесне порівняння:
Аспект
Vue Композабли
React хуки
Навчальний поріг
М’якший для початківців
Зміцнений (масив залежностей, замикання)
Реактивність
Автоматична, чарівна ✨
Ручна через setState
Керування посиланнями
Явне доступ до .value
Інтеграція життєвого циклу
Спеціалізовані хуки
useEffect з залежностями
Повторне використання коду
natural і інтуїтивне
Потребує обережного дизайну хуків
Підтримка TypeScript
Висновок
Для реального часу застосунків Vue з композаблами забезпечують значні переваги: автоматична реактивність зменшує ментальне навантаження, а React хуки надають більше контролю, але потребують більше boilerplate. Реальні програми з використанням моделі Composition API та TypeScript надають серйозні переваги.
Реальні результати продуктивності
Ось що вдалося досягти з цією імплементацією Vue:
– Встановлення з’єднання: у середньому ~200 мс
– Передача аудіо-блоків: інтервали 100 мс, затримка <10 мс
- Оновлення UI: 60fps навіть при швидких оновленнях транскрипції
- Використана пам’ять: приблизно 15 МБ для всього застосунку
- Розмір бандла: близько 45КБ (gzipped, без урахування Vue)
Висновок: радість від Vue композаблі
Створення реальних застосунків для обробки мови в режимі реального часу за допомогою Vue-композаблі стало для мене відкриттям. Елегантна реактивність Composition API у поєднанні з типобезпекою TypeScript створюють досвід розробки, який важко перевершити.
Ключові висновки:
- Композаблі забезпечують чітке розділення понять — кожний композаблі відповідає за одну задачу
- Реактивність просто працює — ручні оновлення стану не потрібні
- Управління життєвим циклом є явним — onUnmounted робить очищення очевидним
- Обіцянкові патерни інтегруються з async/await
- Тестування просте — композаблі це просто функції
Якщо ви будуєте реальні часопрограми у Vue, використовуйте Composition API. Ваше майбутнє «я» (і ваша команда) подякують вам.
Ресурси
Документація Vue Composition API
WebSocket API — MDN
MediaRecorder API — MDN
Повні приклади коду
HI-FI News
via DEV Community: typescript https://ift.tt/LAExW9i
16 лютого 2026 о 18:30
February 16, 2026 at 06:30PM

Залишити відповідь