Персональный блог Игоря Антонова aka "spider_net"

Sails.js – фреймворк для ленивых. Пример простого баг трекера на sails.js


Рубрика: sails.js -> JavaScript -> Программирование -> Статьи
Метки: | | | | |
Просмотров: 15998
Sails.js – фреймворк для ленивых. Пример простого баг трекера на sails.js

Мода на серверные JavaScript фреймворки только начинает зарождаться и пока здесь трудно выделить явного лидера. Одни гибко конфигурируются, другие хвастаются изящной архитектурой и примочками на все случаи жизни. А еще есть настоящие трудоголики – фреймворки, готовые взять на себя рутинную работу. Sails.js – один из таких трудяг.

Немного предыстории

Знакомство с платформой node.js открывает сумасшедшие перспективы, но стоит взяться за разработку реального приложения, начинаешь тосковать по знакомым инструментам и привычным подходам. Фреймворков под node.js было не много. Из самых полярных мне больше всего понравились Meteor и Express. Оба классные инструменты, но у меня как-то не прижились. Всегда хотелось найти что-то более простое и работающее из коробки.

Почему я выбрал sails.js

Скажите, вы пробовали Ruby On Rails? Если да, то наверняка успели восхититься архитектурой и подходом к разработке приложений. Так вот, разработчики sails.js тоже пропитались духом именитого фреймворка и с этим настроением попытались создать нечто подобное, но только на JavaScript. Результатом стал достаточно легкий, заряженный на быстрое создание сервисов фреймворк с морским названием.

Мне всегда нравились простые инструменты и постепенное наращивание сложности. «Sails.js» четко следует этому принципу: сначала рабочий инструмент, потом расширение. Именно поэтому порог вхождения в sails.js (по сравнению с альтернативами) относительно низкий. Для старта есть все необходимое: сообщество и официальная документация. При написании первых примеров мне не приходилось лезть в исходный код, все проблемы удавалось решить с помощью документация и сообщества.

Вторым железным плюсом sails.js для меня стал – REST API. И что в этом такого? Другие разве не умеют делать REST? Другие позволяют создавать REST, а sails.js генерирует самостоятельно на основании моделей. Описываем модель и сразу получаем REST в виде CRUD, поиска и т.д. Добавляем на фронт angular.js и прототип типового приложения готов. Разве не прекрасно?

Теперь немного залезем под капом. Что мы видим? Классический паттерн MVC, мощный ORM в виде Waterline, поддержка популярных СУБД и никаких намеков на клиентские предпочтения. Неважно, поклонник вы геройского angular.js или специфичного knockout.js. Выбирайте, что душе угодно, sails.js ничего не навязывает.

Ну и последнее, вы любите grunt? Я вот как-то к нему привык при разработке web-проектов и рад, что в sails.js он уже настроен и готов к работе. Как говориться мелочь, а приятно. Вот все такие, небольшие, казалось бы, мелочи, в итоге склонили меня к более детальному знакомству с sails.js.

Рисунок 1. Официальный сайт sails.js

Sails.js в действии

По-хорошему, стоило бы добавить огоньку во фронт, но в рамках объема статьи все уместить будет проблематично. Тем более применение связки «sails.js + angular.js + require.js» для построения одностраничного приложения я уже демонстрировал. С подробностями можно ознакомиться в моей статье [1].

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

Каждая заявка должна содержать набор стандартный информации: заголовок, предполагаемая дата исполнения, исполнитель и текущий статус. Все полученные от пользователей данные будем сохранять в СУБД. Для удобства разработки мы воспользуемся услугами файловой базы, а перед переходом в рабочий режим мигрируем в ламповый MySQL.

Основная цель состоит в знакомстве с sails.js, в связи с этим, этап подготовки верстки опустим. Для ускорения этого процесса, мы воспользуемся готовым шаблоном «SB-admin» [2], созданным на базе CSS-фреймворка Bootstrap.

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

Рисунок 2. Готовое web-приложение на sails.js

Установка sails.js

Помимо sails.js нам в обязательно порядке потребуется платформа node.js. Она доступа под все популярные операционные системы, поэтому выбор системного окружения зависит сугубо от личных предпочтений. Кстати, под Windows node.js работает ничуть не хуже, чем в Linux, поэтому если операционная система от Microsoft вам ближе, то никаких проблем со сборкой проекта не возникнет.

Будем считать, что с установкой всех необходимых компонент вы справились, пора переходить к установке sails.js. Проще всего установить sails.js глобально (см. рисунок 3):

npm install -g sails

После установке обязательно проверяем версию sails.js командой:

sails -v

Рисунок 3. Установка sails.js с помощью NPM

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

Создаем проект

В состав sails.js входят различные генераторы кода, автоматизирующие типичные операции. Например, создание заготовки нового проекта решается вызовом генератора new:

sails new bugTracker 
cd bugTracker

Готовая структура проекта изображена на рисунке 4.

Рисунок 4. Структура нового проекта

Генерируем API

Автоматическое формирование Resftul API – одна из сильнейших сторон фреймворка sails.js. Только представьте, для создания типичного CRUD, разработчику не требуется писать не одной строчки кода. Достаточно заготовить болванку модели и все, CRUD будет работать автоматом. Через несколько абзацев мы увидим этот механизм в действии, а пока определимся с необходимыми моделями.

Для решения поставленной задачи нам потребуется три модели: Status (статус заявки), User (исполнитель) и Task (заявка). В целом содержимое моделей будет похожим, поэтому для экономии места мы рассмотрим внутренности модели Status и Task. Модель User вы сможете написать самостоятельно или скопировать из моих исходников.

Начнем с модели Status. Создадим для нее заготовку:

sails generate api status

Наряду с моделью сразу получаем необходимый функционал для работы с данными (CRUD) и заготовку для контроллера. Да! Вот так просто мы получили API.

Все создаваемые модели помещаются в каталог api/models. Откроем в нем нашу модель (Status.js) и опишем необходимые поля (атрибуты). Нам потребуется три поля – title (заголовок), name (системное имя) и description (описание). В объекте attributes определяем все эти сущности:

title: {
          type: 'string',
          required: true,
          maxLength: 100
      },
      
      name: {
          type: 'string',
          required: true,
          maxLength: 30
      },
      
      description: {
          type: 'string',
          required: false,
          maxLength: 1024
      }

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

При описании полей модели нам обязательно требуется указать тип поля (type). Остальные атрибуты указываются при необходимости. На этом описание модели Status завершено. По идее сейчас мы должны начать двигаться дальше, но мы прервемся и попробуем протестировать хваленный REST в действии.

Для этого перейдите в консоль и запустите проект командой: sails lift (см. рисунок 5). Во время запуска будет поднят встроенный web-сервер и вы сразу сможете протестировать приложение в работе. Откройте в браузере страницу http://localhost:1337/status/ и приложение автоматически вернет пустой массив. Мы по факту ничего не создали, а фреймворка нам уже построил удобный интерфейс для взаимодействия с серверной частью.

Рисунок 5. Запуск проекта

Ok, теперь попробуем добавить данные. Формируем URL следующего вида http://localhost:1337/status/create?title=test&name=new2&description=test, отправляем на сервер и получаем новую запись. Удобно? Получается, для создания прототипа SAP, останется только написать простенький front, а остальное за нас сделает sails.js (см. рисунок 6).

Рисунок 6. REST API в действии

Хорошо, первый шаг пройден. Теперь надо подготовить модель User и Task (не забудьте их сгенерировать). Последняя модель будет связана с User и Status. В модели Task нам потребуются следующие поля: title (заголовок), description (описание), endData (дата дедлайна), performer (исполнитель), status (статус).

Из всех перечисленных полей больше всего нас интересуют performer и статус. Дело в том, что при описании этих полей нам необходимо указать связь с одноименными моделями. Если этого не сделать, то ORM Waterline нас не поймет, и возможности работать с данными из базы в объектном стиле у нас не будет. Отношения к моделям описываем так (api/models/Task.js):

performer: {
          required: true,
          model: 'user'
      },
      
      status: {
          required: true,
          model: 'status'
      },

Waterline ORM не ограничивает нас «в связях» и позволяет указывает различные варианты отношений между моделями (многие ко многим, один к одному и т.д.). В нашем примере мы используется вариант односторонней связи - у одной заявки может быть один исполнитель и один статус. С другими вариантами вы можете ознакомиться в документации.

На этом вопрос с моделями можно считать закрытым. Переходим к следующей составляющей MVC – контроллерам.

Контроллеры

Болванки для контроллеров к каждой модели sails.js у нас уже есть (api/controllers). Нам остается описать действия на запросы пользователей. Код контроллеров получился достаточно объемным, поэтому разберём лишь интересные моменты, а полную версию вы всегда сможете посмотреть в прилагаемых исходниках.

Для начала рассмотрим структуру контроллера. Каждое действие контроллера – это стандартный метод JS-объекта. В качестве параметров такому метода передаются два объекта - req (запрос), res (ответ). Через них мы сможем взаимодействовать получать и отправлять запросы клиенту.

В листинге 1, 2, 3 приведен текст заготовок для методов добавления, редактирования и удаления записей.

Листинг 1. Добавляем запись в БД

addStatus: function (req, res) {
        
        var data = {
            title       : req.param('title'),
            name        : req.param('name'),
            description : req.param('description'),
        }
        
        Status.create(data).exec(function (err, status) {
                                                
            if (err) return res.send(500);
            
            res.redirect('/status/');
            
        });
    }

Листинг 2. Удаляем запись из БД

deleteStatus: function (req, res) {
        
        var statusId = req.param('id');
        
        Status.destroy(statusId).exec(function (err) {
            
            if (err) return res.send(500);
                        
            res.redirect('/status');
        });
    }

Листинг 3. Редактирование записи

updateStatus: function (req, res) {
        
        var statusId = req.param('id');
        
        var updatedData = {
            title       : req.param('title'),
            name        : req.param('name'),
            description : req.param('description'),
        };
        
        Status.update(statusId, updatedData).exec(function (err) { 
            res.redirect('/status');
        });
    },

Перед созданием новой записи в БД мы должны получить данные от клиента. Это мы можем сделать с помощью объекта req (передается параметром к методу контроллера). Доступ к полям осуществляется с помощью метода param(). Например, чтобы получить значение из поля title отправленной формы, достаточно указать – req.param('title').

Для добавления записи в БД мы собираем их в один объект и пользуемся услугами ORM – вызываем метод create() определенной модели. Не забываем, что все работа происходит в асинхронном режиме, поэтому для определения факта завершения создания/выборки данных определяем колбэк. Если никаких ошибок не возникло, то перенаправляем запроса на определенный метод контроллера.

Подобный подход используется во всех контроллерах нашего демонстрационного примера, поэтому не будем заострять на нем внимание. Процесс редактирования выполняется похожим образом (см. листинг 3). Только помимо данных, нам необходимо еще получить идентификатор записи, которую собираемся обновлять. Обновление записи 0 в БД выполняется с помощью метода update() определенной модели.

С удалением ситуация опять же похожая. Для удаления записи из БД необходимо знать лишь ее ID. Передаем его методу модели destroy() и получаем желаемый результат.

Если для простых моделей (без ссылочных полей) код контроллеров чертовски похож, то с сущностью «Заявка» все несколько иначе (см. листинги 4, 5). Первая проблема, которую нам предстоит решить – получение данных из связанных моделей.

Например, в контроллере Task описан метод действия index (листинг 4). Он должен возвращать клиенту список всех зарегистрированных заявок. Поскольку в модели Task есть ссылочные поля, то при выборке необходимо попросить Waterline подтянуть данных из связанных таблиц. Для этого используем метод .populate(), принимающий в качестве параметра имя модели.

Листинг 4. Код метода index контроллера Task

Task.find()
            .populate('performer')
            .populate('status')
            .sort('id DESC')
            .exec(function (err, tasks) {
                
                if (err) return res.send(500);
            
                res.view({ tasks : tasks}); 
            
            });

Листинг 5. Код метода create контроллера Task

var data = {};       
        
        User.find()            
            .then(function(users) {            
                data.performers = users;                
                Status.find()                
                    .then(function(statuses) {                    
                        data.statuses = statuses;                    
                        res.view({data : data});
                });
        });

Немного нестандартная ситуация возникает в пятом листинге. Метод действия create() вызывается при запросе клиентом страницы для создания новой заявки. Поскольку у нас есть подготовленный список статусов и исполнителей, то необходимо передать его клиенту. Для этого организуем две выборки. Задача не представляет сложности, но тут главное помнить об асинхронности. Выборку данных обязательно организовываем в цепочку (метод .then()), иначе на выходе получим не то, что ожидаем.

Маршрутизация

Мы знаем, что данные можно отправлять методом GET и POST. Как наши контроллеры будут определять метод действия для выполнения? Ведь одни данные могут прийти POST, а другие GET. Для этого в sails.js есть маршрутизация. Мы определяем шаблон url, протокол и метод действия определенного контроллера. На мой взгляд, это не совсем удобно, но в текущей версии роуты придется прописывать вручную. Настройка маршрутизации выполняется в файле config/routes.js.

Любой маршрут строится по следующей схеме:

<Метод отправки данных> <псевдо путь> [параметры] : {
	controller: <Имя контроллера>
	action: <Метод действия>
}

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

'post /task/create': {
	controller: 'task',
	action: 'addTask'
}

Именно этот путь (/task/create) мы должны прописать для формы создания новой заявки. Рассмотрим еще один пример – удаление заявки. Тут форма отдельная не нужна, поэтому достаточно описать контроллера для метода передачи GET и удалять записи можно просто по URL - /task/delete/2 (удаление записи с идентификатором равным двум).

'get /task/delete/:id': {
	controller: 'task',
	action: 'deleteTask'
}

Представления

sails.js может работать с любым шаблонизатором (Jade, Haml), но из коробки с ним поставляется лишь EJS. Многим он не нравится, но я успел к нему прикипеть. Тем более, начиная с версии 0.9, появилась возможность создавать мастер-макеты. Например, оформляем повторяющий код верстки в мастер-макет и используем его вместе с остальными представлениями. В коде это выглядит так:

<!—Верстка -->
<%- body %>
<! -- Верстка / -->

Мастер-макет (views/layout.ejs) уже настроен на Grunt, поэтому все стили и JavaScript файлы будут подключать автоматически во время сборки проекта.

Теперь поговорим немного о поиске представлений. В момент вызова представления, контроллер будет искать соответствующий ejs файлик в директории views/имя_контроллера/имя_метода.ejs. Например, если требуется подгтовить представление для метода create, то контроллер обратиться по адресу views/task/create.ejs.

А каким образом можно передавать данные из контроллера в представление? Передача управления представлению осуществляется с помощью метода view() объекта res (вспоминанием про параметры к методам действия контроллера). Параметром к методу view() может быть произвольный объект. Вот именно к этому объекту мы сможем обратиться во время обработки представления. Например, у нас есть массив tasks, который необходимо вывести в представлении:

//Возвращаем представление
res.view( { tasks: tasks });
<!-- Получаем данные объекта в представлении -->
<% _.each(tasks, function (task) { %>
                                    <tr>
                                        <td><%= task.id %></td>                                        
                                        <td><a href="/task/view/<%= task.id %>"><%= task.title %></a></td>
                                        <td><%= task.endDate %></td>
                                        <td><%= task.performer.userName %></td>                                 
                                        <td><a class="btn btn-warning" href="/task/delete/<%= task.id %>">Delete</a></td>                                        
                                    </tr>
                                    <%}) %>

Шлифуем клиентскую часть

Все клиентские ресурсы (изображения, скрипты, стили и т.д.) должны располагаться в директории assets. В ней уже подготовлены одноименные папки (styles, js и т.д.). Все что мы помещаем в эту директорию, будет доступно по адресу: http://your-server.com/js/yourscript.js. С точки зрения клиента, все ресурсы хранятся в корневой директории сервера.

Думаю, с этим ясно. Теперь вспомним про Grunt. Вся рутина должна быть автоматизирована и Grunt нам в этом поможет. Из коробки Grunt настроен на подключение сценариев, стилей из соответствующих директорий.

В демонстрационном примере используется LESS версия bootstrap. Загруженные файлы со стилями помещаются в одноименную папку (bootstrap) и при сборке будут автоматически скомпилированы в css. По умолчанию less компилируется в файл importer.css. Для этого в файлике imporeter.less необходимо прописать пути к файлам для компиляции:

@import 'bootstrap/bootstrap';

Всегда есть вероятности необходимости управления порядком подключения JavaScript файлов. По умолчанию grunt делает это на свое усмотрение, но мы всегда можем жестко задать определенный порядок. Для этого достаточно внести правки в файл /tasks/pipeline.js. Для нашего примера я установил следующий порядок подключения сценариев:

'js/dependencies/sails.io.js',
'js/dependencies/jquery-2.1.1.min.js',
'js/front.js',
'js/dependencies/bootstrap.min.js',

Меняем СУБД

Для перехода с файловой базы данных на MySQL требуется внести буквально пару строчек в конфигурационный файл. Перед этим в обязательном порядке требуется установить адаптер для работы с mysql – «sails-mysql». Адаптер устанавливается при помощи менеджера пакетов npm:

npm install sails-mysql

После установки открываем файл config/connections.js, находим объект someMysqlServer и указываем в нем настройки для подключения к СУБД. Сохраняем изменения и в файле config/models.js указываем о своем желании работать с сервером MySQL:

connection: someMysqlServer

После сборки проекта, в качестве хранилища будет использоваться MySQL.

Рисунок 7. Программа в действии

Каждому проекту по фреймворку

Нельзя сказать, что sails.js совершил революцию в области серверной разработке на JavaScript. Мы получили качественный JavaScript-фреймворк с оригинальными идеями под капотом. Разработчик проекта проделал большую работу и созданный им уровень абстракции, упростит многим программисту путь на сторону сервера.

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

Я не случайно использовал в названии статьи фразу «фреймворк для ленивых». Из коробки Sails.js действительно автоматизирует многие вещи и избавляет разработчика от изучения десятков страниц документации для запуска первого приложения (в отличии от того же derby.js.

В заключении хочу добавить. Если вы только перешли на node.js и ищите хороший инструмент для создания web-приложений с низким порогом входа, то обязательно посмотрите в сторону sails.js.

Что почитать по теме

  • [3] — мои заметки о фреймворке sails.js. Среди них вы найдете подробную статью по использованию связки технологий: sails.js + angular.js + require.js.
  • [4] - первая и единственная книга о sails.js (на английском). В настоящее время книга находится в стадии написания, но уже можно приобрести готовые главы (MEAP). Один из авторов книги – создатель фреймворка.
  • [5] - прекрасная серия скринкастов по практическому применению sails.js. Автор по шагам рассматривает разработку одностраничного приложения с использованием sails.js и angular.js.Единственный минус – весь контент на английском.
  • [6] – документация по проекту ORM Waterline.

А в продакшн можно?

Если вы дочитали до этого места, то sails.js вас наверняка заинтересовал и возникает резонный вопрос: «А можно использовать sails.js для разработки реальных проектов?». Мнений тут может быть сколько угодно, но в нашей компании начали применять фреймворк в продакшне еще с версии 0.10. Мы перенесли на него проект с ASP .NET MVC, который успешно работает уже год.

Список ссылок:

Исходники примера

Статья опубликована в журнале "Системный администратор" (http://samag.ru/). Июнь 2015 г.

Ссылка на журнал: http://samag.ru/archive/more/151

Оставьте комментарий!
comments powered by HyperComments