Задача
Пару лет назад начал разрабатывать редактор текстовых квестов на JavaScript и обратил внимание на то, что неплохо было бы добавить в JSON-сериализатор поддержку ссылок на объекты. Чтобы можно было одним методом сохранить и загрузить состояние объекта, не нарушая его целостность и связь с внешним миром. Что-то подобное есть в PHP при работе метода serialize.
Спустя год начал разрабатывать пошаговую стратегию, в которой такой метод был бы идеальным для реализации сохранений и сетевого режима (пересылка сохранений от игрока к игроку, как это реализовано в Heroes of Might&Magic 3). Имея такой метод, можно было бы не заботиться о сохранении/загрузке объектов игрового мира при их изменениях. Например, добавим лучнику привязку его стрел к конкретному типу дерева. Или в морском пароме создадим массив перевозимых юнитов. При обычной тактике обработки данных это создало бы немало проблем для организации сохранения ссылок.
В итоге, кроме банальной организации внутренних ссылок, идея разрослась амбициозными планами, а именно:
-
Сохранять цепочку прототипов объекта со всеми их значениями;
-
Организовать связь с внешними статичными объектами (чтобы не тянуть их в сериализацию);
-
Сохранять методы объекта;
-
Сделать так, чтобы один объект, размещённый в нескольких местах, так и оставался одним объектом.
Этими идеями данный сериализатор отличается от имеющихся аналогов.
Решение
В итоге была разработана следующая утилита
https://github.com/nerd220/JSONext
Она содержит два метода — toLinkedJSON и fromLinkedJSON для запаковки и распаковки объекта в JSON.
fromLinkedJSON производит распаковку объекта из сериализации.
toLinkedJSON принимает три параметра:
-
Объект. Если нужно вставить не объект, необходимо обернуть его в объект, например {myData: array1, elseData: text1}.
-
Массив ссылок на внешние объекты и способ их восстановления, в формате
[ [объект, ‘способ восстановления’], [объект2, ‘способ 2’]… ]Пример:
[ [document.body, ‘document.body’], [canvas1, ‘document.getElementById(«canvas1»)’] ]Если в исходном объекте будет найдена ссылка на document.body, она будет заменена скриптом, который при распаковке объекта присоединит ему ссылку на document.body.
-
Массив конструкторов (прототипов), объекты которых нужно пересоздать при распаковке.
Пример: [’employers’, ‘workers’]
Примеры
Простой пример:
var a={x: 1}; var b={y: 2, z: a}; var c={a: a, b: b}; var json=toLinkedJSON(c); var x=fromLinkedJSON(json); console.log(x.a == x.b.z); // true, потому что все ссылки были сохранены
Более сложный пример:
// создаём функции конструкторы function constructAProto(){ this.i=1; this.protoMethod=function(){ this.i+=2; } } function constructA(){ // странный, но рабочий метод наследования this.__proto__=new constructAProto(); this.constructor=constructA; // нужно указать конструктор объекта // данные this.body=document.body; } function constructB(link){ this.someMethod=function(){ this.l.i++; } this.l=link; } // создаём объекты и заполняем их данные var a=new constructA(); var b=new constructB(a); var c={linkA: a, linkB: b, method: (o)=>console.log(o.l.i)}; var o={a:a, b:b, c:c}; // сериализуем объект var json=toLinkedJSON(o,[[document.body,'document.body']],['constructA','constructB']); // десериализуем объект var x=fromLinkedJSON(json); // тестируем результат x.b.someMethod(); // проверяем метод объекта B, увеличиваем i у связанной с ним A x.a.protoMethod(); // вызываем метод прототипа объекта А, прибавляем к А ещё 2 x.c.method(x.b); // используя сериализованный метод объекта C, выводим A = 4 console.log(x.c.linkA === x.b.l); // true console.log(x.a.body); // вернёт наш текущий document.body
Этот пример демонстрирует бесшовный перенос сложного связанного объекта в строку и обратно.
Принцип работы
При сохранении всё довольно просто — метод рекурсивно обходит объект, собирая информацию об его содержимом (ссылки на внутренние и внешние объекты, принадлежность объектов к тем или иным прототипам), затем записывает в JSON формат с собственными добавлениями.
При этом исходный объект меняется. В связанные с ним объекты добавляются идентификаторы, которые удаляются после завершения работы метода.
При распаковке метод делает три вещи:
-
Восстанавливает ссылки на внутренние объекты, для этого осуществляется поиск точек монтирования внутреннего объекта;
-
Восстанавливает ссылки на внешние объекты, используя указанные пользователем директивы;
-
Пересоздаёт объекты так, чтобы они вновь принадлежали нужному прототипу, при этом вызывается функция-конструктор, в которую подбираются входные параметры исходя из данных объекта. После создания объекта в него загружаются сохранённые данные, а также восстанавливаются ссылки на него во всех точках монтирования внутри основного объекта;
-
Распаковывает методы объектов, не принадлежащих указанным прототипам.
Минусы подхода
-
Метод может нагружать процессор. Это связано с необходимостью переобхода объекта в поиске точек монтирования. Было бы значительно проще, если бы JavaScript позволял осуществлять доступ к внутреннему ID объекта;
-
Использование eval для пересоднания объектов гипотетически небезопасно (например, если пользователь имеет доступ к сериализации);
-
Необходимо использовать специфический способ наследования объектов (как в указанном примере);
-
Если какие-то внешние объекты ссылались на внутренние объекты нашей сериализации, эти связи необходимо обновлять самостоятельно.
Заключение
Данный метод позволит разработчикам экономить время на разработке алгоритмов сохранения и загрузки данных. Фактически, можно сохранить всё приложение как есть, а затем развернуть его из строки без потери связности и функциональности.
Метод создаёт компактную сериализацию за счёт игнорирования содержимого указанных прототипов, однако восстанавливает значения не только самого объекта, но и всех его прототипов (как в указанном выше примере с значением i).
В данный момент метод успешно работает для сохранений в сетевых играх до 10мб (сектора поля, юниты, события и т.д.)
ссылка на оригинал статьи https://habr.com/ru/articles/896606/
Добавить комментарий