В этой статье я подробно описываю, как я создал тонкий слой поверх FeathersJS, чтобы сделать приложение более готовым к производству. Универсальная конфигурация предоставляет расширяемые параметры для различных сред развертывания аналогичным образом на стороне клиента / сервера. Это основа общепринятых производственных практик, таких как:

  • иметь возможность настроить транспорт, который будет использоваться для обхода брандмауэров / проблем безопасности,
  • выполнить префикс API для одновременного развертывания нескольких версий,
  • сделать уровень журнала настраиваемым для отслеживания невоспроизводимых ошибок.

Такой подход был использован для успешной поддержки проекта Weacast.

Универсальная конфигурация

Что касается конфигурации, FeathersJS уже предоставляет небольшую оболочку над node-config; на стороне клиента, однако, дополнительных функций не предусмотрено. Я также хотел иметь возможность управлять различными файлами конфигурации развертывания. Поскольку я использую webpack для сборки своего клиентского приложения, я решил также интегрировать node-config для внешнего интерфейса. Итак, я добавил это в свою сборку:

var fs = require(‘fs’)
// Load config based on current NODE_ENV value
var clientConfig = require(‘config’)
// Save it to ‘config/client-config.json’
fs.writeFileSync(path.join(‘config’, ‘client-config.json’), JSON.stringify(clientConfig))

И псевдоним в моей конфигурации WebPack:

webpackConfig = {
 resolve: {
 …
   alias: {
     config: path.resolve(__dirname, ‘./client-config.json’)
   }
 },
 …
}

Этот псевдоним позволяет стандартным образом интегрировать этот конфигурационный файл в клиентские файлы:

import config from ‘config’

Основное отличие от конфигурации на стороне сервера заключается в том, что эта конфигурация создается во время сборки, а не во время выполнения, потому что на клиенте нет запущенного процесса NodeJS, это просто статический импорт файла. в конце. Однако вы извлекаете выгоду из мощности node-config для управления сложными конфигурациями. На самом деле лучше всего держать их независимыми и делиться частями, когда это необходимо. В самом деле, вы не хотели бы утечки переменных среды частного сервера в выходных данных конфигурации клиента, что могло бы вызвать серьезные проблемы с безопасностью.

Префикс API

Во-первых, я добавил свойство apiPath в файлы конфигурации приложения, определяющее используемый префикс API. Затем, чтобы избавиться от префикса API внутри при объявлении / извлечении службы, я добавил небольшой набор функций поверх экземпляра приложения Feathers на стороне внешнего / внутреннего интерфейса. Действительно, получение сервисов Feathers всегда основывается на пути, а не на имени сервиса, поэтому вам необходимо знать детали префикса API:

app.declareService = function (name, app, service) {
  const path = app.get('apiPath') + '/' + name
  // Initialize our service
  app.use(path, service)
return app.getService(name)
}

app.getService = function (name) {
  return app.service(app.get('apiPath') + '/' + name)
}

При настройке транспортов на бэкэнд-стороне вы должны предоставить другую конечную точку для REST / Websocket, чтобы избежать конфликта, поэтому в моем случае я изменил конфигурацию socketio по умолчанию:

app.configure(rest())
app.configure(socketio({
  path: app.get('apiPath') + 'ws'
}))

Аналогично на стороне клиента:

import config from 'config'
let socket = io(window.location.origin, {
  path: config.apiPath + 'ws'
})
app.configure(feathers.socketio(socket))

Уровень аутентификации

Чтобы аутентификация работала с префиксом API, вам необходимо настроить конфигурацию по умолчанию, на стороне бэкенда файл конфигурации выглядит так:

var API_PREFIX = '/api'
module.exports = {
  apiPath: API_PREFIX,
  authentication: {
    ...
    path: API_PREFIX + '/authentication',
    service: API_PREFIX + '/users'
  },
  ...

Тогда на стороне клиента настройка конфигурации FeathersJS будет выглядеть так:

app.configure(feathers.authentication({
  storage: window.localStorage,
  path: config.apiPath + '/authentication'
}))

Позаботьтесь о настройке ваших транспортов (REST / Websocket) перед аутентификацией, в противном случае некоторые требуемые хуки (заполнение заголовков и т. д.) могут быть неправильно зарегистрированы.

Транспорт

Как было показано ранее, серверное приложение предоставляет конечную точку REST, а также конечную точку Websocket, последнее, что нужно сделать, - это выбрать правильный вариант в клиенте в соответствии с конфигурацией:

import config from 'config'
let app = feathers()
app.configure(hooks())
if (config.transport === 'websocket') {
  let socket = io(window.location.origin, {
    transports: ['websocket'],
    path: config.apiPath + 'ws'
  })
  app.configure(feathers.socketio(socket))
} else {
 app.configure(feathers.rest(window.location.origin).fetch(window.fetch.bind(window)))
}

логирование

Когда вы работаете с FeatherJS, вы быстро понимаете, что хуки - это первоклассные граждане, особенно на стороне сервера, но они также полезны и на стороне клиента. Используя хуки, я создал простой настраиваемый уровень журнала с обеих сторон, используя winston для бэкэнда и loglevel для внешнего интерфейса. Хуки очень похожи, бэкэнд уже сгенерирован FeathersJS CLI, так что он предназначен для клиента (hooks / logger.js):

import logger from 'loglevel'
export function log (hook) {
  let message = `${hook.type}: ${hook.path} - Method: ${hook.method}`
  if (hook.type === 'error') {
    message += `: ${hook.error.message}`
  }
  logger.debug(message)
  if (hook.error) {
    logger.error(hook.error)
  }
  if (hook.data) {
    logger.trace(hook.data)
  }
  if (hook.params) {
    logger.trace(hook.params)
  }
  if (hook.result) {
    logger.trace(hook.result)
  }
}

Настройте его в клиентском приложении следующим образом:

import logger from './hooks/logger'
app.hooks({
  before: {
    all: [ logger ]
  },
  after: {
    all: [ logger ]
  },
  error: {
    all: [ logger ]
  }
})

И прочтите уровень журнала, который будет использоваться из конфигурации при запуске приложения:

import logger from 'loglevel'
import config from 'config'
let app = feathers()
if (config.logs && config.logs.level) {
  logger.setLevel(config.logs.level, false)
} else {
  logger.setLevel('info')
}

Ошибка отлова

Еще одно полезное использование обработчиков на стороне клиента (отчасти связанное с ведением журнала) - это глобальный перехват ошибки, как это возможно на внутренней стороне, путем вывода общего всплывающего сообщения или чего-то подобного в пользовательском интерфейсе. Однако это больше привязано к технологии внешнего интерфейса, которую вы используете, чем к ведению журнала. Например, в Weacast мы реализовали это в Vue.js с помощью хука, поднимающего глобальные события на шине:

import EventBus from '../event-bus'
export function emit (hook) {
  EventBus.$emit(hook.type + 'Hook', hook)
}

Эти события затем перехватываются такими компонентами управления ошибками:

EventBus.$on('errorHook', hook => {
  // Display an alert, possibly depending on the hook properties
})

Думаю, у вас возникла идея сделать так, чтобы это работало с React или Angular.

Заключение

Я надеюсь, что знание того, как сделать ваше приложение FeathersJS более настраиваемым, будет вам полезно, чтобы получить более полный пример, вы можете взглянуть на проект Weacast:

Если вам понравилась эта статья, не стесняйтесь взглянуть на наши решения с открытым исходным кодом и насладиться нашими другими статьями о Feathers, команде Kalisio!