Крошечный контейнер для мини-приложений в 30 строк на чистом JS

в 7:54, , рубрики: 30 строк, javascript, localStorage, ненормальное программирование, метки: , ,

Увидев на хабрахабре крутые реализации программ в 30, 24, 19 и даже 1 строчку, тоже решил поморать чистые страницы хабра чем-нибудь этаким. Тем более, что давно хочется инвайт выпала возможность поработать в новом для себя качестве.

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

Вообще в самом начале я честно погуглил на предмет наличия разных фреймворков для расширений. Нашел, например, Kango. Но мне не нужна была кроссбраузерность (расширение писалось только для Chrome), и не было желания заморачиваться изучением сторонних библиотек ради небольшого приложения, поэтому было решено написать свой велосипед.

Задачи, которые ставились перед контейнером:

  • Удобная работа с localStorage. Получаем и сохраняем любые данные, в том числе объекты и массивы.
  • Какая-нибудь валидация параметров (хотя бы в случае, если такого параметра в localStorage просто нет).
  • Возможность фасовать параметры по группам — плохо когда все свалено в одну кучу.
  • Подключение функциональности с разносом по отдельным компонентам, при этом вероятно будет необходима преинициализация некоторых параметров.

В принципе все удалось реализовать в небольшом контейнере, и для небольших приложений он меня устраивает на 100%.

Итак, у нас есть объект app, и он содержит всего 3 метода: addStorage, addValidator и addModule.

addStorage

Любое расширение должно где-то хранить настройки пользователя. ОК, напишем

app.addStorage('option');

После этого у нас появляется новый метод app.option(). Этот метод служит для сохранения и извлечения из localStorage параметров из группы option (смотрим хотелку #2: отделяем мух от котлет, эта группа только для пользовательских настроек, для других параметров заведем другой метод). app.option() принимает 2 аргумента: название параметра и его значение. 2-ой аргумент необязателен. Если он не указан, то метод вернет значение параметра, если указан — сохранит параметр с таким значением. app.option под капотом использует для сериализации JSON.stringify, а для извлечения данных — JSON.parse.

app.option('foo', 'bar'); // localStorage['option.foo'] = 'bar'
var a = app.option('foo'); // a = 'bar'

// можно сохранять массивы и объекты
app.option('foo', [1,2,3]);
var a = app.option('foo'); // a = [1,2,3]

app.option('foo', { bar: [1,2,3] });
var a = app.option('foo'); // a =  { bar: [1,2,3] }

Естесственно, одними лишь настройками пользователя addStorage не ограничивается, там мы можем хранить любые данные, которые необходимо помещать в localStorage:

app.addStorage('whatever_you_want');
app.whatever_you_want('foo', 'bar');
addValidator

Этот метод добавляет правило валидации для конкретного параметра. Принимает 3 аргумента: название группы параметров, название параметра и собственно валидирующую функцию. Эта функция принимает как аргумент значение этого параметра и должна возвращать нужное значение. Каждый раз при вызове app.option('foo') вызывается валидатор для foo, если он задан. Например:

var a = localStorage['option.foo']; // a = undefined
var a = app.option('foo'); // a = undefined

app.addValidator('option', 'foo', function(foo) {
    return typeof foo === 'undefined' ? 'bar' : foo;
});

var a = localStorage['option.foo']; // a = undefined
var a = app.option('foo'); // a = 'bar'

Это очень удобно в расширениях при первом запуске, так как никаких данных у нас еще нет.

addModule

addModule — это самое интересное. Он позволяет добавить в контейнер целый модуль. Принимает 2 аргумента: название и объект, содержащий методы или свойства.

app.addModule('foo', {
    bar: function() {
        // do something...
    }
});

// Теперь можно вызвать метод bar
app.foo.bar();

Естесственно, если бы все было так просто, смысла бы в этом не было. Добавляем немного магии. Во-первых, объект модуля может содержать метод init(). Тогда этот метод будет вызван при добавлении модуля (типа как конструктор). В нем прописываем любую инициализирующую логику для модуля. Во-вторых, модуль foo сам становится мини-копией контейнера app. Т.е. в нем самом становятся доступны функции addStorage, addValidator и даже addModule! Чтобы лучше продемонстрировать работу такого подхода, давайте попробуем реализовать модуль для локализации приложения.

// вся функциональность модуля состоит только из одной функции trans() - перевод фразы
app.addModule('i18n', {
    // здесь будем хранить массив переводов
    data: {},

    // настроим модуль
    init: function() {
        // где-то надо хранить локаль пользователя, заведем группу параметров option для модуля i18n, (!) эта не та же самая группа, которую мы использовали ранее
        this.addStorage('option');

        // тут же валидатор, т.к. при первом запуске локаль не задана
        this.addValidator('option', 'locale', function(locale) {
            return locale === 'undefined' ? 'ru' : locale;
        });

        document.write('<script src="/path/to/i18n/' + this.option('locale') + '.js"></script>');
        this.data = data_from_file;

        $(document).ready(function() {
            $('.i18n').each(function() {
                $(this).text(this.trans($(this).attr('data-i18n')));
            });
        });
    },
    
    trans: function(field) {
        return typeof this.data[field] === 'undefined' ? field : this.data[field];
    }
});

Немного подробнее о this.addStorage('option');. Здесь мы создали группу параметров для настроек пользователя в модуле i18n. Она не совпадает с той, которой мы пользовались ранее, так создана в контексте объекта i18n. В приложении доступ к этой группе осуществляется через app.i18n.option('foo');. Все это опять же сделано для хотелки #2: разделяем пользовательские настройки по модулям. Так лучше понятно что к чему относится.

Итак, что мы получили в итоге: четкую, ясную структуру кода, ничего не болтается в глобальной области видимости; возможность разбить логику приложения по разным компонентам; беспроблемную работу с localStorage — что положили, то и получили; валидация данных — один раз написали и забыли; ну и маленький исходный код контейнера — всего 30 строк :)

Disclaimer

Ни на какую 100% нужность и актуальность данного контейнера не претендую. Любая конструктивная логика приветствуется. Надеюсь, кто-то найдет интересную информацию в этом посте.

Полный текст кода контейнера

var app = {
    validator: {},
    addStorage: function(name) {
        this[name] = function(key, value) {
            var local_storage_name = typeof this.name === 'undefined' ? name : this.name + '.' + name;
            if (typeof value === 'undefined') {
                var param = typeof localStorage[local_storage_name + '.' + key] !== 'undefined' ? JSON.parse(localStorage[local_storage_name + '.' + key]) : null;
                if (typeof this.validator[name + '.' + key] !== 'undefined') {
                    param = this.validator[name + '.' + key](param);
                }
                return param;
            }
            localStorage[local_storage_name + '.' + key] = JSON.stringify(value);
        };
    },
    addValidator: function(storage, name, callback) {
        this.validator[storage + '.' + name] = callback;
    },
    addModule: function(name, object) {
        object.name = name;
        object.validator = {};
        object.addStorage = this.addStorage;
        object.addValidator = this.addValidator;
        object.addModule = this.addModule;
        this[name] = object;
        if (typeof this[name].init !== 'undefined') {
            this[name].init();
        }
    }
};

Автор: Blumfontein

Источник

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


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