Стек MEAN. Пример использования

Автор оригинала: Nomi Ali
  • Перевод
Здравствуйте, уважаемые читатели.

Сегодня вашему вниманию предлагается статья о стеке MEAN (Mongo, Express, Angular, Node) который кажется нам перспективной (при этом достаточно модной) темой. Просим высказаться, хотите ли вы увидеть на русском языке книгу об этом стеке. Добро пожаловать под кат.

В этой статье мы познакомимся со стеком MEAN, в котором сочетаются несколько свободных фреймворков JavaScript. Обсудим, как они работают и как создать с их помощью одностраничное приложение. Технологии JavaScript в данном случае задействуются не только на фронтенде, но и на бэкенде.

Введение


Как понятно из названия, MEAN – это аббревиатура, под которой объединены три фреймворка JavaScript и документоориентированная NoSQL-база данных Итак,

M – это MongoDB

MongoDB – это свободная документоориентированная база данных, организованная таким образом, чтобы обеспечить как масштабируемость, так и гибкость при разработке. Данные в MongoDB записываются не в таблицах и столбцах, как в реляционной базе данных. Вместо этого в MongoDB хранятся JSON-подобные документы с динамическими схемами.

E – это ExpressJS

Express.js – это фреймворк для сервера приложений Node.js, предназначенный для создания одностраничных, многостраничных и гибридных веб-приложений. Де-факто он является стандартным серверным фреймворком для node.js.

A — это AngularJS

AngularJS – это структурный фреймворк для динамических веб-приложений. В нем можно использовать HTML в качестве языка шаблонов, а также расширять синтаксис HTML для четкого и лаконичного описания компонентов вашего приложения. Связывание данных и внедрение зависимостей в Angular позволяют избавиться от массы кода, который пришлось бы писать в противном случае.

N это NodeJS

Node.js – это свободная кроссплатформенная исполняющая среда для разработки серверных веб-приложений. Приложения Node.js пишутся на языке JavaScript и могут запускаться в исполняющей среде Node.js в операционных системах OS X, Microsoft Windows, Linux, FreeBSD, NonStop, IBM AIX, IBM System z и IBM.



Что же такое MEAN?


Термин «стек MEAN» означает набор технологий на базе JavaScript, предназначенных для разработки веб-приложений. MEAN – это сокращение от MongoDB, ExpressJS, AngularJS и Node.js. На уровне клиента, сервера и базы данных весь стек MEAN написан на JavaScript.

Расскажу, как они взаимодействуют. Angular используется для разработки клиентской части. Я буду разрабатывать представления при помощи Angular.js так, чтобы все они отображались на одной странице. На серверной стороне я применю Node.js, то есть, воспользуюсь фреймворком Express.js. При помощи Express я напишу API для коммуникации с базой данных. Наконец, я воспользуюсь MongoDB для хранения данных. Все это показано на схеме.

Итак, с самого начала.

Инструменты

Сперва потребуется установить Node и MongoDB. Они находятся здесь

Скачать Mongodb

Скачать Node

В этой статье я буду работать с Visual Studio Code версии 0.9.2, но вы вполне можете воспользоваться редактором Sublime Text. Пакеты буду устанавливать при помощи Node Package Manager через командную строку Windows. Это очень просто делается командой node cmd после того, как будет установлен nodejs.

Предпосылки


Статья задумывалась как ознакомление с работой стека MEAN и Node, я решил написать ее под впечатлением множества простых вопросов, встречавшихся на stackoverflow. В принципе, здесь показано, как зарегистрировать пользователя, а затем аутентифицировать его при помощи соответствующих контроллеров и модулей.

Использование кода

У меня есть основная директория под названием MeanApp. В этом корневом каталоге лежат различные подкаталоги.

Опишу некоторые наиболее важные подкаталоги.

  1. В Angular будет три подкаталога: Controllers для контроллеров angular, Modules и Models.
  2. В Public будут лежать все библиотеки javascript.
  3. В Routes будут лежать api expressjs api, которые станут обрабатывать запрос и взаимодействовать с MongoDB.
  4. В Views будут лежать все рассматриваемые представления, все – в своих каталогах.

В корневом каталоге я воспользуюсь server.js для файла запуска. Итак, начнем.

Все остальные свободные библиотеки в вашем приложении нужно устанавливать через диспетчер пакетов npm при помощи команды:

npm install [package] --save

Нужно выполнить следующую команду для установки требуемых учебных пакетов:

npm install express --save
npm install path --save
npm install morgan --save
npm install cookie-parser --save
npm install body-parser --save
npm install bcrypt-nodejs --save
npm install passport --save
npm install passport-local --save
npm install express-session --save
npm install mongoose --save

Начнем с проектирования пользовательского интерфейса. Сначала создадим новый файл modules.js в каталоге Angular/Modules.

// Первое приложение на Angular 
var main = angular.module("main", ['ui.router','ngRoute','ngResource'])
.run(function($http,$rootScope)
{
    if(sessionStorage.length > 0){
        $rootScope.current_user = sessionStorage.current_user;
        $rootScope.authenticated = true;
    }else{
        $rootScope.authenticated = false;
        $rootScope.current_user = 'Guest';
    }
    
    $rootScope.signout = function(){
        $http.get('auth/signout');
        $rootScope.authenticated = false;
        $rootScope.current_user = 'Guest';
        sessionStorage.clear();
    };

});                                                                                                     
// Конфигурация маршрутизации (определяем маршруты)
main.config([
    '$stateProvider', '$urlRouterProvider', '$httpProvider',
    function ($stateProvider, $urlRouterProvider,$rootScope) {
        $urlRouterProvider.otherwise('/');
        $stateProvider
            .state('home', {
                url: '/',
                templateUrl: 'Index.html',
                caseInsensitiveMatch: true,
                controller: 'MainController'
            })
            .state('contact', {
                url: '/contact',
                templateUrl: 'Contact.html',
                caseInsensitiveMatch: true,
                controller: 'MainController'
            })
            .state('about', {
                url: '/about',
                templateUrl: 'About.html',
                caseInsensitiveMatch: true,
                controller: 'MainController'
            })
            .state('login',{
                url: '/login',
                templateUrl: 'login.html',
                caseInsensitiveMatch: true,
                controller: 'AuthController'
            })
            .state('register',{
                url: '/register',
                templateUrl: 'register.html',
                caseInsensitiveMatch: true,
                controller: 'AuthController'
            }).state('unauth',{
                url: '/unauth',
                templateUrl: 'unauth.html',
                caseInsensitiveMatch: true
            });
    }
]);       

Теперь создадим модель user.js в каталоге Angular/Models:

//создаем новую модель
var mongoose = require('mongoose'); // ссылаемся на mongoose для создания понятной модели типа класса
// определяем схему для пользовательской модели
var userSchema = new mongoose.Schema({
    username: String,
    password: String,
    email: String,
    role: String,
    created_at: {type: Date, default: Date.now}
    
});
mongoose.model('User', userSchema);
var User = mongoose.model('User');
exports.findByUsername = function(userName, callback){
User.findOne({ user_name: userName}, function(err, user){
        if(err){
            return callback(err);
        }
return callback(null, user);
});
}

exports.findById = function(id, callback){
User.findById(id, function(err, user){
        if(err){
           return callback(err);
        }
         return callback(null, user);
    });
}

Модель готова. Теперь создадим два новых контроллера:

AuthController.js

и

MainController.js

в каталоге Angular/Controller.

// контроллер auth
main.controller("AuthController", function ($scope, $http, $rootScope, $location) {
$scope.user = {username: '', password: ''};
$scope.error_message = '';
// входной вызов к webapi (сервис, реализованный при помощи node)
$scope.login = function(){
        $http.post('/auth/login', $scope.user).success(function(data){
        if(data.state == 'success'){
                $rootScope.authenticated = true;
                $rootScope.current_user = data.user.username;
                $rootScope.sess = data.user;
                sessionStorage.setItem('current_user', $rootScope.sess.username);
                $location.path('/');
                }
            else{
                $scope.error_message = data.message;
                $rootScope.sess = null;
            }
        });
};
  // входной вызов к webapi (сервис, реализованный при помощи node)
    $scope.register = function(){
console.log($scope.user);
        $http.post('/auth/signup', $scope.user).success(function(data){
if(data.state == 'success'){
                $rootScope.authenticated = true;
                $rootScope.current_user = data.user.username;
                $location.path('/');
            }
            else{
                $scope.error_message = data.message;
            }
        });
    };
});

//в главном контроллере можнно определить что угодно для создания однонаправленной связи  you в главном представлении
main.controller("MainController", function ($scope) {}); // не заполняется, если в нем нет необходимости – не создается

Создадим новый каталог Views, и добавим в главный каталог следующий код ejs, так как ejs – это механизм отображения, используемый nodejs в файле Starter.ejs.

<html ng-app="main">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="viewport" content="width=device-width" />
    <title>Super Application</title>
    <link href="bootstrap.css" rel="stylesheet" />
    <link href="Site.css" rel="stylesheet" />
    <script src="modernizr-2.6.2.js"></script>
    <script src="jquery-1.10.2.js"></script>
    <script src="bootstrap.js"></script>
    <script src="angular.js"></script>
    <script src="angular-route.js"></script>
    <script src="angular-ui-router.js"></script>
    <script src="angular-resource.js"></script>
    <script src="/Modules/mainApp.js"></script>
    <script src="/Controllers/MainController.js"></script>
    <script src="/Controllers/AuthController.js"></script>
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#/home">Application name</a>
            </div>
            
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li><a href="#/home">Home</a></li>
                    <li><a href="#/about">About</a></li>
                    <li><a href="#/contact">Contact</a></li>
                </ul>
                <ul class="nav navbar-nav navbar-right">
                    <li><p class="navbar-right navbar-text">Signed in as {{current_user}}</p></li>
                    <li><p class="navbar-right navbar-text" ng-hide="authenticated">
                        <a href="#/login">Login</a> or <a href="#/register">Register</a>
                    </p></li>
                    <li><p class="navbar-right navbar-text" ng-show="authenticated">
                        <a href="#" ng-click="signout()">Logout</a>
                    </p></li>
                </ul>
            </div>
        </div>
    </div>
    <div class="container body-content">
        <div ui-view>
        </div>
        <hr />
        <footer>
            <p>My ASP.NET Application</p>
        </footer>
    </div>
</body>
</html>

Теперь создадим каталог Authentication в каталоге Views; здесь будут содержаться все представления для аутентификации (регистрация, вход в систему, т.д.).

Для представления, предназначенного для аутентификации, добавим в каталог Authentification новый файл под названием login.html.

<form class="form-auth" ng-submit="login()">
    <h2>Log In</h2>
    <p class="text-warning">{{error_message}}</p>
    <input type="username" ng-model="user.username" placeholder="Username" class="form-control" required><br>
    <input type="password" ng-model="user.password" placeholder="Password" class="form-control" required><br>
    <input type="submit" value="Log in" class="btn btn-primary" />
</form>

Для представления, предназначенного для регистрации, добавим в каталог Authentification новый файл под названием register.html

<form class="form-auth" ng-submit="register()">
    <h2>Register</h2>
    <p class="text-warning">{{error_message}}</p>
    <input type="email" ng-model="user.email" placeholder="Email" class="form-control" required><br>
    <input type="username" ng-model="user.username" placeholder="Username" class="form-control" required><br>
    <input type="password" ng-model="user.password" placeholder="Password" class="form-control" required><br>
    <select ng-init="user.role = roles[0].name" ng-model="user.role" ng-options="role.name as role.name for role in roles" class="form-control" required></select><br>
    <input type="submit" value="Sign Up" class="btn btn-primary" />
</form>


Для представления, сообщающего, что аутентификация не пройдена, добавим в каталог Authentification новый файл под названием unauth.html:

<form class="form-auth">
    <h2>You are Authentic/Unauthorize to access this page, This is because </h2>
    <p>1) Not login? Please register to access resources.</p>
    <p>2) Registered: You are not Authorize user, Please contact Administrator.</p>
</form>

Теперь создадим в каталоге Views новый каталог Main. Здесь будут содержаться все основные представления (index, about us, contact и т.д).

Для представления Index добавим новый файл index.html в каталог Main.

<div>
    <div class="jumbotron">
        <h1>Node.js Application</h1>
        <p class="lead">Node.js is a free Javascript framework for building great Web sites and Web applications using HTML, CSS and JavaScript.</p>
        <p><a class="btn btn-primary btn-lg">Learn more »</a></p>
    </div>
    <div class="row">
        <div class="col-md-4">
            <h2>Getting started</h2>
            <p>
                ASP.NET MVC gives you a powerful, patterns-based way to build dynamic websites that
                enables a clean separation of concerns and gives you full control over markup
                for enjoyable, agile development.
            </p>
            <p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301865">Learn more »</a></p>
        </div>
        <div class="col-md-4">
            <h2>Get more libraries</h2>
            <p>NuGet is a free Visual Studio extension that makes it easy to add, remove, and update libraries and tools in Visual Studio projects.</p>
            <p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301866">Learn more »</a></p>
        </div>
        <div class="col-md-4">
            <h2>Web Hosting</h2>
            <p>You can easily find a web hosting company that offers the right mix of features and price for your applications.</p>
            <p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301867">Learn more »</a></p>
        </div>
    </div>
</div>

Для представления About us добавим новый файл About.html в каталог Main.

<div>
    <h2>About Us</h2>
    <h3>Message</h3>
    <p>Use this area to provide additional information.</p>
</div>
Для представления «Contact us» добавим новый файл Contact.html в каталог Main.
<div>
    <h2>Contact Us</h2>
    <h3>Message</h3>
    <address>
        One Microsoft Way<br />
        Redmond, WA 98052-6399<br />
        <abbr title="Phone">P:</abbr>
        425.555.0100
    </address>
    <address>
        <strong>Support:</strong>   <a href="mailto:Support@example.com">Support@example.com</a><br />
        <strong>Marketing:</strong> <a href="mailto:Marketing@example.com">Marketing@example.com</a>
    </address>
</div>

Итак, мы завершили проектирование интерфейса на angular.

Давайте напишем бэкенд на node.js

Создаем новые маршруты на основе запросов об аутентификации под названием authentication.js в каталоге Routes.

var express = require('express');
var router = express.Router();
module.exports = function(passport){
// Отправляем состояние успешного входа в систему обратно в представление (angular)
    router.get('/success',function(req,res){
           res.send({state: 'success', user: req.user ? req.user: null});    
    });
// Отправляем состояние неудавшегося входа в систему обратно в представление (angular)
    router.get('/failure',function(req,res){
res.send({state: 'failure',user:null,message:"Invalid username or password"});
    });
    // Запрос входа в систему
router.post('/login',passport.authenticate('login',{
        successRedirect: '/auth/success',
        failureRedirect: '/auth/failure'
    }));
// Запрос на регистрацию в системе
    router.post('/signup', passport.authenticate('signup', {
        successRedirect: '/auth/success',
        failureRedirect: '/auth/failure'
    }));
// Запрос на выход из системы
    router.get('/signout', function(req, res) {
        req.session.user = null;
        req.logout();
        res.redirect('/');
    });
return router;
}

Создаем router.js в том же каталоге.

var express = require('express');
var router = express.Router();
var mongoose = require( 'mongoose' );

router.get('/',function(req,res,next){    
    res.render('Starter',{title:"Super App"});    
});

module.exports = router;

Создаем новый каталог под названием Passport и добавляем новый файл (API) Name passport-init.js, после чего добавляем следующий код. В вашем маршруте аутентификации это будет называться Authentication Api.

var mongoose = require('mongoose');   
var User = mongoose.model('User');
var LocalStrategy   = require('passport-local').Strategy;
var bCrypt = require('bcrypt-nodejs');
module.exports = function(passport){
// Passport должен иметь возможность сериализовывать и десериализовывать пользователей,чтобы
Поддерживать устойчивые сеансы работы с системой
    passport.serializeUser(function(user, done) {
        console.log('serializing user:',user.username);
        done(null, user._id);
    });
passport.deserializeUser(function(id, done) {
        User.findById(id, function(err, user) {
            console.log('deserializing user:',user.username);
            done(err, user);
        });
    });
passport.use('login', new LocalStrategy({
            passReqToCallback : true
        },
        function(req, username, password, done) { 
        // Проверяем в mongo, существует ли пользователь с таким именем
            User.findOne({ 'username' :  username }, 
                function(err, user) {
                    // В случае любой ошибки происходит возврат через метод done 
                    if (err)
                        return done(err);
                    // Имя пользователя не существует, логируем ошибку и делаем перенаправление назад
                    if (!user){
                        console.log('User Not Found with username '+username);
                        return done(null, false);                 
                    }
                    // Пользователь существует, но введен неверный пароль. Логируем ошибку 
                    if (!isValidPassword(user, password)){
                        console.log('Invalid Password');
                        return done(null, false); // переадресация назад на страницу входа
                    }
                    // Имя пользователя и пароль верны, возвращаем пользователя через метод done
                    // что будет трактоваться как успех
                    return done(null, user);
                }
            );
        }
    ));
passport.use('signup', new LocalStrategy({
            passReqToCallback : true // позволяет нам передать весь запрос в обратный вызов
        },
        function(req, username, password, done, email, role) {
// находим в mongo пользователя с указанным именем
            User.findOne({ 'username' :  username }, function(err, user) {
                // В случае любой ошибки возвращаемся через метод done
                if (err){
                    console.log('Error in SignUp: '+ err);
                    return done(err);
                }
                // уже существует
                if (user) {
                    console.log('User already exists with username: '+username);
                    return done(null, false);
                } else {
                    // если пользователя нет – создаем его
                    var newUser = new User();
                    // задаем локальные учетные данные пользователя
                    newUser.username = username;
                    newUser.password = createHash(password);
                    newUser.email = req.body.email;
                    newUser.role = req.body.role;
                    // сохраняем пользователя
                    newUser.save(function(err) {
                        if (err){
                            console.log('Error in Saving user: '+err);  
                            throw err;  
                        }
                        console.log(newUser.username + ' Registration succesful');    
                        return done(null, newUser);
                    });
                }
            });
        })
    );
var isValidPassword = function(user, password){
        return bCrypt.compareSync(password, user.password);
    };
    // Генерируем хеш при помощи bCrypt
    var createHash = function(password){
        return bCrypt.hashSync(password, bCrypt.genSaltSync(10), null);
    };
};

Теперь добавляем в корневой каталог новый файл server.js, так как именно с этого файла мы будем стартовать:

//server.js
//добавляем в приложение свободные модули 
var express = require('express'); //express 
var path = require('path'); // здесь будем ссылаться на физические файлы
var logger = require('morgan'); 
var cookieParser = require('cookie-parser'); //для поддержки сеансов
var bodyParser = require('body-parser'); //для синтаксического разбора json
var bcrypt = require('bcrypt-nodejs'); 
var passport = require('passport'); //Используем passportjs для аутентификации
var LocalStrategy = require('passport-local').Strategy; //используем стратегию паспорта
var session = require('express-session'); // для поддержки сеансов
var mongoose = require('mongoose'); //для mongodb, базы данных
var models_user = require('./Angular/Models/user.js'); // ссылаемся на модели в server.js

// связь с базой данных
mongoose.connect('mongodb://localhost/AngularizeApp');

// импортируем маршрутизаторы
var router = require('./Routes/router');
var authenticate = require('./Routes/authentication')(passport);

// для использования express в рамках всего приложения
var app = express();

//сообщаем узлу, что My application будет использовать движок ejs для визуализации, смотрим настройку движка
app.set('views', path.join(__dirname, 'Views'));
app.set('view engine', 'ejs');

//сообщаем узлу глобальную конфигурацию parser, logger и passport
app.use(cookieParser());
app.use(logger('dev'));
app.use(session({
  secret: 'keyboard cat'
}));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(passport.initialize()); // инициализация паспорта
app.use(passport.session()); // инициализация сеанса паспорта

// сообщаем узлу о тех каталогах, из которых приложение может брать ресурсы
app.use('/', router);
app.use('/auth', authenticate);
app.use(express.static(path.join(__dirname, 'scripts')));
app.use(express.static(path.join(__dirname, 'Content')));
app.use(express.static(path.join(__dirname, 'Angular')));
app.use(express.static(path.join(__dirname, 'Views/Main')));
app.use(express.static(path.join(__dirname, 'Views/Authentication')));

//указываем auth-api для паспорта, так, чтобы он мог его использовать.
var initPassport = require('./Passport/passport-init');
initPassport(passport);

//запускаем сервер на узле
var server = app.listen(3000, function () {
  var host = server.address().address;
  var port = server.address().port;
  console.log('Example app listening at http://%s:%s', host, port);
});

// экспортируем это приложение в виде модуля 
module.exports = app;

Итак, с кодом все готово. Это приложение можно запустить при помощи команды node server.js через командную строку, но сначала перейти в корневой каталог MEANApp при помощи команды cd c://MeanApp etc.

Также понадобится запустить сервер mongodb. Для этого откройте новое окно cmd и введите mongod -dbpath. Если установлена база данных mongodb, и добавлено имя переменной, то эта команда успешно запустит ваш сервер mongodb server. Не закрывайте это окно командной строки, так как если вы его закроете, то и сервер прекратит работу.

mongod -dbpath



После запуска вышеприведенной команды можете проверить вашу базу данных в командной строке. Для этого откройте еще одно окно командной строки, введите в нем mongodb – увидите среди подключенных клиентов клиент интересующей вас базы данных и сможете на него переключиться. О mongodb можно почитать в документации, расположенной здесь.

Запустить приложение можно при помощи следующей команды:

MeanApp>node server.js



После входа в систему вы сможете проверить все вызовы.

Итак, перед нами работающее приложение, поддерживающее возможность аутентификации и сеансы для хранения данных на стороне сервера.

Весь код расположен на github здесь.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Актуальность стека MEAN

  • 86,0%Тема интересна, стоит подобрать по ней книгу178
  • 14,0%Не привлекает29

Комментарии 7

    +4
    Намного интереснее было бы почитать про такой стек со вторым ангуляром, так как он теперь позволяет рендерить что-то на сервере (да и вообще, можно делать изоморфные приложения).

    Если же всё-таки брать первый ангуляр, то тогда желательно использовать его современно: убрать $scope из контроллеров и использовать синтаксис controllerAs, побольше использовать компоненты, ну и es6 с babel подключить.
      +3
      Вот я бы с удоволствием почитал бы вводный ккурс по MEAN — на апворке все чаще и чаще заказы на него мелькают. Но — не простыню типа делаем вот так и копируем сюда, немного магии и вот оно, видите, работает! Не, такое нафиг не надо. Надо бы что нибудь с уклоном в теорию но не выпадая из практики,  то есть вот сделали мы вот так, ну прикольно, а зачем? а как бы мы делали без этого? А раньше как жили? А почему мы делаем так а не иначе и как это отражается на шелковистости наших волос?

      Почему я должен выбросить свой постгрес и пересесть на монгу (реально, все разговоры на эту тему приводили к тому что к монге еще и редис всплывал для обеспечения очереди ну и там еще какие костыли, в зависимости от собеседника). 

      Только пожалуйста не надо "каждой задаче свой инструмент". Вот есть инструмент, мы его обсуждаем, Вы интересуетесь что было бы интересно, я отвечаю — каким именно задачам и где у него ручка? Вот такая статья была бы интересна. Желательно от пострадавшего при неправильном использовании инструмента персонажа.
        +1
        В таком случае вам подойдёт вот эта книга Getting MEAN with Mongo, Express, Angular, and Node.
        Тут всё начинается с серверного рендеринга на ноде, продолжается с добавлением ангуляра для динамичности, и заканчивается SPA.
        Прочитал пол книги, в целом неплохо, главное, что без воды. Теория всегда подкрепляется практическими примерами, которые можно скачать на гитхабе.
        Конечно тут нету ES6, ангуляр компонентов и всех остальных передовых вещей. Здесь делается упор на процесс построения приложения согласно подходу MEAN.
          0

          Мммм. Выглядит аппетитно! Спасибо большое!

            0
            Вы как в воду глядели, сэр!

            Уже переведена и в редактуре
          0
          "Node.js – это свободная кроссплатформенная исполняющая среда для разработки серверных веб-приложений"

          Почему же только веб?
          • НЛО прилетело и опубликовало эту надпись здесь

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое