Введение
Мы пишем онлайн-компилятор для отладки ИИ-сервисов. И статья — это скорее приглашение к обсуждению. Хочу поделиться практическим подходом, который показался простым и удобным в этом проекте. Ваши комментарии и критика будут очень полезны.
Компилятор выглядит достаточно привычно: в левой части экрана находится редактор с вкладками для файлов, а справа — поля для ввода и вывода данных от ИИ-сервисов. Пользователи могут создавать, загружать, скачивать, переименовывать и удалять файлы. Файлы также должны кэшироваться в браузере.
Структура файла в редакторе (псевдокод)
export default class UserFile { private id: string; private name: string; private fullName: string; private extension: TUserFileExtension; private downloadedState: TUserFileIsDownloaded; private content: TUserFileContent; private cachedState: TuserFileIsCached; // Методы сериализации public static fromSerializable(data: IUserFileSerializable): UserFile {} public toSerializable(): IUserFileSerializable {} // Методы для изменения и получения значений атрибутов файла. public setId(id: string): void {} public setName(name: string): void {} public setExtension(extension: TUserFileExtension): void {} public setDownloadedState(state: TUserFileIsDownloaded): void {} public setFullName(fullName: string): void {} public setContent(content: TUserFileContent): void {} public setCachedState(state: TUserFileIsCached): void {} // Методы для получения значений атрибутов файла. public getId(): string {} public getName(): string {} public getExtension() {} public getDownloadedState() {} public getFullName() {} public getContent() {} public getCachedState() {} // Вспомогательные методы для обработки имени файла и его расширения. private createFullName(name: string, extension: TUserFileExtension): string {} // Извлекает имя файла из полного имени. private getNameFromFullName(fullName: string): string {} // Извлекает расширение файла из полного имени. private getExtensionFromFullName(fullName: string): TUserFileExtension {} }
Проблема
Изначально я отправлял в Redux экземляры класса UserFile
и ни о чем не парился. Все работало замечательно. И все продолжало работать без сериализации. Мазолило глаз только куча ошибок A non-serializable value
в консоли браузера.
В доке Redux описано, что мы можем и не сериализовывать данные, если нас не смущает регидратация и time-travel debugging. Почему же тогда при нарушении вылетает не Warning
, а целая ошибка?
Тут стоит капнуть в философию Redux. И понять, что всё же нас должна смущать невозможность регидратации при отправке файлов на endpoint
.
Основная цель Redux
Сделать состояние приложения предсказуемым и легко управляемым. В этом контексте сериализация состояния — это не просто техническое требование, а ключевая часть философии Redux. И вот почему Redux так строго относится к сериализуемости данных:
-
Time-travel debugging: несериализуемые объекты (например, экземпляры классов) могут не воспроизводить состояние корректно при «перемотке», так как могут не сохранять методы и прототипы, нарушая предсказуемость.
-
Регидратация и серверный рендеринг (SSR): состояние Redux часто сохраняется для последующего восстановления — например, между сессиями или при серверном рендеринге.
То есть в нашем случае, отправленные файлы на endpoint
, могут привести к ошибке.
Если состояние содержит несериализуемые (объект с примитивными типами) объекты, его сложно или невозможно восстановить корректно.
То есть риск потерять функции класса UserFile
достаточно велик.
Отключение проверки сериализации
Вы можете, конечно, все же проигнорировать требования и отключить проверку, добавив middleware:
import { configureStore } from '@reduxjs/toolkit'; const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false, // отключаем проверку сериализуемости }), });
Решение кейса
В нашем кейсе я все-таки сериализовал данные. И выглядит это следующим образом:
У нас есть класс UserFile
, отвечающий за файл и его контент (описан в начале статьи). И есть класс FilesService
, отвечающий за работу со множеством файлов, а также, архивирование и разархивирование файлов. Ниже приведены методы классов и их использование в компонентах и Redux.
Интерфейс сериализованного файла
export interface IUserFileSerializable { id: string; name: string; fullName: string; extension: TUserFileExtension; // string downloadedState: TUserFileIsDownloaded; // boolean content: TUserFileContent; // string cachedState: TUserFileIsCached; // boolean }
Класс UserFile
// Метод для создания объекта UserFile из сериализованных данных, // используемых для хранения в Redux. public static fromSerializable(data: IUserFileSerializable): UserFile { return new UserFile( data.id, data.name, data.extension, data.downloadedState, data.content, data.cachedState ); } // Метод для преобразования UserFile в формат, который можно // сохранить в Redux. Возвращает объект интерфейса IUserFileSerializable. public toSerializable(): IUserFileSerializable { return { id: this.id, name: this.name, fullName: this.fullName, extension: this.extension, downloadedState: this.downloadedState, content: this.content, cachedState: this.cachedState, }; }
Класс FilesService
// Сериализация public static toSerializableFiles(files: TUserFiles): IUserFileSerializable[] { return files.map(file => file.toSerializable()); } // Десериализация public static fromSerializableFiles(serializedFiles: IUserFileSerializable[]): TUserFiles { return serializedFiles.map(fileData => UserFile.fromSerializable(fileData)); }
Слайс файлов в Redux
const initialState: IUserFilesSlice = { files: files, currentFileId: initialCurrentFileKey, }; export const filesSlice: Slice<IUserFilesSlice> = createSlice({ name: "projectFiles", initialState, reducers: { setCurrentFileId: (state, action: PayloadAction<string>) => { // Устанавливает текущий идентификатор файла. state.currentFileId = action.payload; }, updateFile: (state, action: PayloadAction<IUserFileSerializable>) => { if (!state.files) { return; } // Обновляет данные файла в массиве файлов по его идентификатору. const index = state.files.findIndex((file: IUserFileSerializable) => file.id === action.payload.id); if (index !== -1) { state.files[index] = action.payload; } }, addFile: (state, action: PayloadAction<IUserFileSerializable>) => { // Добавляет новый файл в массив файлов. state.files.push(action.payload); }, removeFile: (state, action: PayloadAction<string>) => { if (!state.files) { return; } // Удаляет файл из массива по его идентификатору. state.files = state.files.filter((file: IUserFileSerializable) => file.id !== action.payload); }, replaceFiles: (state, action: PayloadAction<IUserFileSerializable[]>) => { // Заменяет весь массив файлов новыми данными. state.files = action.payload; }, deleteAllFiles: (state) => { // Удаляет все файлы из состояния. state.files = []; }, }, });
Использование файлов в компонентах
const serializedFiles = useSelector( (state: TRootState) => state.projectFiles.files ); // Преобразуем сериализованные файлы обратно в объекты UserFile. const [files, setFiles] = useState<TUserFiles>( FilesService.fromSerializableFiles(serializedFiles) );
Мутация файлов из компонентов
const addFilesToWorkspace = (files: TUserFiles): void => { const serializableFiles: IUserFileSerializable[] = FilesService.toSerializableFiles(files); // Отправляем сериализованные файлы в Redux dispatch(replaceFiles(serializableFiles)); };
Заключение
В этом подходе сериализация и десериализация позволили сохранить данные в Redux в подходящем формате, не жертвуя гибкостью и возможностью работы с полноценными объектами класса. Если вам знакомы подобные ситуации, делитесь опытом в комментариях — буду рад обсудить альтернативные решения!
ссылка на оригинал статьи https://habr.com/ru/articles/857430/
Добавить комментарий