Pull to refresh

Надёжный localStorage для букмарклетов

Reading time 5 min
Views 12K
В отличие от расширений, букмарклеты хороши простотой и кроссбраузерностью. Конечно, они ограничены контекстом окна (содержимого страницы), но часто этого достаточно. А с возникновением механизма localStorage у них появился простой способ сохранять и запрашивать данные на стороне клиента.

Например, я пользуюсь таким простейшим букмарклетом, чтобы сохранять промежуточные «закладки» на места в длинных статьях, которые не получается прочесть за один присест.

javascript:(function(d, scrT){
	scrT = d.documentElement.scrollTop || d.body.scrollTop;
	if (scrT) {
		localStorage['bmk_' + d.location.href] = scrT;
	}
	else {
		scrollTo(0, localStorage['bmk_' + d.location.href] || 0);
	}
})(document)

Один и тот же букмарклет работает одновременно и для сохранения места чтения, и для перехода к нему. Если страница только что загружена или находится в начальном состоянии прокрутки (скажем, была нажата клавиша Home), букмарклет предполагает, что нам нужно перейти в прежде сохранённое место, пытается извлечь его из localStorage и прокрутить до него страницу. Если же страница уже прокручена, букмарклет сохраняет текущее состояние прокрутки.

Если не меняется размер и разрешение монитора, размер окна браузера и размер его основной части (за вычетом боковой панели и панелей инструментов), не меняется масштаб страницы и её основная структура, — программа работает, как часы. То есть в большинстве случаев на её простоту можно положиться.

Однако сайты по-разному обращаются со своим localStorage. Некоторые вообще его не используют. Некоторые используют активно, но изменяют или удаляют только те ключи, которые сами создают (например, Twitter). Но есть и абсолютные монархи: скажем, Facebook периодически полностью очищает свой localStorage, вызывая, скорее всего, метод localStorage.clear(). Упрекнуть последний тип сайтов, конечно, не в чем — они полновластные хозяева в этой области и не обязаны считаться с тем, что кто-то посторонний может захотеть воспользоваться их собственностью.

Как уберечься в таких случаях? Конечно, можно перейти на IndexedDB (если сайт её, опять-таки, не очищает) или написать расширение (Google Chrome выделяет расширениям свой localStorage, Firefox этого не разрешает, но обеспечивает свой способ хранения данных в общей базе настроек) — вот только для таких простых случаев пользоваться такими сложными инструментами не очень сподручно.

К сожалению, не получится воспользоваться и localStorage, принадлежащим внутренним фреймам страницы, если такие обнаружатся: этого не позволит нам сделать принцип одинакового источника.

Однако выход есть — на первый взгляд неуклюжий, но на практике достаточно простой и удобный, если один раз всё настроить и запастись шаблонами на все случаи жизни.

Поможет нам технология Window.postMessage. Нам всего лишь нужно создать крохотную веб-страничку, вот такую:

<!doctype html>
<html>
	<head><meta charset="UTF-8"><title></title>
		<script>
			function processMessage(event) {
				if (event.data.action == 'get') {
					event.source.postMessage(localStorage[event.data.key], event.origin);
				}
				else {
					localStorage[event.data.key] = event.data.value;
				}
			}
			window.addEventListener('message', processMessage, false);
		</script>
	</head>
	<body></body>
</html>

Затем поместить её на любой хостинг, который поддерживает протоколы http: и https: — например, на GitHub Pages, которые доступны всем зарегистрированным пользователям. После этого мы можем встраивать эту страничку как внутренний фрейм в любой документ, налаживать с ней общение из окна основного документа, в контексте которого работает букмарклет, и пользоваться ею как прокси-сервером между букмарклетом и localStorage этой самой странички, то есть почти как маленьким сервером базы данных.

К сожалению, встроить для этих нужд локальную страницу не получится по ограничениям безопасности, но загрузка такой маленькой страницы происходит совершенно незаметно, даже если она не кешируется.

Остаётся переделать наш букмарклет под новый метод. Размер, конечно, вырастет с девяти строчек до почти сорока (при читаемом форматировании), но всё равно код останется довольно простым. Работа же с букмарклетом для пользователя совсем не изменится.

javascript:(function(d, mySt, scrT){
	scrT = d.documentElement.scrollTop || d.body.scrollTop;
	mySt.origin = d.location.protocol + '//user.github.io';
	mySt.URL = mySt.origin + '/storage/storage.html';
	mySt.iframe = d.querySelector('iframe#myStorageIframe');
	if (mySt.iframe) {
		mySt.iframe.contentWindow.postMessage(
			scrT ?
			{'action': 'set', 'key': 'bmk_' + d.location.href, 'value': scrT}
			:
			{'action': 'get', 'key': 'bmk_' + d.location.href},
			mySt.origin
		);
	}
	else {
		function processMessage(event) {
			if (event.origin == mySt.origin) {
				scrollTo(0, event.data || 0);
			}
		}
		addEventListener('message', processMessage, false);
		mySt.iframe = d.body.appendChild(d.createElement('iframe'));
		mySt.iframe.style.display = 'none';
		mySt.iframe.id = 'myStorageIframe';
		mySt.iframe.src = mySt.URL;
		mySt.iframe.addEventListener('load', function() {
			mySt.iframe.contentWindow.postMessage(
				scrT ?
				{'action': 'set', 'key': 'bmk_' + d.location.href, 'value': scrT}
				:
				{'action': 'get', 'key': 'bmk_' + d.location.href},
				mySt.origin
			);
		});
	}
})(document, {})

Главный принцип остаётся тем же: букмарклет анализирует состояние прокрутки, в начальном её положении извлекает закладку и переходит по ней, в промежуточном положении прокрутки сохраняет состояние.

Изменения же заключаются в следующем.

Сперва букмарклет создаёт служебный объект mySt (myStorage) с тремя свойствами: origin — для обеспечения интерфейса необходимой информацией об источнике адресата, URL (на основании предыдущего свойства) — для задания фрейму полного адреса, iframe — для ссылки на сам фрейм. Так как страницы с протоколом https: не позволят встроить в себя страницу с обычным протоколом, на первом же этапе мы определяем текущий протокол и в зависимости от него формируем свойства origin и URL (в примере дан недействующий условный URL, «user» нужно будет заменить на реальное имя пользователя, если страничка будет размещена именно на этом сайте).

Потом букмарклет проверяет, встроена ли уже наша страничка-посредник (если мы читаем эту статью уже какое-то время и сохраняли закладки раньше, не закрывая окно, — на всякий случай или временно отлучаясь, — вполне возможно, что фрейм уже встроен и нам не нужно повторять вставку). Если страничка встроена, мы сразу переходим к общению: или отправляем ключ и значение для сохранения, или запрашиваем значение ранее сохранённого ключа. Поскольку общение происходит асинхронно, больше нам делать нечего — даже если мы запросили значение, ответ подхватит обработчик, который мы повесили раньше, когда встраивали фрейм (о нём см. дальше).

Если фрейма нет, тогда мы совершаем следующие шаги.

1. Впрок вешаем на основной документ обработчик ответов нашего будущего окошка-посредника.
2. Встраиваем само окошко и скрываем его, чтобы не мешало.
3. Вешаем на окошко-посредник одноразовый обработчик загрузки, чтобы начать общение, когда наш прокси будет готов. В этом обработчике мы произведём те же действия, что и в прежде описанном случае, при обнаружении готовой странички-посредника.

Код ни в коем случае не претендует на эталонность, библиотечную универсальность и чистоту, я ведь не профессиональный программист. Так что любые поправки и предостережения будут восприняты с благодарностью.

Пока же работа букмарклета успешно проверена на последних версиях Firefox (Nightly), Google Chrome (Canary), Internet Explorer (11).
Tags:
Hubs:
+17
Comments 2
Comments Comments 2

Articles