Feathers — мало известный (увы!), но при этом очень мощный и удобный фреймворк для создания серверных приложений на Node.js. В его основе лежит гораздо более популярная технология Express.

Но если Express в основном ориентирована на создание web-приложений и генерацию html-кода с использованием различных шаблонизаторов, то Feathers предназначен для создания сервисов (REST, Socket.io и Primus). При этом от разработчика требуется минимум усилий и доработки кода — ведь всё уже написано до нас.

При всей мощи Feathers, пишут о нём крайне мало. Последняя публикация на Хабре о нём была в 2013 году, никаких книг статей и курсов не существует. Сам я наткнулся на него совершенно случайно, когда искал наиболее удобный вариант написания сервера для создающейся сейчас системы персональной эффективности.

От такой несправедливости мне стало горько и я решил написать этот текст о том, как с помощью Feathers за жалкие 5 минут создать действительно работающий сервер, предоставляющий сервисы для того же React.

Итак, включаем секундомер? Нет, чуть позже. Сейчас разберемся с теорией.

В Feathers есть несколько типов объектов:

  • middleware — то есть промежуточные обработчики, которые функционируют так же, как и в Express, а значит для нашего рассказа не интересны;
  • сервисы — обработчики, которые исполняются на сервере, а данные передают клиента. Для вызова сервисов используются три технологии — REST, Socket.io и Primus. Причем разработчику нет нужды самостоятельно реализовывать передачу данных. Он просто реализует несколько предопределенных методов (find, get, create, update, patch, remove, setup), а обо всем остальном позаботится фреймворк;
  • хуки — процедуры, которые автоматически вызываются до и после работы сервисов. Они позволяют проверить/исправить запрос, передаваемый сервису, и изменить данные, возвращаемые клиенту. Хуки можно писать самому, но есть стандартные хуки (например, отрезающие одну колонку из возвращаемого набора данных).

Вот теперь можно и начать работу.

Первым делом устанавливаем пакет feathers-cli:

$ npm install -g feathers-cli

Дальше нужно создать каталог для приложения и перейти в него:

$ mkdir feathers-app
$ cd feathers-app/

Теперь генерируем скелет приложения:

$ feathers generate

Вначале зададим имя и описание для нового приложения, а потом выберем какие API для доступа к нему мы хотим использовать":

? Project name TestApp
? Description Приложение для быстрой демонстрации возможностей Feathers
? What type of API are you making? (Press <space> to select, <a> to toggle all, <i> to inverse selection)
?? REST
 ? Realtime via Socket.io
 ? Realtime via Primus

Затем нужно определиться будем ли мы разрешать CORS (Cross-Origin Resource Sharing) и если да, то для каких доменов:

? Project name TestApp
? Description Приложение для быстрой демонстрации возможностей Feathers
? What type of API are you making? REST, Realtime via Socket.io
? CORS configuration (Use arrow keys)
? Enabled for all domains 
  Enabled for whitelisted domains 
  Disabled

Теперь пришла пора выбрать основной тип базы данных. Если у вас их несколько, не волнуйтесь, для каждого конкретного сервиса вы сможете указывать альтернативные типы БД:

? Project name TeatApp
? Description Приложение для быстрой демонстрации возможностей Feathers
? What type of API are you making? (Press <space> to select, <a> to toggle all, <i> to inverse selection)REST, Realtime via Socket.io
? CORS configuration Enabled for all domains
? What database do you primarily want to use? (Use arrow keys)
  Memory 
  MongoDB 
  MySQL 
  MariaDB 
? NeDB 
  PostgreSQL 
  SQLite 
  SQL Server 
  I will choose my own

Если вы не собираетесь допускать к своему backend всех подряд, то пора подумать об аутентификации. Провайдера можно написать своего или выбрать готового из списка (на самом деле провайдеров аутентификации больше, чем показано, поэтому стоит прокрутить список в поисках нужного):

? Project name TestApp
? Description Приложение для быстрой демонстрации возможностей Feathers
? What type of API are you making? REST, Realtime via Socket.io
? CORS configuration Enabled for all domains
? What database do you primarily want to use? NeDB
? What authentication providers would you like to support? (Press <space> to select, <a> to toggle all, <i> to inverse selection)
 ? local
 ? bitbucket
 ? dropbox
?? facebook
 ? github
 ? googles</spoiler>tagram

И секунд за 10 генератор создаст для нас все необходимые файлы + автоматически загрузит npm пакеты.

Структура каталогов исходного кода следующая:

  • config — конфигурационные файлы, описывающие параметры приложения (домен, порт, ключ аутентификации и т.п). default.json используется на этапе разработки, а production.json — в продакшене
  • public — этот каталог был напрямую унаследован от Express. Сюда помещаются все статические ресурсы (картинки, html-файлы), которые должны быть переданы в браузер клиента
  • test — код для тестирования backend
  • source/service — тут будут храниться «сердца» нашего приложения — сервисы, по одному на подкаталог
  • source/hooks — глобальные хуки, которые будут применяться ко всем сервисам
  • source/middleware — обычное middleware Express, например логи
  • source/app.js — основной файл приложения, который подключает сервисы, middleware, хуки, статические ресурсы и прочее. Обычно ручное изменение не требуется
  • source/index.js — просто импорт и старт app.js. В большинство случаев трогать этот файл нет смысла, но если вы пишете, например приложение Electron, то изменения вносите именно сюда

Формально приложение готово. Его даже можно запустить с помощью npm start. Из появившейся надписи мы узнаем, что сервер запущен по адресу localhost:3030. Зайдем на этот адрес — увидим пустую страничку с логотипом. Всё.

Дело в том, что мы пока что не создали ни одного сервиса. К счастью, сгенерировать не сложнее приложения.

Вводим в консоли:

$ feathers generate service

Задаем название сервиса (я выбрал contacts — для списка контактов) и выбираем откуда найдитесь сервис будет получать данные:

? What do you want to call your service? contacts
? What type of service do you need? (Use arrow keys)
  generic 
? database

В принципе можно выбрать вариант generic и самостоятельно реализовать с помощью методов сервиса (find, get, create, update, patch, remove, setup) процесс чтения/записи в базу данных. Но зачем? Ведь Feathers готов всё сделать за нас…

Итак, мы выбрали работу с базой, а в качестве базы указали уже привычный NeDB. На вопрос об аутентификации пока ответим отрицательно. Не потому что она не нужна вообще, просто обсудим её позже.

? What do you want to call your service? contacts
? What type of service do you need? database
? For which database? NeDB
? Does your service require users to be authenticated? (y/N)

В результате получаем сообщение о создании трёх файлов:

create src/services/contacts/index.js
create src/services/contacts/hooks/index.js
create test/services/contacts/index.test.js

Первый — собственно сама реализация сервиса, второй — его хуки, третий — код для тестирования работы сервиса.

Вот теперь можно с помощью npm start запустить приложение и посмотреть как оно работает. Вводим в консоли команду

$ curl 'http://localhost:3030/contacts/' -H 'Content-Type: application/json' --data-binary '{ "first_name": "John", "last_name":"Smith", "email":"jsmith@mail.emall"}'

и видим что у нас появился каталог /data (как и было задано в файле конфигурации), в нем создался файл contacts.db, а его содержимое — информация о Джоне Смите.

То есть мы получили вполне работающее приложение, не написав ни единой строчки кода!
И уложились в 5 минут.

Но это плюсы, а ведь как всегда есть и минусы:

  • сервис не позволяет валидировать передаваемые ему для сохранения данные
  • сервис возвращает данные в исходном виде, без какого-либо преобразования

Всё так. Но эти недостатки легко исправляются с помощью хуков. И во второй части мы потратим еще минут 15, чтобы превратить наш базовый сервер в весьма продвинутый.

Вторая часть здесь
Поделиться с друзьями
-->

Комментарии (27)


  1. justboris
    20.02.2017 16:43
    +2

    То есть мы получили вполне работающее приложение, не написав ни единой строчки кода!

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


    1. Andrew_I
      20.02.2017 16:47
      +1

      Этот код очень лаконичный. И очень хорошо структурированный.
      Если коллегам будет интересно, могу запланировать третью часть, в которой опишу структуру сгенеренного кода.


      1. Fen1kz
        20.02.2017 17:14

        А можно пример?


        1. Andrew_I
          20.02.2017 17:35
          +1

          Вот код нашего сервиса:

          'use strict';
          const contacts = require('./contacts');
          const authentication = require('./authentication');
          const user = require('./user');
          
          module.exports = function() {
            const app = this;
          
            app.configure(authentication);
            app.configure(user);
            app.configure(contacts);
          };
          


          А вот — файла app.js:
          'use strict';
          
          const path = require('path');
          const serveStatic = require('feathers').static;
          const favicon = require('serve-favicon');
          const compress = require('compression');
          const cors = require('cors');
          const feathers = require('feathers');
          const configuration = require('feathers-configuration');
          const hooks = require('feathers-hooks');
          const rest = require('feathers-rest');
          const bodyParser = require('body-parser');
          const socketio = require('feathers-socketio');
          const middleware = require('./middleware');
          const services = require('./services');
          
          const app = feathers();
          
          app.configure(configuration(path.join(__dirname, '..')));
          
          app.use(compress())
            .options('*', cors())
            .use(cors())
            .use(favicon( path.join(app.get('public'), 'favicon.ico') ))
            .use('/', serveStatic( app.get('public') ))
            .use(bodyParser.json())
            .use(bodyParser.urlencoded({ extended: true }))
            .configure(hooks())
            .configure(rest())
            .configure(socketio())
            .configure(services)
            .configure(middleware);
          
          module.exports = app;
          


          Мне кажется, тут всё очень просто


          1. catHD
            20.02.2017 21:03

            а чем это лучше чем Sails? sailsjs.org


            1. Andrew_I
              20.02.2017 21:04

              Я чуть ниже ответил ))


            1. baka_cirno
              20.02.2017 22:55
              +1

              Feathers живой и не завязан на тормозной Waterline. Плюс он простой как пять копеек, что сильно выделяет его на фоне подобных фреймворков.


      1. x07
        20.02.2017 18:33

        Во всяких hello world примерчиках, код всегда лаконичный и структурированный. А если сравнить сгенерированный код Feathers JS, с кодом который генерирует LoopBack 3.0, то не такой уж он и структурированный.


        1. Andrew_I
          20.02.2017 21:05

          Дело в том, что эти примерчики тем и хороши, что их трогать не надо ))
          Единственный код, который может и стоит поменять — хуки. Они отвечают и за аутентификацию и за изменение данных.


  1. k12th
    20.02.2017 18:37
    +2

    Для всякого CRUD типа админок, да и просто быстро набросать основу — очень прикольно. Давайте продолжение!


  1. winne4r
    20.02.2017 20:56

    А в чем преимущества перед тем же Sails, где своя ОРМка з адапторами, CRUD, socket и т.д.?
    Он тоже так умеет, только комьюнити у него заметно больше, и апдейты почаще…


    1. Andrew_I
      20.02.2017 21:03
      +1

      Sails позиционирует себя как MVC. Создатели же Feathers хотели, чтобы библиотека была легкой (как перышко), а код простым.
      Поэтому они и ограничились реализацией работы с сервисами.
      В этом случае (без клиентской логики) тот же ORM является совершенно излишним. С другой стороны, на клиенте могут работать React, Angular 2, AngularJS, Vue.js и т.п., которые самостоятельно разберутся что им делать с данными, которые они получили с сервера (вот примеры).
      В любом случае — каждый выбирает себе по душе. Лично меня Feathers покорил своей простотой — после 10 минут чтения документации я написал своей первый сервис. А документацию Sails мучаю уже минут 20 — и пока понимания не сложилось как это все в принципе работает.
      Опять же, не хочу обидеть поклонников Sails )), но меня больше устраивает вариант, когда сервер управляет данными, а на клиенте с ними работает React


      1. Andrew_I
        20.02.2017 21:08

        А вот как на этот вопрос отвечают создатели Feathers: https://docs.feathersjs.com/why/vs/sails.html


        1. catHD
          20.02.2017 22:39

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

          Sails так же позволяет использовать любой «клиент» для этого существует `--no-frontend`.

          Sails doesn't come with any built-in authentication support


          Явная проблема Feathers в том, что даже сравнение которое они делают уже устарело :( слишком медленно для JS community в 2017 году

          auth

          Я не хочу сказать что Feathers плох, просто хотелось бы видеть сравнение :)


          1. Andrew_I
            20.02.2017 23:25

            Сравнений гуглится куча. Типа вот этого.
            Но по сути все они субъективны и выбирать нужно сердцем ))
            Я начал писать на Feathers и понял — моё (точно так же как Angular 2 у меня «не пошел», в отличие от React), при том что у обоих библиотек куча сторонников.


  1. Strate
    20.02.2017 23:00

    Предположим, у меня есть некий ресурс. И я хочу получить список ресурсов, которые доступны только текущему пользователю. Критерий доступности может быть самый самый разный. В результате в БД должен уйти некий запрос вида owner = :user or participant = :user и так далее. Какие возможности тут предоставляет feathers?


    1. Andrew_I
      20.02.2017 23:14

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


      1. Strate
        20.02.2017 23:15

        Конечно же сервер, потому что это относится к правам доступа, клиенту это делегировать нельзя. Спасибо за поинт, буду смотреть.


        Попутно ещё вопрос: автогенерация sdk для клиента, автогенерация документации, raml/swagger описания, есть?


        1. Andrew_I
          20.02.2017 23:39

          Кстати, у меня в приложении и клиент и сервер локальные, поэтому основная логика именно на клиенте ))
          В классической системе такое конечно работать не будет

          Куча вещей реализована через пакеты. Вот, например, swagger
          А полный список тут


          1. Strate
            20.02.2017 23:50

            Эх, всё равно немного не то. Поясню чего хотелось бы.


            Например описать сущность (далее typescript):


            export class Todo {
              name: string
              owner: User
            }
            
            export class User {
              name: string
            }

            И используя описание сущности на выходе: документация, sdk, простой crud, валидация параметров и так далее. Мечты.


            1. x07
              21.02.2017 09:59

              Мечты уже давно стали реальностью. http://loopback.io/doc/en/lb3/index.html Эта штука как раз работает именно так. Описываешь сущность, а на выходе у тебя полноценный api без единой строчки кода. И клиентский sdk


              1. Andrew_I
                21.02.2017 10:58
                -1

                Проблема только в том, что сущность — штука вторичная. Наш backend обслуживает запросы фронтенда через API. Поэтому первичен именно API, а как он реализует сущность и данные — вообще говоря дело десятое.
                Вот если мы хотим сами руками данные править, тогда да, для нас важна модель.


                1. x07
                  21.02.2017 11:10

                  Что значит «дело десятое»? Если вы фронтендер то да, вообще, до лампочки как там в бекенде все организовано. Если вы пишете про бекенд фреймворк, с помощью которого вы собираетесь разработать тот самый «первичный» API, то здесь все важно, и сущность и все остальное.


                  1. Andrew_I
                    21.02.2017 11:19

                    Важно передать правильные данные через API. А как вы ими управляете — через модель или хуки, вы сами решаете.
                    И кстати, feather generate model — создание модели в feathers ))


              1. Andrew_I
                21.02.2017 11:08
                +1

                И кстати. Просмотрел всю документацию по LoopBack и нигде не нашел упоминания Socket.io
                Мне правильно кажется, что всё там заточено исключительно на REST?
                У LoopBack есть одно преимущество — его спонсирует IBM, поэтому у разработчиков есть на что допиливать функционал.
                Прочие преимущество кому-то нужны, кому-то нет. Лично мне полностью безразличны методы в моделях, зато важна связь с React. С помощью вот этой штуки я могу передавать действия Redux сервисам Feathers. Ну круто же ))



    1. Alendorff
      28.02.2017 18:35

      Зачастую используется какой-нибудь адаптер к базе данных, который назвается 'feathers-<your-db>'. Этот адаптер предоставдяет уже готовый сервис, через который осуществляются основные операции с базой.
      Пример: создаём сервис Новости. Указываете для адаптера табличку, в которую хотите складывать данные. Что-то вроде такого кода будет написано:


      let service = require('feathers-rethink');
      let news = new service({dbconnection, 'news'});
      let app = require('../app')
      
      app.use('/api/news', news); // теперь доступен REST на данный endpoint

      Но хочется какую-то авторизацию, аутентификацию, ещё что-то кастомное, а не голый доступ к бд через REST. Здесь и приходит на помощь система хуков. Пишем что-то такое:


      news = app.service('/api/news') // получили сервис, который отвечает на http запросы.

      И теперь навешиваем middleware-функции, которые должны будут выполнятся до


      let hooks = require('hooks');
      news.before(hooks.before)

      и после


      news.after(hooks.after);

      основного метода (напр. если это POST, то сервис вызывает create, который в свою очередь обращается к базе и создаёт запись). В этих хуках вы и определяете различные ограничения и прочие штуки.
      Файл hooks/index.js выглядит примерно так:


      
      exports.before = {
        all: [auth.verifyToken(),
          auth.populateUser(),
          auth.restrictToAuthenticated()],
        find: [],
        get: [],
        create: [
          globalHooks.addCreatorInfo(),
          globalHooks.sanitizeFields({fields:['newsTitle', 'newsText']})
        ],
        update: [
             function (hook) { return new Error('method not allowed');}
        ],
        patch: [],
        remove: [
          auth.restrictToOwner({ownerField: 'createdBy'}),
          commonHooks.softDelete()
        ]
      };
      
      exports.after = {
        all: [
          hooks.remove('deleted')
        ],
        find: [],
        get: [],
        create: [
          subscribeCreator(),
        ],
        update: [],
        patch: [],
        remove: []
      };

      Т.е. хук это такая функция, в которую передаётся параметр hook и который возвращает обещание вернуть этот хук. И все эти хуки выполняются в порядке их определения. Т.е. хук очень похож на обычный express middleware и определяются для каждого сервиса отдельно. Хуки имеют ряд полезных параметров:


      • это hook.params, где могут хранится всеразличные переменные, флаги и прочие костыли. Здесь же хранятся id запроса, например при запросе /api/news/123/comments/4 на endpoint /api/news/:news_id/comments hook.params.news_id будет равен 123, а hook.id = 4

      • hook.data — это данные, которые используются хуками типа before, в адаптер, который запустит операцию создания записи в бд, в качестве объекта для создания как раз и передаётся data;


      • hook.result — результат, который был получен после обращения к БД. Это или созданный (измененный) объект или список найденных ресурсов. Этот параметр доступен в хуках типа after. Но можно ещё схитрить и на этапе before объявить result. В таком случае метод адаптера не будет вызван вовсе.