Содержание


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

MEAN и CRUD-приложение UGLI с адаптивным веб-дизайном

Comments

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

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

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

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

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

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

Приложение, которое мы будем строить в остальной части этой серии статей, носит нежное название UGLI: User Group List and Information (приложение для управления списками и данными групп пользователей). Я с 2010 года управляю денверской группой пользователей HTML5 (а до этого управлял группой пользователей Java Боулдера, а до этого — денверской группой пользователей Java), поэтому не удивительно, что я — большой сторонник местных групп пользователей. Удивительно то, что для управления группами пользователей нет специализированного программного обеспечения. (Дети сапожника всегда босые, не правда ли?) Пришло время исправить это положение.

Многие группы пользователей нашли онлайновое пристанище на сайте Meetup.com. Цель этого MEAN-приложения UGLI — не заменить Meetup.com, а, скорее, глубже интегрироваться с ним. Meetup.com прекрасно справляется с большинством основных задач, которые приходится решать для успешной работы группы пользователей: регистрация новых пользователей, публикация информации о мероприятиях, обработка RSVP и т.п. Однако там все же отсутствует ряд важных функций для руководителей групп пользователей, включая управление списком докладчиков и возможность давать ссылки на презентации. UGLI заполнит эти пробелы. (Полный пример кода приведен в разделе Загрузки.)

Настройка брендинга

Прежде чем приступить к созданию приложения UGLI, настроим его брендинг. Для этого необходимо внести некоторые изменения в каталоги config и app со стороны сервера и в каталог public со стороны клиента.

Начнем с метаданных, хранящихся в каталоге config/env/all.js. Измените имя на HTML5 Denver (или название другой группы пользователей), а описание — на HTML5 Denver User Group, как показано в листинге 1.

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

module.exports = {
    app: {
        title: 'HTML5 Denver',
        description: 'HTML5 Denver User Group',
        keywords: 'MongoDB, Express, AngularJS, Node.js'
    },

Имя в файле config/env/development.js также нужно изменить, как показано в листинге 2. Как вам известно из прошлой статьи, файлы development.js и all.js во время выполнения объединяются.

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

module.exports = {
    db: 'mongodb://localhost/test-dev',
    app: {
        title: 'HTML5 Denver'
    },

Далее, измените название, отображаемое в верхнем левом углу панели навигации. Для этого отредактируйте файл public/modules/core/views/header.client.view.html. Найдите тег anchor с классом navbar-brand примерно в 9-й строке и измените его тело на HTML5 Denver, как показано в листинге 3.

Листинг 3. Файл public/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">HTML5 Denver</a>
    </div>

    <!-- ...snip... -->
</div>

Чтобы проверить свои изменения, запустите MongoDB, набрав mongod в командной строке, затем запустите приложение командой grunt. Просмотрите веб-приложение в браузере, чтобы убедиться, что в верхнем меню и в строке заголовка отображается ваше название.

В завершение изменений брендинга нужно заменить текст шаблона в файле public/modules/core/views/home.client.view.html, который отображается в основной части домашней страницы. Создайте копию с именем home.client.view.html.original, чтобы впоследствии (при необходимости) можно было вернуться к ней.

В этом файле используются возможности платформы Bootstrap, чтобы с самого начала гарантировать готовность веб-сайта к работе с мобильными устройствами. Прежде чем мы продолжим, вам будет полезно познакомиться с табличным макетом из 12 колонок, который предлагает Bootstrap.

Bootstrap и адаптивный веб-дизайн

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

Веб-страницы — не исключение. Например, зайдите на веб-сайт TIME. Вы сразу увидите макет на основе колонок. Но посмотрите, что произойдет, если значительно сузить окно браузера. По мере того как окно становится, меньше количество видимых колонок уменьшается, а когда окно становится больше — увеличивается.

Этот эффект называется адаптивным веб-дизайном, потому что веб-страница адаптируется и корректирует свой дизайн под размер экрана устройства, на котором она отображается. Современный веб-разработчик создает веб-сайты, которые органично перетекают с самых компактных портативных устройств на самые большие экраны, настольные или настенные. Создание отдельных веб-сайтов для смартфонов, планшетов, ноутбуков и т.п. — с использованием отдельных URL-адресов типа http://m.* и http://www.* — устаревшая стратегия прошлого века.

Адаптивный веб-дизайн — это не стратегия безразмерного веб-сайта; скорее, это стратегия «веб-сайта, который хорошо смотрится и ведет себя на любом устройстве». Вы не можете выбирать, с каких устройств пользователи будут посещать ваш веб-сайт, поэтому важно, чтобы дизайн обладал встроенной гибкостью и приспосабливался соответствующим образом.

Многие популярные веб-сайты (включая Facebook и Instagram) чаще посещают с мобильных устройств, а не с традиционных компьютеров. У Twitter преимущественно мобильная база пользователей. Twitter канонизировал свою стратегию адаптивного веб-дизайн и сделал ее платформой с открытым исходным кодом Bootstrap. Bootstrap — это макет из 12 колонок, который может расширяться или сжиматься с помощью используемых для определения колонок CSS-классов.

Обратите внимание на четыре колонки MongoDB, Express, AngularJS и Node.js на домашней странице примера приложения, показанной на рисунке 1.

Рисунок 1. Пример колончатого макета Bootstrap
Пример дизайна Bootstrap's columnar layout
Пример дизайна Bootstrap's columnar layout

Теперь, чтобы увидеть макет из 12 колонок Bootstrap в действии, рассмотрим исходный код public/modules/core/views/home.client.view.html, показанный в листинге 4.

Листинг 4. Файл public/modules/core/views/home.client.view.html
<div class="row">
    <div class="col-md-3">
        <h2><strong>M</strong>ongoDB</h2>
    </div>
    <div class="col-md-3">
        <h2><strong>E</strong>xpress</h2>
    </div>
    <div class="col-md-3">
        <h2><strong>A</strong>ngularJS</h2>
    </div>
    <div class="col-md-3">
        <h2><strong>N</strong>ode.js</h2>
    </div>
</div>

Если добавить к родительскому элементу div элемент class="row", то дочерние элементы div можно снабдить атрибутами class="col-xx-N", разделив их таким образом на колонки. Значение N должно быть числом от 1 до 12, а значение xx зависит от размера устройства, для которого оптимизируется макет:

  • xs для очень малых устройств (менее 768 пикселей в ширину);
  • sm для малых устройств (от 768 до 991 пикселей);
  • md для средних устройств (от 992 до 1199 пикселей);
  • lg для больших устройств (1200 и более пикселей).

Подробнее см. в разделе Grid system документации CSS Bootstrap.

Каждая колонка в листинге 4 оптимизирована для средних устройств (md), так что если вы посетить эту страницу с устройства с шириной экрана менее 992 пикселов, колонки выстроятся по вертикали, а не по горизонтали. Чтобы вызвать это изменение, сделайте окно браузера достаточно узким, как показано на рисунке 2.

Рисунок 2. Пример адаптивного веб-дизайна на мобильном устройстве
Пример адаптивного веб-дизайна на мобильном устройстве
Пример адаптивного веб-дизайна на мобильном устройстве

Теперь пора применить вновь приобретенные знания для замены шаблонного текста страницы home.client.view.html конкретным текстом, относящимся к UGLI.

Для начала загрузите логотип HTML5 размером 256 пикселей со страницы логотипов HTML5 W3C и скопируйте его в файл public/modules/core/img/brand/HTML5_Logo_256.png. Затем замените существующий HTML-код в файле public/modules/core/views/home.client.view.html исходным кодом, приведенным в листинге 5.

Листинг 5. Файл public/modules/core/views/home.client.view.html
<section data-ng-controller="HomeController">
    <div class="jumbotron text-center">
        <div class="row">
            <div class="col-md-4">
                <img alt="HTML5" class="img-responsive center-block" 
                src="modules/core/img/brand/HTML5_Logo_256.png" />
            </div>
            <div class="col-md-8">
                <h1>The HTML story is still being written.</h1> 
                <h2><em>Come hear the latest chapter at the HTML5 Denver User Group.</em></h2>
            </div>
        </div>
    </div>
</section>

При просмотре веб-сайта в широком окне браузера логотип HTML5 появляется рядом с текстом, как показано на рисунке 3.

Рисунок 3. Новая домашняя страница UGLI
Скриншот новой домашней страницы UGLI
Скриншот новой домашней страницы UGLI

Если же сделать окно браузера достаточно узким, логотип разместится над текстом, как показано на рисунке 4.

Рисунок 4. Новая домашняя страница UGLI, как она выглядит на мобильном устройстве
Новая домашняя страница UGLI, как она выглядит на мобильном устройстве
Новая домашняя страница UGLI, как она выглядит на мобильном устройстве

Видите, как легко сделать свой веб-сайт дружественным к мобильным устройствам с помощью Bootstrap? Bootstrap лежит в основе каждого нового веб-сайта, который я создаю для своих клиентов.

Теперь займемся модулем CRUD стека MEAN.

Основы CRUD

Meetup.com очень помогает мне управлять мероприятиями в группах пользователей. Но когда мероприятие прошло, аспект времени его проведения становится менее важным, чем прочитанные на нем доклады.

Другими словами, один из вопросов, волнующих пользователей данного веб-сайта: «Что будет обсуждаться на конференции?» Meetup.com превосходно справляется с этим.

Ответ на второй вопрос — «Какие доклады, независимо от даты, были посвящены стеку MEAN?» — как раз та задача, которую я собираюсь решить с помощью приложения UGLI. Для этого необходимо создать инфраструктуры CRUD вокруг нового объекта модели с именем Talk. К счастью, есть генератор Yeoman, который поможет в создании этой инфраструктуры.

Введите команду yo meanjs:crud-module talks из корневого каталога приложения. В ответ на соответствующие запросы:

  1. Выделите все четыре дополнительных папки (css, img, directives и filters).
  2. Ответьте Yes, чтобы добавить в меню ссылки на модуль CRUD.
  3. Примите значение по умолчанию (topbar), когда генератор попросит присвоить меню имя.

Интерактивная последовательность командной строки приведена в листинге 6.

Листинг 6. Создание нового модуля CRUD с помощью генератора Yeoman
$ yo meanjs:crud-module talks
[?] Which supplemental folders would you like to include in your angular module? 
css, img, directives, filters
[?] Would you like to add the CRUD module links to a menu? Yes
[?] What is your menu identifier? topbar
   create app/controllers/talks.server.controller.js
   create app/models/talk.server.model.js
   create app/routes/talks.server.routes.js
   create app/tests/talk.server.model.test.js
   create public/modules/talks/config/talks.client.routes.js
   create public/modules/talks/controllers/talks.client.controller.js
   create public/modules/talks/services/talks.client.service.js
   create public/modules/talks/tests/talks.client.controller.test.js
   create public/modules/talks/config/talks.client.config.js
   create public/modules/talks/views/create-talk.client.view.html
   create public/modules/talks/views/edit-talk.client.view.html
   create public/modules/talks/views/list-talks.client.view.html
   create public/modules/talks/views/view-talk.client.view.html
   create public/modules/talks/talks.client.module.js

В листинге 6 обратите внимание на то, что генератор создает нужную инфраструктуру на стороне сервера (которая хранится в каталоге app): маршруты, контроллер, модель и модульный тест. Он также создает все артефакты на стороне клиента — в каталоге public/modules/talks.

Чуть позже мы добавим в объект Talk некоторые настраиваемые поля. Но перед этим посмотрите, что получилось по умолчанию, зайдя на веб-сайт из своего браузера.

Нажмите на ссылку Signin в верхнем правом углу и введите имя пользователя и пароль, которые вы создали раньше, или нажмите кнопку Signup и создайте новый набор учетных данных.

Вы увидите меню Talks в верхнем левом углу. Выберите из этого меню New Talk, чтобы открыть HTML-форму с единственным полем Name, как показано на рисунке 5.

Рисунок 5. Форма New Talk до настройки
Форма New Talk до настройки
Форма New Talk до настройки

Это хорошее начало, но чтобы ввести все атрибуты Talk, одного текстового поля недостаточно.

Добавление новых полей для хранения данных

Чтобы добавить новые поля в форму Talk, необходимо изменить шесть файлов — четыре для отображения и два для хранения:

  • app/models/talk.server.model.js
  • public/modules/controllers/talks.client.controller.js
  • public/modules/talks/views/create-talk.client.view.html
  • public/modules/talks/views/edit-talk.client.view.html
  • public/modules/talks/views/view-talk.client.view.html
  • public/modules/talks/views/list-talks.client.view.html

Сначала займемся хранением. Половина решения находится на стороне сервера, а вторая половина — на стороне клиента.

Модель на стороне сервера (определенная в файле app/models/talk.server.model.js) — это источник истины нашего приложения. В ней будут указаны имена полей, типы предоставляемых данных, правила проверки и т.п.

Контроллер на стороне клиента (определенный в файле public/modules/controllers/talks.client.controller.js) собирает данные от пользователя и передает их на сервер посредством HTTP-запросов. Этот же контроллер получает данные JSON и передает их представлениям для отображения.

В этой архитектуре интересно то, что объект модели никогда не покидает сервер. Вместо этого он материализуется из данных, отправленных клиентом, и в HTTP-ответе сериализуется в формат JSON.

В этом приложении два контроллера — один на стороне сервера, другой на стороне клиента, — но интерес представляет только контроллер на стороне клиента. Контроллер на стороне сервера просто заливает входящие данные JSON в объект модели, так что при добавлении к модели дополнительных полей в контроллер на стороне сервера не нужно вносить никаких изменений. Для размещения новых полей в контроллере на стороне клиента требуется некоторая перестройка.

Откройте файл app/models/talk.server.model.js и добавьте новые поля к модели на стороне сервера, как показано в листинге 7. Вы видите, что ожидаемое поле name (см. рисунок 5) определено вместе с двумя другими полями метаданных: created и user.

Листинг 7. Файл app/models/talk.server.model.js
/**
 * Схема Talk
 */
var TalkSchema = new Schema({
    name: {
        type: String,
        default: '',
        required: 'Please fill Talk name',
        trim: true
    },
    created: {
        type: Date,
        default: Date.now
    },
    user: {
        type: Schema.ObjectId,
        ref: 'User'
    }
});

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

Добавьте новые поля description, presenter и slidesUrl, как показано в листинге 8. В данном случае поля description и presenter— обязательные. Поле slidesUrl факультативно.

Листинг 8. Файл app/models/talk.server.model.js
/**
 * Схема Talk
 */
var TalkSchema = new Schema({
    name: {

        type: String,
        default: '',
        required: 'Please fill Talk name',
        trim: true
    },
    description: {
        type: String,
        default: '',
        required: 'Please fill Talk description',
        trim: true
    },  
    presenter: {
        type: String,
        default: '',
        required: 'Please fill Talk presenter',
        trim: true
    },
    slidesUrl: {
        type: String,
        default: '',
        trim: true
    },
    created: {
        type: Date,
        default: Date.now
    },
    user: {
        type: Schema.ObjectId,
        ref: 'User'
    }
});

Теперь механизм на стороне сервера готов к приему новых полей. Переходим к контроллеру на стороне клиента. Откройте файл public/modules/controllers/talks.client.controller.js и добавьте новые поля, как показано в листинге 9.

Листинг 9. Файл public/modules/controllers/talks.client.controller.js
// Создание нового объекта Talk
$scope.create = function() {
    // Создание нового объекта Talk
    var talk = new Talks ({
        name: this.name,
        description: this.description,
        presenter: this.presenter,
        slidesUrl: this.slidesUrl
    });

    // Перенаправление после сохранения
    talk.$save(function(response) {
        $location.path('talks/' + response._id);
    }, function(errorResponse) {
        $scope.error = errorResponse.data.message;
    });

    // Очистка полей формы
    this.name = '';
    this.description = '';
    this.presenter = '';
    this.slidesUrl = '';
};

Функция $scope.create объединяет поля формы в объект JSON, чтобы отправить их на сервер для сохранения. После добавления соответствующих полей из модели в контроллер вы получаете механизм персистенции.

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

Добавление средств отображения новых полей

Загляните в папку public/modules/talks/views/. С жизненным циклом CRUD связаны четыре файла:

  • create-talk.client.view.html
  • edit-talk.client.view.html
  • view-talk.client.view.html
  • list-talks.client.view.html

Откройте файл create-talk.client.view.html, как показано в листинге 10.

Листинг 10. Сгенерированный файл create-talk.client.view.html
<section data-ng-controller="TalksController">
  <div class="page-header">
    <h1>New Talk</h1>
  </div>
  <div class="col-md-12">
    <form class="form-horizontal" data-ng-submit="create()" novalidate>
      <fieldset>
        <div class="form-group">
          <label class="control-label" for="name">Name</label>
          <div class="controls">
            <input type="text" data-ng-model="name" id="name" class="form-control" 
            placeholder="Name" required>
          </div>
        </div>
        <div class="form-group">
          <input type="submit" class="btn btn-default">
        </div>
        <div data-ng-show="error" class="text-danger">
          <strong data-ng-bind="error"></strong>
        </div>
      </fieldset>
    </form>
  </div>
</section>

Скопируйте блок кода, относящийся к полю Name, три раза для поддержки полей Description, Presenter и slidesUrl, как показано в листинге 11. Обратите внимание, что я сделал поле Description полем типа textarea, а не простым текстовым полем. Кроме того, я убрал атрибут required из поля slidesUrl и заменил input type в элементе text на url.

Листинг 11. Отредактированный файл create-talk.client.view.html
<section data-ng-controller="TalksController">
  <div class="page-header">
    <h1>New Talk</h1>
  </div>
  <div class="col-md-12">
    <form class="form-horizontal" data-ng-submit="create()" novalidate>
      <fieldset>
        <div class="form-group">
          <label class="control-label" for="name">Name</label>
          <div class="controls">
            <input type="text" data-ng-model="name" id="name" class="form-control" 
            placeholder="Name" required>
          </div>
        </div>
        <div class="form-group">
          <label class="control-label" for="description">Description</label>
          <div class="controls">
            <textarea data-ng-model="description" id="description" class="form-control" 
            placeholder="Description" required></textarea>
          </div>
        </div>
        <div class="form-group">
          <label class="control-label" for="presenter">Presenter</label>
          <div class="controls">
            <input type="text" data-ng-model="presenter" id="presenter" class="form-control" 
            placeholder="Presenter" required>
          </div>
        </div>
        <div class="form-group">
          <label class="control-label" for="slidesUrl">Slides</label>
          <div class="controls">
            <input type="url" data-ng-model="slidesUrl" id="slidesUrl" class="form-control" 
            placeholder="Slides Url">
          </div>
        </div>                        
        <div class="form-group">
          <input type="submit" class="btn btn-default">
        </div>
        <div data-ng-show="error" class="text-danger">
          <strong data-ng-bind="error"></strong>
        </div>
      </fieldset>
    </form>
  </div>
</section>

В веб-браузере измененная страница New Talk выглядит, как показано на рисунке 6.

Рисунок 6. Форма New Talk после настройки
Форма New Talk после настройки
Форма New Talk после настройки

Если вы удовлетворены изменениями, откройте файл edit-talk.client.view.html и внесите соответствующие изменения туда, как показано в листинге 12.

Листинг 12. Файл edit-talk.client.view.html
<div class="col-md-12">
    <form class="form-horizontal" data-ng-submit="update()" novalidate>
      <fieldset>
        <div class="form-group">
          <label class="control-label" for="name">Name</label>
          <div class="controls">
            <input type="text" data-ng-model="talk.name" id="name" class="form-control" 
            placeholder="Name" required>
          </div>
        </div>
        <div class="form-group">
          <label class="control-label" for="description">Description</label>
          <div class="controls">
            <textarea data-ng-model="talk.description" id="description" class="form-control" 
            placeholder="Description" required></textarea>
          </div>
        </div>
        <div class="form-group">
          <label class="control-label" for="presenter">Presenter</label>
          <div class="controls">
            <input type="text" data-ng-model="talk.presenter" id="name" class="form-control" 
            placeholder="Presenter" required>
          </div>
        </div>
        <div class="form-group">
          <label class="control-label" for="slidesUrl">Slides</label>
          <div class="controls">
            <input type="url" data-ng-model="talk.slidesUrl" id="name" class="form-control" 
            placeholder="Slides Url">
          </div>
        </div>
        <div class="form-group">
          <input type="submit" value="Update" class="btn btn-default">
        </div>
        <div data-ng-show="error" class="text-danger">
          <strong data-ng-bind="error"></strong>
        </div>
      </fieldset>
    </form>
</div>

Обратите внимание, что код HTML для редактирования несколько отличается от варианта для формы создания, которую мы изменили выше. При редактировании объект Talk уже есть, поэтому атрибуты data-ng-model относятся к полям с полными именами, например, talk.name, вместо name. Просмотрите изменения в веб-браузере, как показано на рисунке 7.

Рисунок 7. Форма Edit Talk после настройки
Форма Edit Talk после настройки
Форма Edit Talk после настройки

Страница view-talk.client.view.html представляет собой неизменное представление объекта. Это представление пользователи видят после сохранения новой формы Talk, обновления существующей формы Talk или выбора формы Talk из списка страниц. Внесите изменения, показанные в листинге 13.

Листинг 13. Файл edit-talk.client.view.html
<div class="page-header">
  <h1 data-ng-bind="talk.name"></h1>
  <h2><em>by {{talk.presenter}} 
    <span ng-if="talk.slidesUrl !== '' ">[<a href="{{talk.slidesUrl}}">slides</a>]</span></em></h2>
  <p>{{talk.description}}</p>              
</div>

Напомним, что поле slidesUrl является факультативным. На странице просмотра используется директива ng-if для условного отображения поля, если оно заполнено. Проверьте это, просмотрев страницу в браузере, как показано на рисунке 8.

Рисунок 8. Форма View Talk после настройки
Форма View Talk после настройки
Форма View Talk после настройки

Последнее представление, которое нужно настроить, это представление списка. Откройте файл list-talks.client.view.html и внесите изменения, показанные в листинге 14.

Листинг 14. Файл list-talks.client.view.html
<div class="list-group">
    <a data-ng-repeat="talk in talks" data-ng-href="#!/talks/{{talk._id}}" class="list-group-item">
    <h4 class="list-group-item-heading" data-ng-bind="talk.name"></h4>
        <p><em>by {{talk.presenter}}</em></p>
    </a>
</div>

Обратите внимание, что здесь используется директива data-ng-repeat для отображения каждого доклада из списка докладов, переданного с сервера. Просмотрите результаты в веб-браузере, как показано на рисунке 9.

Рисунок 9. Форма List Talks после настройки
Форма List Talks после настройки
Форма List Talks после настройки

Заключение

Теперь вы имеете хорошее представление о том, как взаимодействуют различные части стека MEAN. Вы использовали возможности адаптивного веб-дизайна Bootstrap, чтобы сайт хорошо выглядел на любых устройствах, а не только на традиционных компьютерах со 101 клавишей и мышью. И вы испытали всю мощь и удобство генератора Yeoman при добавлении в свое приложение нового модуля CRUD. Генератор помещает все исходные артефакты в нужные каталоги, оставляя вам лишь несложную задачу их настройки.

В следующем выпуске мы покажем, как легко встроить в приложение данные из удаленных источников. В частности, мы начнем со сбора данных о мероприятиях прямо из Meetup.com через REST API Meetup. Пока же попрактикуйтесь в программировании с использованием стека MEAN.


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


Похожие темы


Комментарии

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

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