Для тех, кто не в курсе, что такое IndexedDB и с чем его едят можно, почитать здесь.
А мы идем далее.
Безлимит
В конторе в которой я работаю появилась необходимость использования индексированной локальной базы данных на стороне клиента и выбор сразу пал на IndexedDB.
Но как всегда есть одно «НО», это самое «НО» — ограничение размера БД на машине пользователя в размере 5 МБ, что отнюдь нас не устраивало. Так как данная технология планировалась использоваться в админке нашего проекта и все юзеры использовали в качестве дефолтного браузера Google Chrome, то было принято решение поиска обхода того самого ограничение через расширение-прокси. Перелопатив много инфы мы пришли к выводу, что ограничение на размер БД можно убрать использовав специальные флаги в манифесте нашего расширения:
"permissions": [ "unlimitedStorage", "unlimited_storage" ],
Отправка сообщений сайт-расширение-сайт
Идем далее. С безлимитным хранением данных мы разобрались, но теперь возникла необходимость работать с той самой безлимитной БД непосредственно с самого сайта. Для этого использовалась отправка сообщений между сайтом и расширением (расширение выступило в роли прокси, между сайтом и безлимитной БД). Для этого в манифесте нашего расширения добавили следующие флаги:
"externally_connectable": { "matches": [ "*://localhost/*", "ЗДЕСЬ_ДОБАВЛЯЕМ_РАЗРЕШЕННЫЕ_ШАБЛОНЫ_URL " ] }
Выяснилось что валидными считаются URL вида: *://google.com/* and http://*.chromium.org/*, а, http:// * / *, * :/ / *. COM / не являются.
Больше информации о externally_connectable можете почитать здесь.
Идем далее.
Наступил этап написания того самого «моста» между сайтом и расширением для доступа к БД.
В качестве основной библиотеки для работы с IndexDB на стороне расширения была использована db.js, с которой вы можете ознакомиться тут.
Чтобы не изобретать велосипед, было принято решение использовать на стороне сайта синтаксис доступа который реализован в db.js.
Расширение
И так поехали, создаем background.js, который будем прослушивать входящие сообщения, и отвечать на них. Листинг кода привожу ниже:
var server; chrome.runtime.onMessageExternal.addListener( function (request, sender, sendResponse) { var cmd = request.cmd, params = request.params; try { switch (cmd) { case "open": db.open(params).done(function (s) { server = s; var exclude = "add close get query remove update".split(" "); var tables = new Array(); for(var table in server){ if(exclude.indexOf(table)==-1){ tables.push(table); } } sendResponse(tables); }); break; case "close": server.close(); sendResponse({}); break; case "get": server[request.table].get(params).done(sendResponse) break; case "add": server[request.table].add(params).done(sendResponse); break; case "update": server[request.table].update(params).done(sendResponse); break; case "remove": server[request.table].remove(params).done(sendResponse); break; case "execute": var tmp_server = server[request.table]; var query = tmp_server.query.apply(tmp_server, obj2arr(request.query)); var flt; for (var i = 0; i < request.filters.length; i++) { flt = request.filters[i]; if (flt.type == "filter") { flt.args = new Function("item", flt.args[0]); } query = query[flt.type].apply(query, obj2arr(flt.args)); } query.execute().done(sendResponse); break; } } catch (error) { if (error.name != "TypeError") { sendResponse({RUNTIME_ERROR: error}); } } return true; });
Но тут нас ждал сюрприз, а именно на выполнении участка кода:
flt.args = new Function("item", flt.args[0]);
получаем исключение:
Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self' chrome-extension-resource:".
.
Для разрешения данной проблемы добавим в манифест еще одну строку, которая разрешает выполнение пользовательского js на стороне расширения.
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
Также пришлось реализовать вспомогательную функцию перегона объекта в массив, для передачи его в качестве аргументов функции.
var obj2arr = function (obj) { if (typeof obj == 'object') { var tmp_args = new Array(); for (var k in obj) { tmp_args.push(obj[k]); } return tmp_args; } else { return [obj]; } }
Полный листинг manifest.json
{ "manifest_version": 2, "name": "exDB", "description": "This extension give proxy access to indexdb from page.", "version": "1.0", "background": { "page": "background.html" }, "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", "externally_connectable": { "matches": [ "*://localhost/*" ] }, "permissions": [ "unlimitedStorage", "unlimited_storage" ], "icons": { "16": "icons/icon_016.png", "48": "icons/icon_048.png" } }
Клиент
С расширением разобрались, теперь приступим к написанию клиент-библиотеки для работы с нашим прокси-расширением.
Первое, что необходимо, при отправке сообщения с клиента указать какому расширению мы хотим его послать, для этого, указываем его id:
chrome.runtime.sendMessage("ID_РАСШИРЕНИЯ", data, callback);
Полный листинг клиентской библиотеки:
(function (window, undefined) { "use strict"; function exDB() { var self = this; this.extensionId = arguments[0] || "knpcnhfbafbjadcbeipdihdblfogiafm"; this.filterList = new Array(); this._table; this._query; self.sendMessage = function sendMessage(data, callback) { chrome.runtime.sendMessage(self.extensionId, data, callback); }; self.open = function (params, callback) { self.sendMessage({"cmd": "open", "params": params}, function(r){ var tn; for(var i=0;i< r.length;i++) tn = r[i]; self.__defineGetter__(tn,function(){ self._table = tn; return this; }); callback(); }); return self; }; self.close = function (callback) { self.sendMessage({"cmd": "close", "params": {}}, callback); return self; } self.table = function (name) { self._table = name; return self; }; self.query = function () { self._query = arguments; return self; }; self.execute = function (callback) { self.sendMessage({"cmd": "execute", "table": self._table, "query": self._query, "filters": self.filterList}, function (result) { if (result && result.RUNTIME_ERROR) { console.error(result.RUNTIME_ERROR.message); result = null; } callback(result); }); self._query = null; self.filterList = []; }; "add update remove get".split(" ").forEach(function (fn) { self[fn] = function (item, callback) { self.sendMessage({"cmd": fn, "table": self._table, "params": item}, function (result) { if (result && result.RUNTIME_ERROR) { console.error(result.RUNTIME_ERROR.message); result = null; } callback(result); }); return self; } }); "all only lowerBound upperBound bound filter desc distinct keys count".split(" ").forEach(function (fn) { self[fn] = function () { self.filterList.push({type: fn, args: arguments}); return self; } }); } window.exDB = exDB; })(window, undefined);
На данном этапе наш комплекс для работы с безлимитной indexDB готов. Ниже приведу примеры использования.
Подключение
var db = new exDB(); db.open({ server: 'my-app', version: 1, schema: { people: { key: { keyPath: 'id', autoIncrement: true }, // Optionally add indexes indexes: { firstName: { }, answer: { unique: true } } } } }, function () {});
Добавление записи
db.table("people").add({ firstName: 'Aaron', lastName: 'Powell', answer: 142},function(r){ });
Обновление записи
db.table("people").update({ id:1, firstName: 'Aaron', lastName: 'Powell', answer: 1242}, function (r) {});
Удаление записи по ID
db.table("people").remove(1,function(key){});
Получение записи по ID
db.table("people3").get(111,function(r){ console.log(r); });
Выборки / Сортировки
db.people.query("firstName").only("Aaron2").execute(function(r){ console.log("GETTER",r); }); db.table("people").query("answer").all().desc().execute(function(r){ console.log("all",r); }); db.table("people").query("answer").only(12642).count().execute(function(r){ console.log("only",r); }); db.table("people").query("answer").bound(20,45).execute(function(r){ console.log("bound",r); }); db.table("people").query("answer").lowerBound(50).keys().execute(function(r){ console.log("lowerBound",r); }); db.table("people").query("answer").upperBound(43).execute(function(r){ console.log("upperBound",r); }); db.table("people").query("answer").filter("return item.answer==42 && item.firstName=='Aaron'").execute(function(r){ console.log("filter",r); });
Вывод
На сегодняшний момент данное решение активно используется на одном из наших проектов, буду благодарен за конструктивную критику и предложения. Так как этом моя первая статья на хабре прошу сильно не судить.
С исходниками Вы можете ознакомится на github.
ссылка на оригинал статьи http://habrahabr.ru/post/198666/
Добавить комментарий