Добрый день, уважаемое сообщество.
Для тех, кто не в курсе, что такое 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.
Автор: AndruSender