На фото: Том Круз в фильме Лучший Стрелок
В этой статье мы рассмотрим взаимодействие 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>
@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. Демо готового проекта
Достаточно рискованно размещать тут ссылку на демо, так как база всё же free.
На случай если упадёт: это выглядело как-то так как на этом скриншоте
-> Заветная демка тут
-> Исходный код демки на GitHub
Автор: student_ivan