📜 ⬆️ ⬇️

How I Invented the Bike, or My First MEAN Project


Today, in the period of rapid development of web technologies, an experienced front-end developer should always remain in trend, each day deepening their knowledge. And what if you are just starting your journey in the world of the web? You have already been ill with the layout and do not want to stop there. You are drawn to the mysterious world of JavaScript! If this is about you, I hope this article will have to be.


Having a one and a half year experience behind me as a front-end developer, I, tired of the monotonous layout of the next ordinary project, set out to deepen my knowledge of web programming. I had a desire to create my first single page application. The choice of technology stack was obvious, since I was always not indifferent to Node.js, the MEAN methodology was what the doctor prescribed.


Today, there are countless different tutorials on the Internet, in which many helloworld, todo, management agency, etc. applications are created. But just mindlessly following the steps of the tutorial is not my choice. I decided to create a kind of messenger: an application with the ability to register new users, create dialogues between them, communicate with the chat-bot for test users. And so, having carefully thought out the action plan, I set to work.


Next, my story will describe the main points of creating this application, and for greater clarity, I will leave the demo here ( link to github ).


* I also want to note that the purpose of this article, perhaps, is to help students learn not to step on the rake, which I arrived at the time, and to enable more experienced developers to view the code and express their opinions in the comments.


We draw up a plan of action:


  1. Preparatory work
  2. Creating an authorization system
  3. Chat on Angular2 and Socket.io

Preparatory work


Workplace preparation is an integral process of any development, and high-quality implementation of this task is the key to success in the future. First of all, you need to install Express and configure a uniform configuration system for our project. If the first and so everything is clear, then I will focus on the second in more detail.


And so, we will use the remarkable nconf module. Let's create a folder called config, and in its index file write:


const nconf = require('nconf'); const path = require('path'); nconf.argv() .env() .file({ file: path.join(__dirname, './config.json') }); module.exports = nconf; 

Next, in this folder, create a file called config.json and make the first configuration in it - the port that our application listens to:


 { "port": 2016 } 

To embed this setting in the application, you need nothing at all, write one / two lines of code:


 const config = require('./config'); let port = process.env.PORT || config.get('port'); app.set('port', port); 

But it is worth noting that this will work in case the port is defined in this way:


 const server = http.createServer(app); server.listen(app.get('port')); 

Our next task is to set up a unified logging system in our application. As the author of the article "About logging in Node.js" wrote:


It is necessary to write to logs both a lot and a little. So little to understand the state of the application now, and so much so that, if the application collapses, understand why.

For this task we will use the winston module:


 const winston = require('winston'); const env = process.env.NODE_ENV; function getLogger(module) { let path = module.filename.split('\\').slice(-2).join('/'); return new winston.Logger({ transports: [ new winston.transports.Console({ level: env == 'development' ? 'debug' : 'error', showLevel: true, colorize: true, label: path }) ] }); } module.exports = getLogger; 

Of course, the setting may be more flexible, but at this stage it will be enough for us. To use our newly created logger, you just need to connect this module to your work file and call it in the right place:


 const log = require('./libs/log')(module); log.info('Have a nice day =)'); 

Our next task will be setting up proper error handling for normal and ajax requests. To do this, we will make some changes to the code that was previously generated by Express (in the example, only the development error handler is specified):


 // development error handler if (app.get('env') === 'development') { app.use(function(err, req, res, next) { res.status(err.status || 500); if(res.req.headers['x-requested-with'] == 'XMLHttpRequest'){ res.json(err); } else{ // will print stacktrace res.render('error', { message: err.message, error: err }); } }); } 

We have almost finished with the preparatory work, one small, but not unimportant detail remained: to adjust the work with the database. First of all, we will configure the connection to MongoDB using the mongoose module:


 const mongoose = require('mongoose'); const config = require('../config'); mongoose.connect(config.get('mongoose:uri'), config.get('mongoose:options')); module.exports = mongoose; 

In mongoose.connect, we pass two arguments: uri and options , which I registered in advance in the config (for more information, see the documentation for the module ).


I will not describe the process of creating user models and dialogues, since a similar process was perfectly described by the author of the web resource learn.javascript.ru in his Node.js screencast in the video tutorial " Create a model for the user / Mongoose Basics ", just to mention that the user will have properties such as username, hashedPassword, salt, dialogs and created. The dialogs property, in turn, will return an object: the key is the interlocutor id, the value is the dialog id.


If someone is still interested to look at the code of these models:


users.js
 const mongoose = require('../libs/mongoose'); const Schema = mongoose.Schema; const crypto = require('crypto'); let userSchema = new Schema({ username: { type: String, unique: true, required: true }, hashedPassword: { type: String, required: true }, salt: { type: String, required: true }, dialogs: { type: Schema.Types.Mixed, default: {defaulteDialog: 1} }, created: { type: Date, default: Date.now } }); userSchema.methods.encryptPassword = function(password){ return crypto.createHmac('sha1', this.salt).update(password).digest('hex'); }; userSchema.methods.checkPassword = function(password){ return this.encryptPassword(password) === this.hashedPassword; } userSchema.virtual('password') .set(function(password){ this._plainPassword = password; this.salt = Math.random() + ''; this.hashedPassword = this.encryptPassword(password); }) .get(function(){ return this._plainPassword; }); module.exports = mongoose.model('User', userSchema); 
dialogs.js
 const mongoose = require('../libs/mongoose'); const Schema = mongoose.Schema; let dialogSchema = new Schema({ data: { type: [], required: true } }) module.exports = mongoose.model('Dialog', dialogSchema); 

All that remains is nothing - to screw the sessions to the backbone of our application. To do this, create a session.js file and plug in such modules as express-session , connect-mongo , and the module we created from the mongoose.js file:


 const mongoose = require('./mongoose'); const session = require('express-session'); const MongoStore = require('connect-mongo')(session); module.exports = session({ secret: 'My secret key!', resave: false, saveUninitialized: true, cookie:{ maxAge: null, httpOnly: true, path: '/' }, store: new MongoStore({mongooseConnection: mongoose.connection}) }) 

Making this setting in a separate file is important, but not necessary. This will provide an opportunity to further reconcile the sessions and web-files with each other without difficulty. Now connect this module to app.js:


 const session = require('./libs/session'); app.use(session); 

What is more, app.use (session) must be specified after app.use (cookieParser ()) in order for the cookie to be read. Everything! Now we have the ability to save sessions to our database.


And this preparatory work is over. It's time to get to the fun part!


Creating an authorization system


Creation of an authorization system will be divided into two main stages: front end and back end. Since, starting up this application, I was going to learn something new all the time, and with Angular1.x I already had experience, the front end decided to organize on Angular2. The fact that when I created the application, the fourth (and now the fifth) pre-release version of this framework had already been released, gave me confidence that the release would be around the corner. And so, collecting my thoughts, I sat down to write authorization.


For guys who have not yet encountered development on Angular2, please do not be surprised if in the code below you will find javascript syntax not known to you earlier. The thing is that the whole Angular2 is built on typescript. And no, this does not mean that it is impossible to work with this framework using regular javascript! Here for example a great article , during which the author considers the development of Angular2 using ES6.


But typescript is javascript, which scales. Being a compiled javascript superset, this language adds to it all features from ES6 & ES7, a real OOP with blackjack and classes, strict typification and many more cool pieces. And there is nothing to be afraid of: after all, everything that is valid in javascript will work in typescript!


First of all, create the user-authenticate.service.ts file, it will contain the authorization service:


 import { Injectable } from '@angular/core'; import { Http, Headers } from '@angular/http'; @Injectable() export class UserAuthenticateService{ private authenticated = false; constructor(private http: Http) {} } 

Further, inside our class, we will create several methods: login, logout, singup, isLoggedIn. All these methods are of the same type: each one performs its task of sending a request of the type post to the appropriate address. It is not difficult to guess what logical load each of them carries. Consider the login method code:


 login(username, password) { let self = this; let headers = new Headers(); headers.append('Content-Type', 'application/json'); return this.http .post( 'authentication/login', JSON.stringify({ username, password }), { headers }) .map(function(res){ let answer = res.json(); self.authenticated = answer.authenticated; return answer; }); } 

To call this method from the Angular2 component, you need to embed this service into the corresponding component:


 import { UserAuthenticateService } from '../services/user-authenticate.service'; @Component({ ... }) export class SingInComponent{ constructor(private userAuthenticateService: UserAuthenticateService, private router: Router){ ... } onSubmit() { let self = this; let username = this.form.name.value; let password = this.form.password.value; this.userAuthenticateService .login(username, password) .subscribe(function(result) { self.onSubmitResult(result); }); } } 

It should be noted: to get access to the same instance of the service from different components, it needs to be embedded in a common parent component.


And with this we end the frontend with the creation of an authorization system.


Starting backend development, I recommend that you familiarize yourself with the interesting async module (module documentation ). It will become a powerful tool in your arsenal for working with asynchronous javascript functions.


Let's create an authentication.js file in the existing routes directory. Now we specify the given middleware in app.js:


 const authentication = require('./routes/authentication'); app.use('/authentication', authentication); 

Next, simply create a handler for the request post to the address authentication / login. In order not to write a long sheet from various if ... else, we use the waterfall method from the above-mentioned async module. This method allows you to perform a collection of asynchronous tasks in order, passing the results of the previous task to the arguments of the following, and execute some useful callback at the output. Let's now and write this callback:


 const express = require('express'); const router = express.Router(); const User = require('../models/users'); const Response = require('../models/response'); const async = require('async'); const log = require('../libs/log')(module); router.post('/login', function (req, res, next) { async.waterfall([ ... ], function(err, results){ let authResponse = new Response(req.session.authenticated, {}, err); res.json(authResponse); }) } 

For my own convenience, I prepared the Response constructor in advance:


 const Response = function (authenticated, data, authError) { this.authenticated = authenticated; this.data = data; this.authError = authError; } module.exports = Response; 

It remains for us only to write the functions in the order we need into the array passed by the first argument in async.waterfall. Let's create these same functions:


 function findUser(callback){ User.findOne({username: req.body.username}, function (err, user) { if(err) return next(err); (user) ? callback(null, user) : callback('username'); } } function checkPassword(user, callback){ (user.checkPassword(req.body.password)) ? callback(null, user) : callback('password'); } function saveInSession (user, callback){ req.session.authenticated = true; req.session.userId = user.id; callback(null); } 

Briefly I will describe what is going on here: we are looking for a user in the database, if there is no such user, we call a colbek with a 'username' error, in the case of a successful search, we transfer the user to the colbek; call the checkPassword method, again, if the password is correct, transfer the user to the callback, otherwise call the callback with the error 'password'; then save the session to the database and call the final callback.


That's all! Now users of our application have the ability to authorize.


Chat on Angular2 and Socket.io


We have come to the writing of a function that carries the main meaning of our application. In this section, we will organize an algorithm for connecting to chat rooms (chat-rooms) and the function of sending / receiving messages. To do this, we will use the Socket.io library, which makes it very easy to implement data exchange between the browser and the server in real time.


Create a file sockets.js and connect this module to bin / www (Express input file):


 const io = require('../sockets/sockets')(server); 

Since Socket.io works with the web-sockets protocol, we need to think of a way to transfer the current user session to it. To do this, write the file sockets.js already created by us:


 const session = require('../libs/session'); module.exports = (function(server) { const io = require('socket.io').listen(server); io.use(function(socket, next) { session(socket.handshake, {}, next); }); return io; }); 

Socket.io is built in such a way that the browser and the server exchange various events all the time: the browser generates events that the server responds to and vice versa , the server generates events that the browser responds to. Let's write client-side event handlers:


 import { Component } from '@angular/core'; import { Router } from '@angular/router'; declare let io: any; @Component({ ... }) export class ChatFieldComponent { socket: any; constructor(private router: Router, private userDataService: UserDataService){ this.socket = io.connect(); this.socket.on('connect', () => this.joinDialog()); this.socket.on('joined to dialog', (data) => this.getDialog(data)); this.socket.on('message', (data) => this.getMessage(data)); } } 

In the code above, we created three event handlers: connect, joined to dialog, message. Each of them calls the corresponding function. So, the connect event calls the joinDialog () function, which in turn generates the join dialog server event, with which the interlocutor id is passed.


 joinDialog(){ this.socket.emit('join dialog', this.userDataService.currentOpponent._id); } 

Further, everything is simple: the event to the dialog receives an array with user messages, the message event adds new messages to the above mentioned array.


 getDialog(data) => this.dialog = data; getMessage(data) => this.dialog.push(data); 

In order not to return to the frontend in the future, let's create a function that will send user messages:


 sendMessage($event){ $event.preventDefault(); if (this.messageInputQuery !== ''){ this.socket.emit('message', this.messageInputQuery); } this.messageInputQuery = ''; } 

This function generates a message event with which it sends the text of the sent message.


The case remains for small - to write event handlers on the server side!


 io.on('connection', function(socket){ let currentDialog, currentOpponent; socket.on('join dialog', function (data) { ... }); socket.on('message', function(data){ ... }); }) 

In the variables currentDialog and currentOpponent we will save the identifiers of the current dialogue and the interlocutor.


Let's start writing the dialogue connection algorithm. To do this, we use the async library, namely the above-mentioned watterfall method. The order of our actions:


Leave the previous dialog:
 function leaveRooms(callback){ //         for(let room in socket.rooms){ socket.leave(room) } //      callback(null); } 
To get from the database of the user and his interlocutor:
 function findCurrentUsers(callback) { //     : // -    // -    async.parallel([findCurrentUser, findCurrentOpponent], function(err, results){ if (err) callback(err); //    ,      callback(null, results[0], results[1]); }) } 
Connect to existing / create new dialog:
 function getDialogId(user, opponent, callback){ //       if (user.dialogs[currentOpponent]) { let dialogId = user.dialogs[currentOpponent]; //    Id ,      callback(null, dialogId); } else{ //    : // -   // -      async.waterfall([createDialog, saveDialogIdToUser], function(err, dialogId){ if (err) callback(err); //    Id ,      callback(null, dialogId); }) } } 
Get message history:
 function getDialogData(dialogId, callback){ //       Dialog.findById(dialogId, function(err, dialog){ if (err) callback('Error in connecting to dialog'); //    ,      callback(null, dialog); }) } 
Calling the above functions, the global callback:
 //     async.waterfall([ leaveRooms, findCurrentUsers, getDialogId, getDialogData ], //   function(err, dialog){ if (err) log.error(err); currentDialog = dialog; //     socket.join(currentDialog.id); //   joined to dialog,       io.sockets.connected[socket.id].emit('joined to dialog', currentDialog.data); } ) 

On this, the dialog connection algorim is complete, the only thing left is to write a handler for the message event:


 socket.on('message', function(data){ let message = data; let currentUser = socket.handshake.session.userId; let newMessage = new Message(message, currentUser); currentDialog.data.push(newMessage); currentDialog.markModified('data'); currentDialog.save(function(err){ if (err) log.error('Error in saveing dialog =('); io.to(currentDialog.id).emit('message', newMessage); }) }) 

In this sample code, we saved the message text and user ID into variables, then, using the previously created Message constructor, created a new message object, added it to the array and, saving the updated dialog to the database, generated the message event in the room with which we passed message.


That's all our application is ready!


Conclusion


Heh, did you still read it ?! Despite the volume of the article, I did not have time to review all the details of creating the application, since my capabilities are limited by this format. But doing this work, I not only greatly deepened my knowledge in the field of web programming, but also received a lot of pleasure from the work done. Guys, never be afraid to take on something new, difficult, because if you carefully approach the matter, gradually sorting out the pop-up questions, even with zero experience at the start, you can create something really good!


')

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


All Articles