IndexedDB — безлимитное хранение данных

в 12:07, , рубрики: chrome extension, Google Chrome, indexeddb, javascript, javascript library, метки: , ,

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js