Итак, в первой части мы использовали потрясающие возможности Feathers.js для того, чтобы за 5 минут создать backend для нашего приложения.
Наша система умеет работать с базами данных и может использовать аутентификацию. Но понятно, что базовых возможностей сгенерированного кода недостаточно для реализации всего необходимого нам функционала.
Поэтому пришла пора «поднять капот и заглянуть в потроха» нашего кода.
Итак, в результате работы генератора, у нас есть следующие файлы:
Давайте с ними разбираться.
Каталог public нам не интересен. Так (как и в Express) хранятся статические ресурсы, которые могут быть показаны при подключению к нашему северному приложению через браузер.
Изначально в этом каталоге хранится страница html, сообщающая о том, что вы запустили приложение Feathers.
В каталоге config находятся конфигурационные файлы («а ты думал обнаружить тут епископа?» /Джон Сильвер) для настройки работы нашего приложения. Их два, default используется в процессе разработки, а production — после оной.
Формат этих файлов одинаков:
{
"host": "localhost",
"port": 3030,
"nedb": "../data/",
"public": "../public/",
"auth": {
"token": {
"secret": "bsnN3z77dVqo+HYUglvAm8pmTpECN159ZYriLpdXNqNMwEURjUV+f2qY2bCt0yuOJPb4rMMTQMJhf4s/LQPzAg=="
},
"local": {}
}
}
Структура этого файла очень проста. Вначале указывается хост и порт, на котором работает сервер. Затем пути к каталогам с данными (в данном случае с файлами базы nedb) и к рассмотренному выше каталогу public. Ну и в самом конце — параметры для аутентификации, с которой нам ещё предстоит познакомиться.
В корне каталога src лежат два интересных файла. Первый и главный (но очень короткий) — index.js.
'use strict';
// Получаем экземпляр нашего приложения Feathers (подробнее рассмотрим чуть дальше)
const app = require('./app');
// Получаем номер порта (который взят из файла конфигурации)
const port = app.get('port');
// Запускаем наш сервер. Ура, товарищи!
const server = app.listen(port);
// Как только сервер начнет работать, выводим сообщение об этом
server.on('listening', () =>
console.log(`Feathers application started on ${app.get('host')}:${port}`)
);
Вся информация о работе с приложением сосредоточена во втором файле — app.js.
Ждёте, что он будет большим и сложным? Вынужден огорчить… Те, кто уже программировал на Express.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');
// Создаем приложение Feathers
const app = feathers();
// Считываем и применяем тот самый файл конфигурации, который рассматривали абзацем выше
app.configure(configuration(path.join(__dirname, '..')));
// Подключаем к приложению различные модули
app.use(compress())
// CORS для любых доменов (помните как мы настраивали этот пункт в прошлый раз?)
.options('*', cors())
.use(cors())
// показывать в браузере иконку при открытии html-страницы
.use(favicon( path.join(app.get('public'), 'favicon.ico') ))
.use('/', serveStatic( app.get('public') )) // использовать каталог public как главный для web-сервера
.use(bodyParser.json())
.use(bodyParser.urlencoded({ extended: true }))
// подключаем глобальные хуки Feathers, которые будут применяться ко всем запросам
.configure(hooks())
// подключаем поддержку REST, теперь любой сервис автоматически доступен через этот протокол
.configure(rest())
// подключаем поддержку Socket.io, теперь любой сервис автоматически доступен через этот протокол
.configure(socketio())
// включаем все имеющиеся в приложении сервисы
.configure(services)
// включаем всё middleware приложения (работает так же), как и в Express
.configure(middleware);
module.exports = app;
Основная прелесть в том, что ни один из этих файлов вам не нужно менять. Ну разве что по мелочи :)
Например можно добавить альтернативный логгер. Хотя — стоп! Зачем добавлять его в app.js, если мы автоматом подключили всё middleware и именно туда надо добавить логгер?!
Откроем папку middleware, а в ней файл index.js. Тут комментарии уже не мои, это генератор постарался.
'use strict';
const handler = require('feathers-errors/handler');
const notFound = require('./not-found-handler');
const logger = require('./logger');
module.exports = function() {
// Add your custom middleware here. Remember, that
// just like Express the order matters, so error
// handling middleware should go last.
const app = this;
app.use(notFound());
app.use(logger(app));
app.use(handler());
};
То есть для того, чтобы добавить свой промежуточный модуль, достаточно:
1. Описать его в соответствующем файле в каталоге middleware.
2. Подключить с помощью require.
3. Добавить в цепочку с помощьюapp.use.
Например простое middleware not-found-handler.js, предназначенное для обработки запросов к несуществующим ресурсам, выглядит вот так:
const errors = require('feathers-errors');
module.exports = function() {
return function(req, res, next) {
next(new errors.NotFound('Page not found'));
};
};
Как видите, каждое промежуточное ПО представляет собой функцию, которая возвращает функцию, в которой вызывается функция next() для перехода к следующему middleware. Да, всё как в express.js, да и модули от неё работают в Feathers.
А вот чего в Express нет, так это хуков. Делятся hooks на глобальные (работающие со всем приложением) и локальные для каждого сервиса. Hooks вызываются до и после вызова методов сервиса. Реализация самого простого хука предлагается нам генератором в файле hooks/index.js
exports.myHook = function(options) {
return function(hook) {
console.log('My custom global hook ran. Feathers is awesome!');
};
};
No comments! Подключать его будем позже.
Уфф! Мы закончили с подготовительной частью и переходим к сердцу Feathers — сервисам.
Сервисы хранятся в каталоге services, состоят из файла index.js и подкаталога hooks, где хранятся специфичные для данного сервиса хуки.
Сервисы бывают обычные и для работы с базами данных. Обычные сервисы — это объекты, которые реализуют несколько методов (или часть из них):
- find
- get
- create
- update
- patch
- remove
- setup
После того как метод реализован, Feathers будет запускать его в ответ на соответствующий запрос по протоколам REST, Socket.io или Primus. Напомню, прелесть в том, что вам не нужно подключать обработчики этих протоколов — просто реализуйте методы!
Каждый из методов должен возвращать объект класса Promise, например вот так:
get(id, params) {
return Promise.resolve({
id,
read: false,
text: `Feathers is great!`,
createdAt: new Date().getTime()
});
В результате, после запроса типа GET, в браузер будет возвращаться соответствующий объект, упакованный в json (помните, что мы подключали поддержку json в app.js?)
В следующем примере я приведу одновременно и шаблон для создания сервиса (ровно то же создаст для вас генератор) и соответствие между методами сервиса и запросами REST.
const myService = {
// GET /messages
find(params [, callback]) {},
// GET /messages/<id>
get(id, params [, callback]) {},
// POST /messages
create(data, params [, callback]) {},
// PUT /messages[/<id>]
update(id, data, params [, callback]) {},
// PATCH /messages[/<id>]
patch(id, data, params [, callback]) {},
// DELETE /messages[/<id>]
remove(id, params [, callback]) {},
setup(app, path) {}
}
А для того, чтобы при работе с базой данных, не реализовывать эти методы вручную — генератор создает для вас специальные сервисы. Выглядят они вот так (комментарии мои):
const path = require('path');
// подключаем библиотеку для работы с нужной нам базой (в данном случае NeDB)
const NeDB = require('nedb');
// и библиотеку Feathers для взаимодействия с этой базой
const service = require('feathers-nedb');
// подключим локальные хуки
const hooks = require('./hooks');
module.exports = function(){
const app = this;
// определим параметры для работы с базой
// каталог возьмем из конфигурационного файла, имя базы по умолчанию = имя сервис
const db = new NeDB({
filename: path.join(app.get('nedb'), 'contacts.db'),
// этот параметр означает автоматическую загрузку данных, в противном случае данные нужно
// будет подгружать вручную
autoload: true
});
//определяем параметры БД
let options = {
Model: db,
// передается ли информация целиком или постранично
paginate: {
default: 5,
max: 25
}
};
// Дальше комментарии генератора весьма полезны
// Initialize our service with any options it requires
app.use('/contacts', service(options));
// Get our initialize service to that we can bind hooks
const contactsService = app.service('/contacts');
// Set up our before hooks
contactsService.before(hooks.before);
// Set up our after hooks
contactsService.after(hooks.after);
};
Единственны минус такого подхода в том, что запросы по факту формируются на стороне клиента и напрямую передаются в базу для обработки.
Вы спросите у меня — а как же быть, если нужно проверить запрос от пользователя, изменить его или поправить набор возвращаемых данных;
Ха, отвечу я вам! А для этого у нас есть хуки!
По умолчанию для каждого сервиса создается свой файл index.js, в который вы можете поместить специфичные хуки.
// подключили глобальные хуки
const globalHooks = require('../../../hooks');
// подключили стандартные хуки Feathers
const hooks = require('feathers-hooks');
// описываем специфичные хуки, которые будут вызываться ДО вызова сервиса
// например для проверки параметров
exports.before = {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
};
// описываем специфичные хуки, которые будут вызываться ПОСЛЕ вызова сервиса
// например для изменения полученных из базы результатов
exports.after = {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
};
Хуки определяются как функции. Каждый хук ДО получит следующие параметры:
- method — имя вызываемого метода
- type — hook type (before или after)
- params — параметры, переданные сервису
- data — данные запроса (для методов create, update и patch)
- app — объект app
- id — id (для get, remove, update и patch)
Хуки ПОСЛЕ дополнительно получают еще и параметр result — результат запроса.
Ну и вишенка на торте! Есть готовые хуки, которые позволяют выполнить стандартные операции с данными. Вот, например, хук remove позволяет удалить некоторые поля из результатов запроса или из самих запросов:
app.service('users').before({
create: hooks.remove('_id'),
update: hooks.remove('_id'),
patch: hooks.remove('_id})
Т.е. при использовании запросов create, update и patch из запросов будет удалено поле _id
app.service('users').after(hooks.remove('password', 'salt'));
А тут мы из результата запроса удалим поля password и salt.
Таких буков достаточно много, все они описаны здесь.
Ура! Мы практически закончили с теорией и в третьей части сможем написать что-то сами. Продолжение следует :)
Большинство из вас к контролю относится отрицательно и как программист я вас понимаю.
Но поскольку программу хорошо покупают, то выходит, что руководители имеют немного другое мнение.
Поэтому в свое свободное время я решил сделать систему личной продуктивности. Она нужна для того, чтобы мы сами знали на что тратим свою жизнь :) Система не будет ничего никому отсылать, она попробует помочь вам (тем, кто этого действительно хочет) стать немного эффективнее.
Написание этой системы было своего рода экспериментом — она целиком создана на JavaScript, даже агент собирающий информацию. В качестве работает Feathers, на клиенте React, данные хранятся в NeDB.
Так вот, в течение ближайшего месяца у меня в планах представить систему на тестирование. Поддерживаться будут Windows и MacOS X, отчетов будет минимальное количество. Но, я буду ждать ваших отзывов и (самое главное!) предложений о том, какой функционал вам нужен.
Автор: Kickidler