📜 ⬆️ ⬇️

Piloting cloud-based MongoDB via VanillaJS or how to make a private todo list for 15 minutes for free


In the photo: Tom Cruise in the film Best Shooter

In this article we will look at the interaction of Single Page HTML Application with cloud MongoDB through JavaScript. As MongoDB-as-a-Service, I'll take Mongolab . The cost of a deployed MongoDB, with a volume of 500mb, will cost us only 0 USD.

To create a todo list, we will not need a backend . We will interact with Mongolab through the REST API, and we will write a wrapper for it in the client part without resorting to third-party JavaScript frameworks.


Article Navigation


1. Registering in Mongolab and getting the API key
2. Data security when communicating with MongoDB
3. Scope of such decisions
4. Let's get down to business
5. We disassemble the application code
6. Demo of the finished project
')

1. Registering in Mongolab and getting the API key


Step one - register


Registration is simple and does not require binding payment cards. Mongolab is a pretty useful service. In our company, we use it as a sandbox during the development of web applications.

Step two - go to the user menu


On the right of the screen will be a link to the user menu. In this menu, we will be waiting for our cherished API-key.

Step three - pick up the API key


After receiving the API key, we can work with Mongolab REST API

2. Data security when communicating with MongoDB



Pictured: Tom Cruise laughs

I want to warn you - the article is purely educational in nature. Communication with a cloud database from a browser can be a fatal error. I think it is obvious that an attacker can easily access the database simply by opening the developer console. Using the read-only user base solves this problem only if absolutely all the data in the cloudy MongoDB doesn’t carry any importance or privacy.

3. Scope of such decisions


Based on this approach, we can create a todo-list application that you can keep on your computer, write an application for Android / iOS / Windows Phone / Windows 8.1 using just one html and javascript.

4. Let's get down to business


It took me exactly 15 minutes to write the todo application, I spent two hours writing this article (+ commenting on the code). The color scheme was taken from Google , which was kindly handed down in LESS by one kind person . What I did, I uploaded to github so that you could appreciate the work with the cloud base without wasting your precious time. You will find the link at the end of the article.

We will communicate with the REST API via XMLHttpRequest . The modern world of web development is very confident focused on solutions like jQuery or Angular - they shove them everywhere and anywhere. Often you can do without them. The new XMLHttpRequest () object is a kind of stream associated with a js object that has the basic open and send methods (open a connection and send data) and the main onreadystatechange event. To communicate with REST, we need to set the Content-Type header : application / json; charset = UTF-8 , for this we use the setRequestHeader method.

This is how a simple REST application might look like:

 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(); 

And the method you do not wrap?

 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); }); 

Add a new entry to the demo collection with title: test

 var test = { title: 'test' }; api.call('POST', 'databases/mydb/demo', test, function (result) { test = result; //  ID     }); 

Synchronous flow problem

Our variable api is only one thread, so the following code is erroneous:

 api.call('POST', 'databases/mydb/demo', test1); api.call('POST', 'databases/mydb/demo', test2); 

In order to bypass synchronicity, we need two separate streams - for the first POST and for the second. To avoid describing the call method each time, we come to the decision to build a MongoRESTRequest “pseudo-class”, which would actually be a function that returns a new XMLHttpRequest object with a ready-to-use call method:

 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); 

Now this code will be executed correctly.
Continuing to modify our MongoRESTRequest, we will come to approximately the version that will be described in the source code of the application below.

A little bit about how you can do without the template engine:

Usually I see something like this in the code of an average jQuery fan:

 $('#myDiv').html('<div class="red"></div>'); 

And now take a look how it should be in reality, without connecting extra 93.6kb (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 

Okay, okay, of course, we all know that this can be done in the following way:

 document.getElementById('myDiv').innerHTML = '<div class="red"></div>'; 


A little more about working with DOM in Vanilla:

Use map to create a list (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); }); 

At the output we have ( link to jsFiddle to play around ):

 <ul id="myList"> <li></li> <li></li> <li></li> </ul> 

The advantage of this work of JavaScript is the ability to fully work with objects:

 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 link to check

5. We disassemble the application code


I tried to comment on every line of code.
 <!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.imtqy.com/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.imtqy.com/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.imtqy.com/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> 


How to change the color scheme of the entire project, changing only one variable
In the main.less file there is the following code (it’s not fully provided, the main thing is to understand the essence):

 @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; } 

We change @themeto any of the above and at the same time we get the change of the theme of the entire application as a whole. I used to do such tricks with LESS more than once. For example, you can do it this way (you can regard this as a bonus for people who have never seen LESS):

 @baseColor: #000000; @textColor: contrast(@baseColor); @someLightenColor: lighten(@baseColor, 1%); 



6.


->
-> GitHub

Source: https://habr.com/ru/post/248605/


All Articles