
На фото: Том Круз в фильме Лучший Стрелок
В этой статье мы рассмотрим взаимодействие Single Page HTML Application с облачной MongoDB через JavaScript. В качестве MongoDB-as-a-Service я возьму Mongolab. Стоимость развернутой MongoDB, с объёмом в 500мб, обойдется нам всего-лишь в 0 USD.
Для того, чтобы создать todo-лист, нам не потребуется бекенд. Взаимодействовать с Mongolab мы будем через REST API, а обертку для него в клиентской части мы напишем не прибегая к помощи сторонних JavaScript-фреймворков.
Навигация по статье
1. Регистрация на Mongolab и получение API-ключа
2. Безопасность данных при общении браузера с MongoDB
3. Область применения подобных решений
4. Давайте уже к делу
5. Разбираем код приложения
6. Демо готового проекта
1. Регистрация на Mongolab и получение API-ключа
Шаг первый — регистрируемся

Регистрация простая и не требует привязки карт оплаты. Mongolab — довольно полезный сервис. В нашей компании мы используем его в качестве песочницы во время разработки веб-приложений.
Шаг второй — заходим в меню пользователя

Справа на экране будет ссылка в пользовательское меню. В этом меню нас и будет ждать наш заветный API-key.
Шаг третий — забираем API-ключ

После получения API-ключа мы можем работать с Mongolab REST API
2. Безопасность данных при общении браузера с MongoDB

На фото: Том Круз смеётся
Хочу предупредить — статья носит чисто учебный характер. Коммуникация с облачной базой данных из браузера может оказаться фатальной ошибкой. Думаю очевидно, что злоумышленник может легко получить доступ к базе просто открыв консоль разработчика. Использование read-only пользователя базы решает эту проблему только в том случае, если, абсолютно все данные находящиеся в облачной MongoDB — не несут никакой важности и приватности.
3. Область применения подобных решений
Основываясь на таком подходе мы с вами можем создать todo-list application, который можно будет держать у себя на компьютере, написать приложение под Android/iOS/Windows Phone/Windows 8.1 используя всего лишь один html и javascript.
4. Давайте уже к делу
На написание todo приложения у меня ушло ровно 15 минут, на написание этой статьи (+ комментирование кода) я потратил два часа. Цветовая схема была взята у Google, которую заботливо вынес в LESS один добрый человек. То, что у меня получилось, я залил на github чтобы вы смогли оценить работу с облачной базой не растрачивая своё драгоценное время. Ссылку вы найдёте в конце статьи.
Коммуникацию с REST API будем осуществлять через XMLHttpRequest. Современный мир веб-разработки очень уверенно сфокусировался на решениях вроде jQuery или Angular — суют их везде и где попало. Зачастую обойтись можно спокойно и без них. Объект
new XMLHttpRequest () — своего рода поток, связанный с js-объектом, у которого есть основные методы open и send (открыть соединение и отправить данные) и основное событие onreadystatechange. Для общения с REST нам потребуется установить заголовок Content-Type:application/json;charset=UTF-8, для этого мы используем метод setRequestHeader.Вот так может выглядеть простое REST-приложение:
var api = new XMLHttpRequest(); api.onreadystatechange = function () { if (this.readyState != 4 || this.status != 200) return; console.log(this.responseText); }; // вместо onreadystatechange можно использовать onload api.open('GET', 'https://api.mongolab.com/api/1/databases?apiKey=XXX'); api.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); api.send();
А метод вам не завернуть?
var api = new XMLHttpRequest(); api.call = function (method, resource, data, callback) { this.onreadystatechange = function () { if (this.readyState != 4 || this.status != 200) return; return (callback instanceof Function) ? callback(JSON.parse(this.responseText)) : null; }; this.open(method, 'https://api.mongolab.com/api/1/' + resource + '?apiKey=XXX'); this.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); this.send(data ? JSON.stringify(data) : null); }; /** код ��иже выглядит намного удобнее и его можно вызывать несколько раз */ api.call('GET', 'databases', null, function (databases) { console.log(databases); });
Внести новую запись в коллекцию demo с title: test
var test = { title: 'test' }; api.call('POST', 'databases/mydb/demo', test, function (result) { test = result; // получить ID из базы после добавления });
Проблема синхронного потока
Наша переменная api является лишь одним потоком, поэтому следующий код ошибочен:
api.call('POST', 'databases/mydb/demo', test1); api.call('POST', 'databases/mydb/demo', test2);
Для того, чтобы обойти синхронность, нам потребуется два отдельных потока — для первого POST и для второго. Чтобы каждый раз не описывать метод call — мы приходим к решению собрать «псевдо-класс» MongoRESTRequest, который на самом деле являлся бы функцией, возвращающей новый объект XMLHttpRequest с готовым методом call:
var MongoRESTRequest = function () { var api = new XMLHttpRequest(); api.call = function (method, resource, data, callback) { this.onreadystatechange = function () { if (this.readyState != 4 || this.status != 200) return; return (callback instanceof Function) ? callback(JSON.parse(this.responseText)) : null; }; this.open(method, 'https://api.mongolab.com/api/1/' + resource + '?apiKey=XXX'); this.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); this.send(data ? JSON.stringify(data) : null); }; return api; }; var api1 = new MongoRESTRequest(); var api2 = new MongoRESTRequest(); api1.call('POST', 'databases/mydb/demo', test1); api2.call('POST', 'databases/mydb/demo', test2);
Теперь этот код будет исполнен корректно.
Продолжая модифицировать наш MongoRESTRequest мы придём приблизительно к тому варианту, который будет изложен в исходном коде приложения ниже.
Немного про то, как можно обойтись без шаблонизатора:
Обычно я наблюдаю в коде среднестатистического фаната jQuery нечто такое:
$('#myDiv').html('<div class="red"></div>');
А теперь взгляните, как это должно быть на самом деле, без подключения лишних 93.6кб (compressed, production jQuery 1.11.2)
var myDiv = document.getElementById('myDiv'); var newDiv = document.createElement('div'); // создать div newDiv.classList.add('red'); // добавить класс red myDiv.appendChild(newDiv); // вставить в myDiv
Ладно, ладно, конечно все мы знаем что это можно сделать и так:
document.getElementById('myDiv').innerHTML = '<div class="red"></div>';
Ещё немного про работу с DOM в Vanilla:
Используем map для создания списка (ReactJS-way):
var myList = document.getElementById('myList'); var items = ['первый', 'второй', 'третий']; items.map(function (item) { var itemElement = document.createElement('li'); itemElement.appendChild(document.createTextNode(item)); myList.appendChild(itemElement); });
На выходе имеем (ссылка на jsFiddle поиграться):
<ul id="myList"> <li>первый</li> <li>второй</li> <li>третий</li> </ul>
Преимуществом такой работы JavaScript является возможность полноценной работы с объектами:
var myList = document.getElementById('myList'); var items = [{id: 1, name: 'первый'}, {id: 2, name: 'второй'}, {id: 3, name: 'третий'}]; items.map(function (item) { var itemElement = document.createElement('li'); itemElement.appendChild(document.createTextNode(item.name)); itemElement.objectId = item.id; // присваиваем свойство objectId для каждого itemElement itemElement.onclick = function () { alert('item #' + this.objectId); }; myList.appendChild(itemElement); });
Ссылка на jsFiddle для проверки
5. Разбираем код приложения
Я постарался прокомментировать каждую строчку кода
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Список дел</title> <!-- подключаем стили: основной и Font-Awesome. --> <link rel="stylesheet" type="text/css" href="client/styles/main.css"> <link rel="stylesheet" type="text/css" href="client/styles/font-awesome.css"> <link rel="icon" type="image/png" href="favicon.png"> </head> <body> <!-- добавляем свойство tabindex для fake-header, для работы без помощи мыши. --> <div id="fake-header" tabindex="1"></div> <header><i class="fa fa-bars"></i> Список дел</header> <div id="extendable"> <label> <!-- добавляем свойство tabindex к input, для работы с элементом без помощи мыши. --> <input name="task" tabindex="2"><button><i class="fa fa-external-link"></i></button> <!-- для button tabindex не обязателен, так как отправка будет выполняться по enter --> </label> </div> <div id="container"> <!-- ниже анимация загрузки, сделанная с использованием Font-Awesome. --> <i class="fa fa-circle-o-notch fa-spin fa-5x"></i> </div> <!-- в этом приложении, в качестве примера использования DuelJS, используется DuelJS --> <script type="text/javascript" src="client/scripts/duel/public/lib/duel.min.js"></script> <!-- DuelJS абсолютно не обязательна для использования тут, и я честно говоря не вижу особого смысла пихать её в это приложение, но так как это обучающая статья - допустимо. --> <script type="text/javascript"> /** * Получаем необходимые DOM элементы в переменные: * header - <header></header> (у нас только один такой header) * taskInput - <input name="task"> (у нас только один такой input) * taskBtn - <button></button> (у нас лишь одна такая кнопка на странице) * extendable - <div id="extendable"></div> */ var header = document.getElementsByTagName('header')[0]; var taskInput = document.getElementsByName('task')[0]; var taskBtn = document.getElementsByTagName('button')[0]; var extendable = document.getElementById('extendable'); /** * Функция отображения блока extendable. */ extendable.show = function () { /** * Устанавливаем CSS {display: block} (показываем блок) для extendable. */ this.style.display = 'block'; /** * переводим фокус браузера на taskInput. */ taskInput.focus(); }; /** * Функция скрытия блока extendable. */ extendable.hide = function () { /** * Устанавливаем CSS {display: none} (убираем блок) для extendable. */ this.style.display = 'none'; }; /** * Обработчик события клика по header. */ header.onclick = function () { /** * Внутренняя переменная secondState используется как * память состояния отображения блока extendable. * Тут конечно можно и красивее код сделать, что-то вроде: * this.secondState = !this.secondState; * extendable.show(this.secondState); */ if (!this.secondState) { extendable.show(); this.secondState = true; } else { extendable.hide(); this.secondState = false; } }; /** * Обработчик свободного нажатия tab в браузере. * Так как у fake-header стоит tabindex = 1, то она будет выделена при нажатии tab. * При выделении будет выполнен callback onfocus. */ document.getElementById('fake-header').onfocus = function () { extendable.show(); header.secondState = true; }; /** * Обработчик события клика по кнопке добавления taskBtn. */ taskBtn.onclick = function () { /** * Добавить новую задачу */ tasks.add({title: taskInput.value}); /** * Очистить taskInput */ taskInput.value = ''; }; /** * Обработчик событий клавиатуры на taskInput. */ taskInput.onkeyup = function (event) { /** * При нажатии кнопки enter скрываем extendable и * кликаем по taskBtn (добавляем задачу). */ if (event.keyCode == 13) { extendable.hide(); header.secondState = false; taskBtn.onclick(); } }; /** * Синтаксический сахар для псевдо-модели todoList. * firstRender - переменная обозначающая что рендеринг еще не происходил. * render(items) - метод для перерисовки списка задач, принимает массив задач. * * По сути я просто хочу делать так: todoList.render(tasks); * и взято это из ReactJS. * * Для интересующихся: * http://facebook.github.io/react/index.html#todoExample */ var todoList = { firstRender: true, render: function (items) { /** * todoContainer получает <div id="container"></div>. */ var todoContainer = document.getElementById('container'); /** * Каждый вызов document.createElement создаёт новый DOM-элемент. */ var listElement = document.createElement('ul'); /** * У каждого DOM-элемента есть свойство innerHTML, которое * позволяет писать/читать HTML-код в виде чистого текста. * * В данном случае происходит полная очистка содержимого todoContainer. */ todoContainer.innerHTML = ''; /** * Вызываем map от items, тем самым мы создаем дешевый цикл по обходу * переданных элементов в функцию и для каждого объекта выполняем * создание li > * label > * input[type="checkbox"] + i + item.title. */ items.map(function (item) { var itemElement = document.createElement('li'), itemLabel = document.createElement('label'), itemCheck = document.createElement('input'), itemFACheck = document.createElement('i'), /** * TextNode это просто текст, мы можем * вставлять его в какой-либо DOM-элемент. */ itemText = document.createTextNode(item.title); /** * Указываем что itemCheck это не просто input. * На самом деле использовать именно checkbox * в данном примере не обязательно. * * Вы можете обойтись и без него, сохраняя состояние * в собственную переменную (смотрите ниже). */ itemCheck.type = 'checkbox'; /** * JavaScript не запрещает нам задавать у объекта * желаемые свойства. * * Мы будем использовать objectId в будущем - для удаления. * В item._id.$oid MongoDB присылает нам * создаваемый автоматически ID объекта. */ itemCheck.objectId = item._id.$oid; /** * Для более красивого checkbox'а я решил * в процессе разработки заменить стандартный checkbox * на решение от Font-Awesome. * * http://fortawesome.github.io/Font-Awesome/examples/#list * * classList - это удобный регистр классов DOM-элемента. * classList.add - добавляет новый класс. * classList.remove - соответственно удаляет. * * Подробнее: * https://developer.mozilla.org/en-US/docs/Web/API/Element.classList */ itemFACheck.classList.add('fa'); itemFACheck.classList.add('fa-square'); itemFACheck.classList.add('fa-check-fixed'); /** * appendChild - это простой метод для добавления * указанного DOM-элемента внутрь текущего. * * Напоминаю структуру: * li > * label > * input[type="checkbox"] + i + item.title. */ itemLabel.appendChild(itemCheck); itemLabel.appendChild(itemFACheck); itemLabel.appendChild(itemText); itemElement.appendChild(itemLabel); if (todoList.firstRender) { /* * Класс, добавляющий анимацию появления, но * только при первом рендеринге (смотрите условие выше). * * Хороший комплект готовых решений по анимации находится на: * http://daneden.github.io/animate.css/ */ itemElement.classList.add('fadeInLeft'); } listElement.appendChild(itemElement); /** * Задаем обработчик для события клика на наш checkbox. */ itemCheck.onclick = function (event) { itemFACheck.classList.remove('fa-check'); itemFACheck.classList.add('fa-check-square'); /** * textDecoration line-through зачеркивает текст. */ itemLabel.style.textDecoration = 'line-through'; /** * Берем заранее положенное свойство objectId из нашего DOM-элемента * и удаляем его. */ tasks.remove(this.objectId); /** * Чистим текущее событие. */ this.onclick = function () {}; }; }); /** * Завершаем рендеринг вставляя наше сгенерированное DOM-дерево в чистый container. */ todoContainer.appendChild(listElement); if (todoList.firstRender) { todoList.firstRender = false; } } }; /** * MongoRESTRequest - это функция, которая на простом, объектно-ориентированном * языке является классом, который наследуется от стандартного XMLHttpRequest. * * MongoRESTRequest принимает объект (хеш) с параметрами для MongoDB REST-сервера: * server - адрес сервера с http:// * apiKey - API ключ * collections - путь до коллекций (для облегчения синтаксиса) * * Код ниже с пояснением: * var x = new MongoRESTRequest({ * server: 'http://server/api/1', apiKey: '123', collections: '/databases/abc/collections' * }); * * @param {{server:string, apiKey:string, collections:string}} apiConfig * @returns {XMLHttpRequest} * @constructor */ var MongoRESTRequest = function (apiConfig) { /** * Создаем объект XMLHttpRequest. */ var api = new XMLHttpRequest(); /** * И заносим в него необходимые нам параметры. */ api.server = apiConfig.server; api.key = apiConfig.apiKey; api.collections = apiConfig.collections; /** * Добавляем метод обработки события ошибки. */ api.error = function () { console.error('database connection error'); }; /** * И регистрируем его как обработчик события error. */ api.addEventListener('error', api.error, false); /** * Пишем основной метод обращения к REST-API * Методы ниже будут являться лишь синтаксической оберткой над этим методом. * * Рекомендую ознакомиться с: * http://docs.mongolab.com/restapi/#overview * * @param method - используемый в REST метод (GET, POST, PUT или DELETE) * @param resource - ресурс MongoDB, к которому мы обращаемся, например коллекция users * @param data - отправляемый на сервер объект, к примеру новый документ в коллекцию * @param callback - обработчик по готовности, который получит распарсенный JSON-ответ от сервера */ api.call = function (method, resource, data, callback) { /** * Регистрируем наш обработчик callback. */ this.onreadystatechange = function () { if (this.readyState != 4 || this.status != 200) return; return (callback instanceof Function) ? callback(JSON.parse(this.responseText)) : null; }; /** * Открываем синхронное соединение методом method на необходимый нам адрес. * Параметр bypass позволяет нам избежать лишнего кеширования на стороне клиента. */ this.open(method, api.server + this.collections + '/' + resource + '?apiKey=' + this.key + '&bypass=' + (new Date()).getTime().toString()); /** * Указываем, что мы будем посылать JSON в теле запроса. */ this.setRequestHeader('Content-Type', 'application/json;charset=UTF-8'); /** * Отправляем запрос. */ this.send(data ? JSON.stringify(data) : null); }; /** * Ниже четыре метода для синтаксического сахара. */ api.get = function () { var bIsFunction = arguments[1] instanceof Function, resource = arguments[0], data = bIsFunction ? null : arguments[1], callback = bIsFunction ? arguments[1] : arguments[2]; return this.call('GET', resource, data, callback); }; api.post = function () { var bIsFunction = arguments[1] instanceof Function, resource = arguments[0], data = bIsFunction ? null : arguments[1], callback = bIsFunction ? arguments[1] : arguments[2]; return this.call('POST', resource, data, callback); }; api.put = function () { var bIsFunction = arguments[1] instanceof Function, resource = arguments[0], data = bIsFunction ? null : arguments[1], callback = bIsFunction ? arguments[1] : arguments[2]; return this.call('PUT', resource, data, callback); }; /** * Вообще в JavaScript не рекомендуется использование reserved words, * однако я думаю что в данном контексте это слово уместно. */ api.delete = function () { var bIsFunction = arguments[1] instanceof Function, resource = arguments[0], data = bIsFunction ? null : arguments[1], callback = bIsFunction ? arguments[1] : arguments[2]; return this.call('DELETE', resource, data, callback); }; return api; }; /** * Задаем конфигурацию. */ var config = { server: 'https://api.mongolab.com/api/1', apiKey: 'ключ_API', collections: '/databases/имя_базы/collections' }; /** * Обозначаем переменную tasks как массив. * На самом деле мы вызываем var tasks = new Array(); * * https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Array */ var tasks = []; /** * Создаем новый поток XMLHttpRequest из нашего MongoRESTRequest. * Проще говоря - делаем новый объект класса MongoRESTRequest. */ var api = new MongoRESTRequest(config); /** * Подключаем DuelJS (что это такое читать на http://habrahabr.ru/post/247739/ ). * * На самом деле DuelJS в этом приложении АБСОЛЮТНО не требуется. * * Добавлена DuelJS в это приложение ТОЛЬКО для примера * возможного потенциального использования DuelJS. */ var channel = duel.channel('task_tracker'); /** * Несмотря на то, что tasks - это массив, массив (Array) есть * ничто иное как объект в JS который создан из new Array. * * Добавим в объект tasks нужные нам методы, превратив его во * что-то, подобное Data-Mapper object. * * Метод sync будет использоваться нами для обновления данных. */ tasks.sync = function () { /** * Основной код этого метода находится под условием if. * window.isMaster() - это метод DuelJS, который позволяет * убедиться что метод выполняется в активной вкладке, а не в фоне. */ if (window.isMaster()) { /** * Выполняем REST-запрос на наш PaaS MongoDB сервер. * * http://docs.mongolab.com/restapi/#list-documents * * Выглядит как: * GET /databases/{database}/collections/tasks * * Если вы прочитали эту строчку вы молодец. * * На самом деле запроса будет ДВА, первый запрос будет с методом OPTIONS. * Вы сможете увидеть это, проанализировав вкладку Network * вашей Developer Toolbar в браузере. * * api.get('tasks', function (result) { ... * очень легко читается и удобно используется. * Оно как бы говорит "получить коллекцию tasks и работать с ней в result" */ api.get('tasks', function (result) { /** * Отчищаем tasks, сохраняя при этом все методы и сам tasks. */ tasks.splice(0); /** * При использовании DuelJS мы оповещаем все остальные вкладки о произошедшем событии. * Это сделано для экономии трафика и меньшей нагрузки на сервер. * Повторюсь что в данном приложении использование DuelJS практически не несет * смысла, и добавлено сюда лишь в целях обучения возможностям DuelJS. */ channel.broadcast('tasks.sync', result); for (var i = result.length - 1; i >= 0; i--) { /** * Вносим в массив поочередно объекты из result. * Мы делаем так потому, что не можем написать * tasks = result * так как это очистит наши методы. */ tasks.push(result[i]); } /** * Идея использования подобного синтаксиса пришла мне когда я начал изучать ReactJS. * Да простят меня за это фанаты React, но 128кб ради одного метода render - * я был использовать не намерен. * * React по сути компилирует свой JSX в почти что VanillaJS. */ todoList.render(tasks); }); } else { /** * Этот блок кода делает то же что и блок выше. * Выполняться он будет только на неактивных страницах. * Уже полученный с сервера tasks будет передан в первый (нулевой) * аргумент этой функции. */ tasks.splice(0); var result = arguments[0]; for (var i = result.length - 1; i >= 0; i--) { tasks.push(result[i]); } todoList.render(tasks); } }; /** * К сожалению я так и не реализовал использование метода * переименования в этом приложении. * * Даёшь НЕТ прокрастинации! */ tasks.rename = function (id, title) { for (var i = tasks.length - 1; i >= 0; i--) { if (tasks[i]._id.$oid === id) { tasks[i].title = title; todoList.render(tasks); if (window.isMaster()) { channel.broadcast('tasks.rename', id, title); var api = new MongoRESTRequest(config); /** * Вот так просто можно отредактировать документ на сервере. * * http://docs.mongolab.com/restapi/#view-edit-delete-document */ api.put('tasks/' + id, {title: title}); } break; } } }; /** * Метод для добавления нового документа task в коллекцию tasks. */ tasks.add = function (task) { /** * Снова проверяем активная ли это вкладка. * Снова повторяю что это лишь для примера использования DuelJS * в разработке своих приложений. */ if (window.isMaster()) { /** * Нам потребуется два новых, отдельных потока (хотя на самом деле один). * Они будут заняты исключительно передачей новых документов * на сервер и им будет всё равно на судьбу остальных потоков. * * Первый поток будет занят новым документом task. * Второй поток будет занят новым документом log. * Использование коллекции logs для логирования показано тут * в целях обучения и на деле никак не обрабатывается нашим приложением. * * Если вы будете делать своё приложение на основе этого, то вы сможете * написать в качестве примера визулальный график создания/решения задач. */ var apiThread1 = new MongoRESTRequest(config); var apiThread2 = new MongoRESTRequest(config); apiThread1.post('tasks', task, function (result) { /** * Обратите внимание что прежде чем добавить task на страницу * мы прежде вносим его в базу данных. * * Сделано это для получения ID документа, который сгенерирует * MongoDB и отдаст нам в наш callback. * * http://docs.mongolab.com/restapi/#insert-document */ tasks.push(result); channel.broadcast('tasks.add', result); todoList.render(tasks); }); /** * Очень легко можно передать текущую дату и время в MongoDB. */ apiThread2.post('logs', { when: new Date(), type: 'created' }); } else { /** * Этот блок кода делает то же что и блок выше. * Выполняться он будет только на неактивных страницах. * Уже полученный с сервера task, вместе с его ID, будет передан в первый (нулевой) * аргумент этой функции. */ tasks.push(arguments[0]); todoList.render(tasks); } }; /** * Метод ниже служит нам для удаления документов из базы по ID документа. */ tasks.remove = function (id) { /** * Простой перебор массива tasks для поиска н��жного документа. */ for (var i = tasks.length - 1; i >= 0; i--) { if (tasks[i]._id.$oid === id) { /** * После того, как мы нашли документ в массиве tasks, у которого ID * равен искомому ID. */ if (window.isMaster()) { /** * Делаем запрос на удаление из активного окна. * * Как и в случае с POST - мы логируем удаление и * поэтому нам потребуется два потока. * * Нам не требуется удалять что-то из массива, потому * что в нашем приложении используется автоматическое обноление * массива tasks, каждые 30 секунд. */ var apiThread1 = new MongoRESTRequest(config); var apiThread2 = new MongoRESTRequest(config); apiThread1.delete('tasks/' + id); apiThread2.post('logs', { when: new Date(), type: 'done' }); } break; } } }; /** * Простое обновление данных, с периодом в 30 секунд. */ setInterval(function () { if (window.isMaster()) { tasks.sync(); } }, 30000); /** * Так как мы используем DuelJS - зададим callbacks для событий. */ channel.on('tasks.add', tasks.add); channel.on('tasks.sync', tasks.sync); channel.on('tasks.rename', tasks.rename); /** * Последнее что мы сделаем при загрузке страницы - обновим данные на ней. */ tasks.sync(); </script> </body> </html>
Как изменить цветовую схему всего проекта, поменяв всего одну переменную
В файле main.less имеется следующий код (приведен не до конца, главное понять суть):
Меняем
@import 'palette'; @themeRed: 'red'; @themePink: 'pink'; @themePurple: 'purple'; @themeDeepPurple: 'deep-purple'; @themeIndigo: 'indigo'; @themeBlue: 'blue'; @themeLightBlue: 'light-blue'; @themeCyan: 'cyan'; @themeTeal: 'teal'; @themeGreen: 'green'; @themeLightGreen: 'light-green'; @themeLime: 'lime'; @themeYellow: 'yellow'; @themeAmber: 'amber'; @themeOrange: 'orange'; @themeDeepOrange: 'deep-orange'; @themeBrown: 'brown'; @themeGrey: 'grey'; @themeBlueGrey: 'blue-grey'; /** * http://www.google.com/design/spec/style/color.html#color-color-palette * thanks to https://github.com/shuhei/material-colors */ @theme: @themeBlueGrey; @r50: 'md-@{theme}-50'; @r100: 'md-@{theme}-100'; @r200: 'md-@{theme}-200'; @r300: 'md-@{theme}-300'; @r400: 'md-@{theme}-400'; @r500: 'md-@{theme}-500'; @r600: 'md-@{theme}-600'; @r700: 'md-@{theme}-700'; @r800: 'md-@{theme}-800'; @r900: 'md-@{theme}-900'; @color50: @@r50; @color100: @@r100; @color200: @@r200; @color300: @@r300; @color400: @@r400; @color500: @@r500; @color600: @@r600; @color700: @@r700; @color800: @@r800; @color900: @@r900; @font-face { font-family: 'Roboto Medium'; src: url('../fonts/Roboto-Regular.ttf') format('truetype'); } body { font-family: 'Roboto Medium', Roboto, sans-serif; font-size: 24px; background-color: @color900; color: @color50; margin: 0; padding: 0; }
Меняем
@theme на любую из перечисленных и при этом получаем изменение темы всего приложения целиком. Раньше я не раз вытворял подобные трюки с LESS. К примеру можно делать таким образом (можете расценить это как бонус для людей, которые никогда не видели LESS):@baseColor: #000000; @textColor: contrast(@baseColor); @someLightenColor: lighten(@baseColor, 1%);
6. Демо готового проекта
-> Заветная демка тут
-> Исходный код демки на GitHub
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Используете ли вы какие-либо DB-as-a-Service в своих проектах?
77.89%не использую148
11.05%использую только для разработки21
11.05%использую для разработки и на продакшне21
Проголосовали 190 пользователей. Воздержались 54 пользователя.
