Перейти к содержанию

Экспорт собственных заметок в JSON

Оглавление серии

Настройки вынесены в постоянную страницу, а список заметок доступен через проверенную команду LIST. Добавим экспорт: пользователь получает файл reader-notes.json с собственными записями. Результат главы — понять путь от local до Blob и различать версию расширения с версией переносимого формата. Сервер, почтовое отправление и доступ к чужому хранилищу для этой операции не требуются.

Экспорт важен как контроль пользователя над данными. Он не является скрытой синхронизацией и не создаётся автоматически при каждом сохранении. Человек нажимает отдельную кнопку на options. Браузер получает файл из уже прочитанных собственных записей, а исходный local не очищается. Поэтому сообщение «Экспорт подготовлен» отличается от «Данные перенесены и удалены»: второй результат здесь вообще не выполняется. Нельзя добавлять очистку рядом с экспортом без отдельного явного договора.

Документ с собственным форматом

LIST возвращает проверенные сохранённые заметки. Options собирает объект с format="reader-notes", version=1 и массивом notes. Format отличает наш документ от произвольного JSON, а version позволит будущему импорту определить правила. Это число не совпадает по назначению с manifest.version. Пакет может получить новый интерфейс и остаться совместимым с форматом 1; новый формат данных может потребовать отдельного изменения даже без большого визуального обновления расширения.

В notes находятся id, createdAt, title, url, quote и note. Настройка defaultNote в этот экспорт не включается, потому что задача связана с переносом записей, а не всего окружения пользователя. Токенов и секретов в модели нет. При этом сам пользователь может написать личную информацию в тексте, поэтому файл остаётся его данными. Наш интерфейс не обещает, что любое содержимое автоматически стало безопасным для публичной публикации только после сериализации.

JSON.stringify с отступом в два пробела делает документ удобным для чтения. Это выбор представления, не изменение предметных значений. Поле заметки с переводами строк будет корректно экранировано JSON. Не формируем файл ручным объединением кавычек и пользовательского текста: так легко получить неверный документ или иной смысл. Отдельная стандартная сериализация даёт точный объектный договор, который следующий импорт сможет разобрать обратно.

Полные файлы шага

Показанные файлы полностью совпадают с выбранным состоянием архива. Остальные согласованные файлы находятся в examples/browser-extension-lab/lesson-08/.

options.js

const status = document.querySelector("#status");
const defaultInput = document.querySelector("#default");

async function request(type, payload) {
  const reply = await chrome.runtime.sendMessage({ version: 1, type, payload });
  if (!reply?.ok) throw new Error(reply?.error ?? "Нет ответа");
  return reply.data;
}

request("GET_SETTINGS").then((settings) => {
  defaultInput.value = settings.defaultNote;
  
}).catch((error) => { status.textContent = error.message; });

document.querySelector("#apply").addEventListener("click", async () => {
  try {
    await request("SET_SETTINGS", { defaultNote: defaultInput.value,
      
    });
    status.textContent = "Настройки сохранены";
  } catch (error) { status.textContent = error.message; }
});


document.querySelector("#export").addEventListener("click", async () => {
  try {
    const notes = await request("LIST");
    const body = JSON.stringify({ format: "reader-notes", version: 1, notes }, null, 2);
    const url = URL.createObjectURL(new Blob([body], { type: "application/json" }));
    const link = document.createElement("a");
    link.href = url;
    link.download = "reader-notes.json";
    document.body.append(link);
    link.click();
    link.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
    status.textContent = "Файл экспорта подготовлен браузером";
  } catch (error) { status.textContent = error.message; }
});

Ожидаемый результат

Это ожидаемый договор для самостоятельного исследования; расширение не устанавливалось и не запускалось.

LIST → {format:"reader-notes",version:1,notes:[…]} в JSON-файле.
Экспорт не очищает local.
Интерфейс подтверждает подготовку файла, не факт его записи на диск.

Blob и временный URL

Blob представляет последовательность данных с типом application/json. Он создаётся в DOM-контексте options, где доступны браузерные инструменты работы со ссылкой. Затем URL.createObjectURL выдаёт временный адрес этого Blob. Это не постоянный сетевой URL и не загрузка файла на сайт. Он служит текущей странице для предоставления содержимого браузеру. Service worker не строит HTML и не вызывает document.createElement, поэтому операция правильно находится в options.js.

Создаём элемент a, назначаем href временного адреса и download="reader-notes.json". После добавления в документ вызываем click, удаляем ссылку и позднее revokeObjectURL. Последний шаг освобождает ресурс временного адреса. В учебном коде задержка выбрана краткой, чтобы дать браузеру начать действие. Это не гарантия завершения физической записи файла за одну секунду. Пакет не получает сигнал, что человек действительно сохранил файл в конкретной папке.

Именно поэтому текст состояния говорит, что файл подготовлен браузером. Он не утверждает «Резервная копия существует на диске» без наблюдения. Пользователь может отменить диалог или изменить место сохранения. Такой предел важен даже в небольшом примере: приложение знает, какой документ создало и какое действие предложило браузеру, но не должно заявлять больше. Автор экспорт не выполнял, поэтому ни фактический файл пользователя, ни конкретная папка в архив не входят.

Снимок коллекции в момент чтения

Экспорт отражает результат LIST. Если после этого другая операция сохранит новую заметку, она не обязана оказаться в уже сформированном Blob. Это обычная граница снимка: данные прочитаны, затем сериализованы. Для локального инструмента с явной кнопкой такой договор достаточен. Не обещаем постоянную резервную копию, которая сама обновляется. Для неё понадобились бы собственное событие, политика хранения и объяснение взаимодействия нескольких версий.

Пустой список также является допустимым экспортом: notes содержит пустой массив. Это не ошибка отсутствия файла. Такой документ может пригодиться для проверки формата или переноса пустого состояния. Когда коллекция непуста, идентификаторы сохраняются, чтобы повторный импорт мог отличать ту же заметку от новой записи. Мы не генерируем новый UUID при экспорте, поскольку тогда каждая резервная копия выглядела бы как другой набор объектов.

Ожидаемый сценарий: пользователь создаёт две свои заметки, нажимает экспорт и получает документ формата reader-notes версии 1. После нажатия список в расширении остаётся из двух записей. Если содержимое note содержит