Содержание


MEAN-программирование

тур по MEAN-приложению

От HTTP-запроса со стороны сервера до отображения результата на стороне клиента

Comments

Серия контента:

Этот контент является частью # из серии # статей: MEAN-программирование

Следите за выходом новых статей этой серии.

Этот контент является частью серии:MEAN-программирование

Следите за выходом новых статей этой серии.

В статье MEAN-программирование: знакомство со стеком MEAN мы установили и настроили среду разработки MEAN. Теперь я познакомлю вас с четырьмя ключевыми элементами стека MEAN — MongoDB, Express, AngularJS и Node.js — в ходе тура по простому приложению MEAN.JS, которое мы создали. Исследуя это приложение, мы проследим весь путь от входящих HTTP-запросов со стороны сервера до отображения результата на стороне клиента.

Запустите свой локальный экземпляр MongoDB, набрав команду mongod. (В UNIX®-подобных операционных системах можно ввести команду mongod &, чтобы процесс выполнялся в фоновом режиме.) Затем перейдите в каталог test, который вы создали при чтении предыдущей статьи, и введите команду grunt, чтобы запустить приложение, сгенерированное генератором Yeoman. Вы увидите результат, подобный показанному в листинге 1.

Листинг 1. Запуск локального приложения MEAN.JS
$ grunt
Running "jshint:all" (jshint) task
>> 46 files lint free.

Running "csslint:all" (csslint) task
>> 2 files lint free.

Running "concurrent:default" (concurrent) task
Running "nodemon:dev" (nodemon) task
Running "watch" task
Waiting...
[nodemon] v1.0.20
[nodemon] to restart at any time, enter 'rs'
[nodemon] watching: app/views/**/*.* gruntfile.js server.js config/**/*.js app/**/*.js
[nodemon] starting 'node --debug server.js'
debugger listening on port 5858

 NODE_ENV is not defined! Using default development environment

MEAN.JS application started on port 3000

Откройте в браузере главную страницу приложения http://localhost: 3000. Она показана на рисунке 1.

Рисунок 1. Локальная главная страница MEAN.JS
Скриншот главной страницы приложения MEAN.JS
Скриншот главной страницы приложения MEAN.JS

Чтобы понять, как работает это приложение, рассмотрим структуру каталогов. (Подробнее см. на странице Структура каталогов документации MEAN.JS.)

Чтобы понять, как работает это приложение, рассмотрим структуру каталогов.

Изучение файлов конфигурации Node.js и Bower

Исходный код мы рассмотрим чуть позже. Пока же вернемся на короткое время к package.json, файлу конфигурации Node.js, о котором мы говорили в предыдущей статье. И еще я добавлю несколько слов о его собрате со стороны клиента, bower.json. Оба эти файла находятся в корневом каталоге проекта.

Файл package.json

Пожалуй, важнейший файл конфигурации в любом приложении Node.js — это файл package.json. В его первой части находятся метаданные приложения, переданные генератору Yeoman, — имя, описание, автор и т.п., — как показано в листинге 2.

Листинг 2. Файл package.json, часть 1
{
  "name": "test",
  "description": "Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js",
  "version": "0.0.1",
  "author": "Scott Davis",
  "engines": {
    "node": "0.10.x",
    "npm": "1.4.x"
  },

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

Листинг 3. Файл package.json, часть 2
"scripts": {
  "start": "grunt",
  "test": "grunt test",
  "postinstall": "bower install --config.interactive=false"
},

Команду grunt вы уже ввели, чтобы запустить приложение. Позднее вы введете команду grunt test для выполнения модульных тестов. Перехватчик postinstall - это первый намек на разрыв между зависимостями на стороне сервера и на стороне клиента.

Но главная часть этого важнейшего файла — список зависимостей приложения, показанный в листинге 4. Все эти модули CommonJS работают на сервере.

Листинг 4. Файл package.json, часть 3
"dependencies": {
  "express": "~4.2.0",
  "mongoose": "~3.8.8"
},
"devDependencies": {
  "grunt-mocha-test": "~0.10.0",
  "grunt-karma": "~0.8.2",
  "karma": "~0.12.0",
  "karma-jasmine": "~0.2.1",
  "karma-coverage": "~0.2.0",
  "karma-chrome-launcher": "~0.1.2",
  "karma-firefox-launcher": "~0.1.3",
  "karma-phantomjs-launcher": "~0.1.2"
}

В блоке dependencies объявляются зависимости времени выполнения (такие как Express для маршрутизации и Mongoose для соединения с MongoDB). В блоке devDependencies объявляются зависимости разработчика и времени разработки (включая платформы тестирования, такие как Mocha, Jasmine и Karma).

Файл bower.json

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

Листинг 5. Файл bower.json
{
  "name": "test",
  "version": "0.0.1",
  "description": "Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js",
  "dependencies": {
    "bootstrap": "~3",
    "angular": "~1.2",
    "angular-resource": "~1.2",
    "angular-mocks": "~1.2",
    "angular-cookies": "~1.2",
    "angular-animate": "~1.2",
    "angular-touch": "~1.2",
    "angular-sanitize": "~1.2",
    "angular-bootstrap": "~0.11.0",
    "angular-ui-utils": "~0.1.1",
    "angular-ui-router": "~0.2.10"
  }
}

Как видите, файл bower.json похож на файл package.json. Он содержит некоторые из тех же полей метаданных и использует блок dependencies для определения зависимостей на стороне клиента, таких как Bootstrap (внешнее оформление и адаптивный веб-дизайн) и AngularJS (одностраничное клиентское приложение).

Аналогично, исходный код приложения распределен между двумя ключевыми каталогами: на стороне сервера и на стороне клиента.

Изучение структуры каталогов

MEAN-приложение имеет четыре основных каталога, как показано в листинге 6.

Листинг 6. Структура каталогов MEAN
$ ls -ld */
drwxr-xr-x+  7 scott  staff   238 Jun  6 14:06 app/
drwxr-xr-x+  8 scott  staff   272 Jun  6 14:06 config/
drwxr-xr-x+ 49 scott  staff  1666 Jun  6 14:07 node_modules/
drwxr-xr-x+  8 scott  staff   272 Jun  6 14:06 public/
app
Содержит исходный код сервера.
config
Содержит файлы конфигурации.
node_modules
Содержит серверные модули, указанные в файле package.json.
public
Содержит клиентский исходный код — включая каталог lib, содержащий клиентские библиотеки, указанные в файле bower.json.

Мы сосредоточимся на каталогах app и public. Охота за неуловимым исходным кодом главной страницы приложения начинается с каталога app.

Изучение стека MEAN со стороны сервера

Структура каталогов приложения показана в листинге 7.

Листинг 7. Структура каталога app (на стороне сервера)
$ tree app
app
|--- controllers
|�� |--- articles.server.controller.js
|�� |--- core.server.controller.js
|�� |--- users.server.controller.js
|--- models
|�� |--- article.server.model.js
|�� |--- user.server.model.js
|--- routes
|�� |--- articles.server.routes.js
|�� |--- core.server.routes.js
|�� |--- users.server.routes.js
|--- tests
|�� |--- article.server.model.test.js
|�� |--- user.server.model.test.js
|--- views
    |--- 404.server.view.html
    |--- 500.server.view.html

    |--- index.server.view.html
    |--- layout.server.view.html

Тем, кто писал серверные приложения MVC, знакома типичная последовательность действий:

  1. Входящий HTTP-запрос поступает в маршрутизатор.
  2. Маршрутизатор находит подходящий контроллер для передачи запроса.
  3. Контроллер строит модель (или модели) из базы данных и передает ее представлению.
  4. Представление создает HTML-страницу, объединяя модель с шаблоном, а затем передает результат ожидающему HTTP-ответу.

В файле app/routes/core.server.routes.js (в составе платформы Express), показанном в листинге 8, содержится ключевая точка входа в приложение.

Листинг 8. Файл app/routes/core.server.routes.js
'use strict';

module.exports = function(app) {
  // Root-маршрутизация
  var core = require('../../app/controllers/core');
  app.route('/').get(core.index);
};

Этот маршрутизатор определяет единственный маршрут — /, — который обрабатывается функцией index основного контроллера. Заметим, что основной контроллер — это обязательный модуль CommonJS.

Оператор 'use strict'; в начале листинга 8 переводит среду выполнения JavaScript в строгий режим, менее либеральный, чем синтаксические отношения «проходит все», характерные для прежних сред исполнения JavaScript. В строгом режиме среда исполнения JavaScript обрабатывает «честные ошибки» — вроде случайного назначения переменной в качестве глобальной или попытки использовать еще не определенную переменную — как синтаксические ошибки. Строгий режим в сочетании с JSHint призван гарантировать, что синтаксические ошибки будут выявлены в процессе разработки, а не в процессе эксплуатации. (Полную гарантию безошибочного выпуска, конечно же, дает только значительное покрытие кода модульными тестами.)

Далее, рассмотрим файл app/controllers/core.server.controller.js (также часть платформы Express), показанный в листинге 9.

Листинг 9. Файл app/controllers/core.server.controller.js
'use strict';

/**
 * Зависимости модуля.
 */
exports.index = function(req, res) {
  res.render('index', {
    user: req.user || null
  });
};

Функция index принимает входящий HTTP-запрос и исходящий HTTP-ответ. Так как этот конкретный запрос ничего не требует из базы данных, модели не создаются. Шаблон index отображает ответ, а также JSON-блок переменных, которые заменяют одноименные заполнители в шаблоне.

Далее идет файл app/views/index.server.view.html, показанный в листинге 10.

Листинг 10. Файл app/views/index.server.view.html
{% extends 'layout.server.view.html' %}

{% block content %}
  <section data-ui-view></section>
{% endblock %}

Здесь мало интересного, хотя ссылка на app/views/layout.server.view.html, показанная в листинге 11, выглядит многообещающей.

Листинг 11. Файл app/views/layout.server.view.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">

<head>
  <title>{{title}}</title>

  <!-- General META -->
  <meta charset="utf-8">
  <meta http-equiv="Content-type" content="text/html;charset=UTF-8">

  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="viewport" content="width=device-width,initial-scale=1">

  <!-- Semantic META -->
  <meta name="keywords" content="{{keywords}}">
  <meta name="description" content="{{description}}">

Здесь начинает появляться HTML, который выглядит знакомым. Разделители {{}} вокруг элементов title, keywords и description определяют их как заполнители Swig, которые нужно заменить фактическими значениями. Swig — это механизм шаблонов, установленный генератором MEAN.JS Yeoman.

Если оглянуться на контроллер core в листинге 9, то можно увидеть, что единственным значением, передаваемым в этот шаблон, является значение user. Если вы подозреваете, что другие заполнители — это значения по умолчанию, определенные где-то в файле конфигурации, то вы на правильном пути.

О конфигурации и средах

Найдите в файле config/env/all.js, показанном в листинге 12, переменные title, description и keywords. (Чтобы определить, где определены эти значения, я выполнил поиск по всей структуре каталогов — вы тоже можете добавить этот метод изучения стека MEAN в свой исследовательский инструментарий.)

Листинг 12. Файл config/env/all.js
'use strict';

module.exports = {
  app: {
    title: 'Test',
    description: 'Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js',
    keywords: 'MongoDB, Express, AngularJS, Node.js'
  },
  port: process.env.PORT || 3000,
  templateEngine: 'swig',

Помимо ключевых слов, которые ожидает шаблон, в этом файле есть некоторые другие интересные значения, такие как port и templateEngine.

Переменные среды

PORT и NODE_ENV— это два примера переменных среды, которые можно установить за пределами приложения, чтобы изменить его внутреннее поведение. Например, обратите внимание на параметр port, устанавливаемый в config/env/all.js:

port: process.env.PORT || 3000,

Этот параметр предписывает приложению «установить значение внутренней переменной port, равное значению переменной среды PORT или значению по умолчанию 3000, если переменную PORT найти не удается».

Для проверки этого параметра нажмите сочетание клавиш Ctrl+C, чтобы остановить приложение. Вместо его перезагрузки «голой» командой grunt, попробуйте PORT=4000 grunt. Теперь приложение работает через порт 4000.

Приложения Node.js могут вести себя по-разному в зависимости от типа среды (среда разработки, среда эксплуатации, промежуточная среда, среда тестирования и т.д), в которой они выполняются. Как и в случае переменной PORT, Express присваивает NODE_ENV значение по умолчанию —development,— если среда исполнения не задана явно. Это объясняет сообщение, которое приложение выдает при его перезапуске:

NODE_ENV is not defined! Using default development environment

Экстернализация конфигурации времени выполнения в переменных среды — остроумный способ добавления гибкости. Такие переменные, как PORT и NODE_ENV, легко настраиваются в командной строке. Эта возможность делает тривиальным переключение значений переменных между задачами разработки и тестирования. (Конечно, им можно придать и более долговечное значение, добавив их в файл .bash_profile или установив на панели управления (в Windows®).)

Переменные среды можно рассматривать и как меру безопасности. Хранение имен пользователей, паролей и URL-адресов в переменных среды позволяет отделить их от файлов конфигурации (и, соответственно, управления версиями), где они могут быть раскрыты. К тому же этот подход упрощает развертывание общего набора файлов конфигурации на компьютерах нескольких разработчиков или на производственных машинах с возможностью для каждой машины вводить уникальные значения или учетные данные через локальные переменные среды.

PORT и NODE_ENV— не единственные переменные среды. Поставщик платформы как сервиса (PaaS) обычно предлагает несколько переменных, зависящих от сервиса.

Именованные среды

Установка отдельных переменных среды — это хорошо, но у вас может быть набор связанных переменных, которые должны изменяться одновременно. Например, нужно исключить вероятную ошибку изменения имени пользователя без соответствующего изменения пароля. К счастью, это MEAN-приложение поддерживает концепцию именованных сред. (Эта идея не уникальна для MEAN-приложений. Rails, Grails и многие другие популярные Web-технологии предлагают аналогичные возможности.)

Загляните в папку config/env в дереве каталогов из листинга 13, и вы увидите несколько файлов именованных сред.

Листинг 13. Структура каталога config
$ tree config/
config/
|--- config.js
|--- env
|�� |--- all.js
|�� |--- development.js
|�� |--- production.js
|�� |--- test.js
|--- express.js
|--- init.js
|--- passport.js
|--- strategies
    |--- facebook.js
    |--- google.js
    |--- linkedin.js
    |--- local.js
    |--- twitter.js
2 directories, 13 files

Файлы development.js, production.js и test.js в папке config/env описывают именованные среды. Файл all.js, как вы, наверное, уже догадались, содержит значения, общие для всех сред.

Чтобы понять, где все эти файлы считываются и объединяются, загляните в файл config/config.js, показанный в листинге 14.

Листинг 14. Файл config/config.js
/**
 * Зависимости модуля.
 */
var _ = require('lodash');

/**
 * Загрузка конфигураций приложения
 */
module.exports = _.extend(
  require('./env/all'),
  require('./env/' + process.env.NODE_ENV) || {}
);

Lo-dash — это модуль CommonJS, который предоставляет удобные функции для работы с массивами, объектами и JSON-структурами. В листинге 14 разработчик намеревался установить некоторые базовые значения в файле all.js и позволить переопределять их значениями development.js (или production.js, или test.js).

Файл config/env/all.js фигурировал в листинге 12. В листинге 15 показан файл config/env/development.js.

Листинг 15. Файл config/env/development.js
'use strict';

module.exports = {
  db: 'mongodb://localhost/meanjs-dev',
  app: {
    title: 'MeanJS - Development Environment'
  },

В идеале функция lodash.extend должна соединять два блока JSON, давая следующий результат:

app: {
  title: 'MeanJS - Development Environment',
  description: 'Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js',
  keywords: 'MongoDB, Express, AngularJS, Node.js'
}

К сожалению, этого не получается. Добавьте строку кода, чтобы распечатать объединенную структуру в файл config/config.js, как показано в листинге 16:

Листинг 16. Протоколирование фактических результатов слияния на консоли
/**
 * Загрузка конфигураций приложения
 */
module.exports = _.extend(
    require('./env/all'),
    require('./env/' + process.env.NODE_ENV) || {}
);
console.log(module.exports)

Перезапустите приложение, набрав PORT=4000 NODE_ENV=development grunt. Консоль покажет:

app: { title: 'MeanJS - Development Environment' }

Похоже, что структура JSON из файла config/env/development.js записалась вместо структуры в файле config/env/all.js, а не соединилась с ней. К счастью, можно внести простое изменение в файл config/config.js, чтобы получить ожидаемые результаты.

Замените в вызове функции _.extend на _.merge. Теперь после перезапуска приложения вы увидите нужные результаты:

app: 
   { title: 'MeanJS - Development Environment',
     description: 'Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js',
     keywords: 'MongoDB, Express, AngularJS, Node.js' },

Если выбрать View > Source на главной странице в браузере, то можно увидеть значения конфигурации, соединенные с HTML-шаблоном, как показано в листинге 17.

Листинг 17. Исправленные результаты слияния в HTML
<head>
  <title>MeanJS - Development Environment</title>

  <!-- Semantic META -->
  <meta name="keywords" content="MongoDB, Express, AngularJS, Node.js">
  <meta name="description" content="Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js">

Теперь, чтобы завершить тур по этому MEAN-приложению, перейдем со стороны сервера на сторону клиента.

Изучение клиентской части стека MEAN

На стороне клиента основные части главной страницы (как определено в app/views/layout.server.view.html, см. листинг 18) заполняет AngularJS.

Листинг 18. Файл app/views/layout.server.view.html
<body class="ng-cloak">
  <header data-ng-include="'/modules/core/views/header.client.view.html'" 
  class="navbar navbar-fixed-top navbar-inverse"></header>
  <section class="content">
    <section class="container">
      {% block content %}{% endblock %}
    </section>
  </section>

Напомним, что каталог app содержит серверную часть MEAN-приложения, сформированную Express. Есть два свидетельства того, что управление header на стороне клиента осуществляет AngularJS. Во-первых, атрибут HTML с ng всегда указывает на то, что им управляет AngularJS. Во-вторых, и это более категорично, в каталоге app, содержащем весь серверный код приложения, нет каталога modules. Если исключить сторону сервера в качестве возможного решения, то остается клиентский исходный код, который находится в каталоге public. Ясно, что каталог modules находится в каталоге public, как показано в листинге 19.

Листинг 19. Структура каталога public (на стороне клиента)
$ tree -L 1 public/
public/

|--- application.js
|--- config.js
|--- lib
|--- modules

Если заглянуть в каталог lib, то можно увидеть несколько сторонних библиотек:

Листинг 20. Сторонние библиотеки в каталоге public/lib
$ tree -L 1 public/lib
public/lib
|--- angular
|--- angular-animate
|--- angular-bootstrap
|--- angular-cookies
|--- angular-mocks
|--- angular-resource
|--- angular-sanitize
|--- angular-touch
|--- angular-ui-router
|--- angular-ui-utils
|--- bootstrap
|--- jquery

Напомним, что это те же библиотеки, которые указаны в bower.json.

Но если изучить каталог modules, в нем можно найти шаблон modules/core/views/header.client.view.html, указанный в файле app/views/layout.server.view.html.

Листинг 21. Шаблон modules/core/views/header.client.view.html
<div class="container" data-ng-controller="HeaderController">
  <div class="navbar-header">
    <button class="navbar-toggle" type="button" data-ng-click="toggleCollapsibleMenu()">
      <span class="sr-only">Toggle navigation</span>
      <span class="icon-bar"></span>
      <span class="icon-bar"></span>
      <span class="icon-bar"></span>
    </button>
    <a href="/#!/" class="navbar-brand">MeanJS</a>
  </div>

Если изменить значение указателя class="navbar-brand" с MeanJS на что-то другое, то после сохранения файла это изменение немедленно отразится в браузере. Но путь к основной полезной нагрузке — основному содержанию главной страницы — более замысловатый. Взгляните еще раз на файл app/views/layout.server.view.html, показанный в листинге 22.

Листинг 22. Файл app/views/layout.server.view.html
<body class="ng-cloak">
  <header data-ng-include="'/modules/core/views/header.client.view.html'" 
  class="navbar navbar-fixed-top navbar-inverse"></header>
  <section class="content">
    <section class="container">
      {% block content %}{% endblock %}
    </section>
  </section>

В разделе container находится block с именем content. Вспомните безобидный app/views/index.server.view.html:

{% extends 'layout.server.view.html' %}

{% block content %}
  <section data-ui-view></section>
{% endblock %}

Этот block content содержит пустой раздел с атрибутом data-ui-view. Он используется маршрутизатором AngularJS на стороне клиента. Рассмотрим файл public/modules/core/config/core.client.routes.js, показанный в листинге 23.

Листинг 23. Файл app/views/index.server.view.html
'use strict';

// Настройка маршрута
angular.module('core').config(['$stateProvider', '$urlRouterProvider',
  function($stateProvider, $urlRouterProvider) {
    // Перенаправление к представлению home, когда маршрут не найден
    $urlRouterProvider.otherwise('/');

    // Маршрутизация к состоянию home
    $stateProvider.
    state('home', {
      url: '/',
      templateUrl: 'modules/core/views/home.client.view.html'
    });
  }
]);

Это не очевидно, но когда URL — это /, маршрутизатор на стороне клиента вставляет в раздел app/views/index.server.view.html, содержащий атрибут data-ui-view, шаблон modules/core/views/home.client.view.html (показан в листинге 24). Содержимое этого шаблона должно соответствовать тому, что вы видите в браузере на главной странице MEAN-приложения.

Листинг 24. Шаблон modules/core/views/home.client.view.html
<section data-ng-controller="HomeController">
    <h1 class="text-center">THANK YOU FOR DOWNLOADING MEAN.JS</h1>
    <section>
        <p>
          Before you begin we recommend you read about the basic building 
          blocks that assemble a MEAN.JS application:
        </p>

Заключение

В этой статье мы, шаг за шагом, обошли целый ряд ключевых компонентов MEAN-приложения. Вы узнали, что на стороне сервера началом служат маршруты Express, которые, в свою очередь, вызывают функции контроллера Express, а те соединяют данные JSON с шаблоном Swig и возвращают их клиенту. Но на этом процесс не заканчивается. На стороне клиента его подхватывают маршруты AngularJS, которые вставляют в код главной страницы HTML-шаблоны.

В следующий раз мы подробнее рассмотрим, какую роль здесь играет MongoDB наряду с AngularJS, исследовав часть приложения, относящуюся к заметкам. Мы также увидим, каким образом проверки на стороне сервера и на стороне клиента гарантируют минимизацию неожиданного поведения в процессе эксплуатации.

Пока же попрактикуйтесь в программировании с использованием стека MEAN.


Ресурсы для скачивания


Похожие темы

  • Оригинал статьи: Mastering MEAN: Tour a MEAN application.
  • Build a real-time polls application with Node.js, Express, AngularJS, and MongoDB (developerWorks, июнь 2014 г.): познакомьтесь с проектом разработки MEAN, развернутым в облачной среде IBM Bluemixtrade™.
  • Node.js for Java developers (developerWorks, ноябрь 2011 г.): введение в Node.js и рассказ о том, почему его событийно-управляемый параллелизм вызвал интерес даже среди твердых приверженцев Java-разработки.
  • MongoDB: A NoSQL datastore with (all the right) RDBMS moves (developerWorks, сентябрь 2010 г.): о настраиваемом API MongoDB, интерактивной оболочке и поддержке как динамических запросов СУБД-стиля, так и коротких, простых расчетов MapReduce.
  • Get started with the JavaScript language (developerWorks, апрель и август 2011 г.): статья из двух частей, посвященная основам JavaScript.
  • JavaScript for Java developers (developerWorks, апрель 2011 г.): статья, объясняющая, почему JavaScript является важным инструментом современного Java-разработчика, и помогающая освоить переменные, типы, функции и классы JavaScript.
  • Introduction to LAMP technology (developerWorks, май 2005 г.): сравнение MEAN с предшествующим ему стеком.

Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Web-архитектура, Open source
ArticleID=1008857
ArticleTitle=MEAN-программирование: тур по MEAN-приложению
publish-date=06182015