Увидев на хабрахабре крутые реализации программ в 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