Пилотируем облачную MongoDB через VanillaJS или как бесплатно сделать приватный todo-лист за 15 минут

в 2:12, , рубрики: api, javascript, javascript html5, mongodb, mongolab, rest, xmlhttprequest, Веб-разработка, Облачные вычисления

Пилотируем облачную MongoDB через VanillaJS или как бесплатно сделать приватный todo-лист за 15 минут - 1

На фото: Том Круз в фильме Лучший Стрелок

В этой статье мы рассмотрим взаимодействие 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-ключа

Шаг первый — регистрируемся

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

Шаг второй — заходим в меню пользователя

Пилотируем облачную MongoDB через VanillaJS или как бесплатно сделать приватный todo-лист за 15 минут - 3
Справа на экране будет ссылка в пользовательское меню. В этом меню нас и будет ждать наш заветный API-key.

Шаг третий — забираем API-ключ

Пилотируем облачную MongoDB через VanillaJS или как бесплатно сделать приватный todo-лист за 15 минут - 4
После получения API-ключа мы можем работать с Mongolab REST API

2. Безопасность данных при общении браузера с MongoDB

Пилотируем облачную MongoDB через VanillaJS или как бесплатно сделать приватный todo-лист за 15 минут - 5

На фото: Том Круз смеётся

Хочу предупредить — статья носит чисто учебный характер. Коммуникация с облачной базой данных из браузера может оказаться фатальной ошибкой. Думаю очевидно, что злоумышленник может легко получить доступ к базе просто открыв консоль разработчика. Использование 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. Демо готового проекта

Достаточно рискованно размещать тут ссылку на демо, так как база всё же free.
Пилотируем облачную MongoDB через VanillaJS или как бесплатно сделать приватный todo-лист за 15 минут - 6

На случай если упадёт: это выглядело как-то так как на этом скриншоте

-> Заветная демка тут
-> Исходный код демки на GitHub

Автор: student_ivan

Источник

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


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