СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

| сохранено

H Feathers JS — как создать backend для своего приложения всего за 5 минут в черновиках


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, чтобы превратить наш базовый сервер в весьма продвинутый.

...To be continued!
+32
~12700

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

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

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

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

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

+1
+2 –1
Andrew_I ,  
Вот код нашего сервиса:
'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;


Мне кажется, тут всё очень просто
0
catHD ,  
а чем это лучше чем Sails? sailsjs.org
0
Andrew_I ,  
Я чуть ниже ответил ))
+1
baka_cirno ,  
Feathers живой и не завязан на тормозной Waterline. Плюс он простой как пять копеек, что сильно выделяет его на фоне подобных фреймворков.
0
x07 ,  
Во всяких hello world примерчиках, код всегда лаконичный и структурированный. А если сравнить сгенерированный код Feathers JS, с кодом который генерирует LoopBack 3.0, то не такой уж он и структурированный.
0
Andrew_I ,  
Дело в том, что эти примерчики тем и хороши, что их трогать не надо ))
Единственный код, который может и стоит поменять — хуки. Они отвечают и за аутентификацию и за изменение данных.
+2
k12th ,  

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

0
winne4r ,  
А в чем преимущества перед тем же Sails, где своя ОРМка з адапторами, CRUD, socket и т.д.?
Он тоже так умеет, только комьюнити у него заметно больше, и апдейты почаще…
+1
+2 –1
Andrew_I ,  
Sails позиционирует себя как MVC. Создатели же Feathers хотели, чтобы библиотека была легкой (как перышко), а код простым.
Поэтому они и ограничились реализацией работы с сервисами.
В этом случае (без клиентской логики) тот же ORM является совершенно излишним. С другой стороны, на клиенте могут работать React, Angular 2, AngularJS, Vue.js и т.п., которые самостоятельно разберутся что им делать с данными, которые они получили с сервера (вот примеры).
В любом случае — каждый выбирает себе по душе. Лично меня Feathers покорил своей простотой — после 10 минут чтения документации я написал своей первый сервис. А документацию Sails мучаю уже минут 20 — и пока понимания не сложилось как это все в принципе работает.
Опять же, не хочу обидеть поклонников Sails )), но меня больше устраивает вариант, когда сервер управляет данными, а на клиенте с ними работает React
0
Andrew_I ,  
А вот как на этот вопрос отвечают создатели Feathers: https://docs.feathersjs.com/why/vs/sails.html
0
catHD ,  
Очень странно. Вы гонитесь за простотой минимального уровня входа, вы говорите что код будет простым и лёгким, но при этом делаете require всего что угодно.

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

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


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

auth

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

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

0
Andrew_I ,  
Вопрос в том, кто должен отвечать за составление запроса — клиент или сервер.
Если логику реализует клиент, то вот тут написано как конструируется запрос.
Если же сервер, то нужно использовать либо hook, либо переопределить класс Service, как показано здесь.
0
Strate ,   * (был изменён)

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


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

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

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

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


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


export class Todo {
  name: string
  owner: User
}

export class User {
  name: string
}

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

0
x07 ,  
Мечты уже давно стали реальностью. http://loopback.io/doc/en/lb3/index.html Эта штука как раз работает именно так. Описываешь сущность, а на выходе у тебя полноценный api без единой строчки кода. И клиентский sdk
–1
Andrew_I ,  
Проблема только в том, что сущность — штука вторичная. Наш backend обслуживает запросы фронтенда через API. Поэтому первичен именно API, а как он реализует сущность и данные — вообще говоря дело десятое.
Вот если мы хотим сами руками данные править, тогда да, для нас важна модель.
0
x07 ,  
Что значит «дело десятое»? Если вы фронтендер то да, вообще, до лампочки как там в бекенде все организовано. Если вы пишете про бекенд фреймворк, с помощью которого вы собираетесь разработать тот самый «первичный» API, то здесь все важно, и сущность и все остальное.
0
Andrew_I ,  
Важно передать правильные данные через API. А как вы ими управляете — через модель или хуки, вы сами решаете.
И кстати, feather generate model — создание модели в feathers ))
+1
Andrew_I ,  
И кстати. Просмотрел всю документацию по LoopBack и нигде не нашел упоминания Socket.io
Мне правильно кажется, что всё там заточено исключительно на REST?
У LoopBack есть одно преимущество — его спонсирует IBM, поэтому у разработчиков есть на что допиливать функционал.
Прочие преимущество кому-то нужны, кому-то нет. Лично мне полностью безразличны методы в моделях, зато важна связь с React. С помощью вот этой штуки я могу передавать действия Redux сервисам Feathers. Ну круто же ))
0
Strate ,  
0
Alendorff ,  

Зачастую используется какой-нибудь адаптер к базе данных, который назвается '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. В таком случае метод адаптера не будет вызван вовсе.