Feathers 的各个模块及其API

 2019年07月29日    115     声明


本文译自Feathers官方API,介绍Feathers的各个模块及其所有API。

  1. Core: Feathers 核心功能
    • Application - Feathers应用程序API
    • Services - Service 对象及其方法和Feathers相关功能
    • Hooks - 用于服务方法的可插拔中间件
    • Events -Feathers服务方法发送的事件
    • Channels - 确定要发送给连接的实时客户端的事件
    • Errors - Feathers中使用的错误类集合
    • Configuration - 用于初始化服务器端应用配置的node-config包装器
  2. Transports: 将 Feathers 应用公开为API服务器
    • Express - Feathers Express框架绑定,REST API提供程序和错误中间件
    • Socket.io - Socket.io实时传输提供程序
    • Primus - Primus实时运输提供程序
  3. Client: 关于如何在客户端上使用Feathers的详细介绍
    • 用法 - Feathers 客户端在Node、React Native和浏览器中的使用(也包括Webpack和Browserify)
    • REST - 客户端和直接REST API服务器使用
    • Socket.io - 客户端和直接Socket.io API服务器使用
    • Primus - 客户端和直接Primus API服务器使用
  4. Authentication: Feathers 认证机制
    • Server - 主要身份验证服务器配置
    • Client - Feathers身份验证服务器的客户端
    • Local - 本地电子邮件/密码验证
    • JWT - JWT 认证
    • OAuth1 - 通过oAuth1获取JWT
    • OAuth2 - 通过oAuth2获取JWT
  5. Database: Feathers公用数据库适配器API及查询机制
    • Adapters - 支持的数据库适配器列表
    • Common API - 数据库适配器常用初始化和配置API
    • Querying - 常见的查询机制

1. Core-Feathers 核心功能

1.1 Application

$ npm install @feathersjs/feathers --save

核心@feathersjs/feathers模块提供了初始化新Feathers应用实例的功能。适用于Node、React Native和浏览器(有关更多信息,请参阅client章节)。每个实例都允许注册和检索服务钩子、插件配置、以及获取和设置配置选项。初始化的Feathers应用称为app对象。

const feathers = require('@feathersjs/feathers');

const app = feathers();

.use(path, service)

app.use(path, service) -> app允许在指定的path上注册service 对象

// 添加一个 service.
app.use('/messages', {
  get(id) {
    return Promise.resolve({
      id,
      text: `This is the ${id} message!`
    });
  }
});

其中,path可以是/以将服务注册在根一级。


.service(path)

app.service(path) -> service返回给定路径的包装service 对象。Feathers内部会从每个注册的服务创建一个新对象。这意味着app.service(path)返回的对象将提供与原始服务对象相同的方法和功能,但也会提供Feathers及其插件(如服务事件其他方法)添加的功能。path可以是带或不带前导和斜杠的服务名称。

const messageService = app.service('messages');

messageService.get('test').then(message => console.log(message));

app.use('/my/todos', {
  create(data) {
    return Promise.resolve(data);
  }
});

const todoService = app.service('my/todos');
// todoService is an event emitter
todoService.on('created', todo => 
  console.log('Created todo', todo)
);


.hooks(hooks)

app.hooks(hooks) -> app可以注册应用级别的钩子。详细参考应用钩子章节。


.publish([event, ] publisher)

app.publish([event, ] publisher) -> app注册一个全局级别的事件发布者。详细参考channels publishing章节。


.configure(callback)

app.configure(callback) -> app运行一个传递应用程序对象的callback函数。其用于初始化插件或服务。

function setupService(app) {
  app.use('/todos', todoService);
}

app.configure(setupService);


.listen(port)

app.listen([port]) -> HTTPServer在指定的端口上启动应用程序。它会设置所有已配置的传输(如果有),然后使用服务器对象运行app.setup(server)(参见下文),然后返回服务器对象。

只有在配置了服务器端传输(REST、Socket.io或Primus)后才能使用listen


.setup([server])

app.setup([server]) -> app用于通过调用每个服务的.setup(app,path)方法(如果可用)来初始化所有服务。其还可以使用传递的server实例(如通过http.createServer)来设置SocketIO(如果已启用)以及可能需要服务器实例的任何其他提供程序。

通常app.setup将在通过app.listen([port])启动应用时自动调用,但有时需要显式调用它。


.set(name, value)

app.set(name, value) -> app将设置name指定给value


.get(name)

app.get(name) -> value获取设置name的值。有关服务器端Express设置的更多信息,请参考Express文档

app.set('port', 3030);

app.listen(app.get('port'));


.on(eventname, listener)

为指定的事件eventname注册一个listener监听器方法(function(data) {})。其由NodeJS EventEmitter .on提供。

app.on('login', user => console.log('Logged in', user));


.emit(eventname, data)

向所有监听器发送eventname事件。其由NodeJS EventEmitter .emit提供。

app.emit('myevent', {
  message: 'Something happened'
});

app.on('myevent', data => console.log('myevent happened', data));


.removeListener(eventname, [ listener ])

移除所有或指定eventname的事件监听器。其由NodeJS EventEmitter .removeListener提供。


.mixins

app.mixins包含服务混合列表。mixin是一个回调((service,path) => {}),它为每个正在注册的服务运行。添加自己的mixins可以为每个注册的服务添加功能。

const feathers = require('@feathersjs/feathers');
const app = feathers();

// 在注册任何服务之前必须添加Mixins
app.mixins.push((service, path) => {
  service.sayHello = function() {
    return `Hello from service at '${path}'`;
  }
});

app.use('/todos', {
  get(id) {
    return Promise.resolve({ id });
  }
});

app.service('todos').sayHello();
// -> Hello from service at 'todos'


.services

app.services包含由app.use(path, service)注册的路径为键的所有services的对象。这会返回所有可用服务名称的列表:

const servicePaths = Object.keys(app.services);

servicePaths.forEach(path => {
  const service = app.service(path);

  console.log(path, service);
});

注意,要获取服务,应使用app.service(path)方法,而不是直接使用app.services.path

Feathers客户端对它所连接的服务器一无所知。这意味着app.services不会自动包含服务器上可用的所有服务。相反,服务器必须提供其服务列表(如,通过一个自定义服务):

app.use('/info', {
  async find() {
    return {
      services: Object.keys(app.services)
    }
  }
});


.defaultService

app.defaultService可以是一个函数,如果尚未注册,则返回app.service(path)的新的标准服务的实例。

const memory = require('feathers-memory');

// 对于没有服务的每个`path`,会自动返回一个新的内存服务
app.defaultService = function(path) {
  return memory();
}

客户端传输适配器使用它来自动注册与Feathers服务器通信的客户端服务。


1.2 Services

"Services"是每个Feathers应用程序的核心。服务是实现某些方法的JavaScript对象(或ES6类实例)。Feathers本身也会为其服务添加一些额外的方法和功能

Service 方法

服务方法是服务对象可以实现的预定义的CRUD方法(或者已经由其中一个数据库适配器实现的方法)。下面是一个Feathers服务接口的完整示例,其是一个普通的JavaScript对象,返回Promise或使用async/await

const myService = {
  find(params) {
    return Promise.resolve([]);
  },
  get(id, params) {},
  create(data, params) {},
  update(id, data, params) {},
  patch(id, data, params) {},
  remove(id, params) {},
  setup(app, path) {}
}

app.use('/my-service', myService);
const myService = {
  async find(params) {
    return [];
  },
  async get(id, params) {},
  async create(data, params) {},
  async update(id, data, params) {},
  async patch(id, data, params) {},
  async remove(id, params) {},
  setup(app, path) {}
}

app.use('/my-service', myService);

服务还可以是一个ES6类实例:

class MyService {
  find(params) {
    return Promise.resolve([]);
  }
  get(id, params) {}
  create(data, params) {}
  update(id, data, params) {}
  patch(id, data, params) {}
  remove(id, params) {}
  setup(app, path) {}
}

app.use('/my-service', new MyService());
class MyService {
  async find(params) {
    return [];
  }
  async get(id, params) {}
  async create(data, params) {}
  async update(id, data, params) {}
  async patch(id, data, params) {}
  async remove(id, params) {}
  setup(app, path) {}
}

app.use('/my-service', new MyService());

注意:

  • 服务方法是可选的,如果没有实现方法,Feathers会自动发出NotImplemented错误。
  • 始终使用app.service(path)返回的服务而不是服务对象(上面的myService对象)。有关更多信息,请参阅app.service文档。

服务方法必须返回一个Promise或定义为async并有以下参数:

  • id — 资源的标识符。资源是由唯一ID标识的数据。
  • data — 源数据
  • params - 方法调用的附加参数,请参阅params

注册后,可以通过app.service()检索和使用该服务:

const myService = app.service('my-service');

myService.find().then(items => console.log('.find()', items));
myService.get(1).then(item => console.log('.get(1)', item));

请注意,服务不一定使用数据库。你可以使用使用某些API的软件包轻松替换示例中的数据库。

注意:本节介绍服务方法的一般用法以及如何实现它们。它们已由官方Feathers数据库适配器实现。 有关如何使用数据库适配器的详细信息,请参阅数据库适配器通用API


params

params包含服务方法调用的附加信息。params中的某些属性可以由Feathers设置。 常用的有:

  • params.query - 来自客户端的查询参数,作为URL查询参数(请参阅REST章节)或通过websockets(请参阅Socket.ioPrimus)传递。
  • params.provider - 调用本服务所使用的传输 (restsocketioprimus)。对于服务器的内部调用,将是undefined(除非明确传递)。
  • params.user - 经过身份验证的用户,可以通过Feathers身份验证设置或显式传递。
  • params.connection - 如果服务是通过实时传输(例如通过websockets)进行的调用,则params.connection是可以与频道(channels)一起使用的连接对象。

注意:对于外部调用,只有params.query会在客户端和服务器之间发送。如果未传入,则params.query将是undefined用于内部调用。


.find(params)

service.find(params) -> Promise - 从服务中检索所有资源的列表。提供的参数将作为params.query传递。

app.use('/messages', {
  find(params) {
    return Promise.resolve([
      {
        id: 1,
        text: 'Message 1'
      }, {
        id: 2,
        text: 'Message 2'
      }
    ]);
  }
});

注意:find不必返回数组,也可以返回一个对象。数据库适配器已经为分页执行此操作。


.get(id, params)

service.get(id, params) -> Promise - 从服务中检索指定id的单个资源。

app.use('/messages', {
  get(id, params) {
    return Promise.resolve({
      id,
      text: `You have to do ${id}!`
    });
  }
});


.create(data, params)

service.create(data, params) -> Promise - 通过指定的data创建一个新资源。该方法应返回带有新创建数据的Promisedata也可能是一个数组。

app.use('/messages', {
  messages: [],

  create(data, params) {
    this.messages.push(data);

    return Promise.resolve(data);
  }
});

注意:成功的create方法调用会发出created服务事件。


.update(id, data, params)

service.update(id, data, params) -> Promise - 将id标识的资源替换为data。该方法应返回带有完整,更新后的资源的Promise。更新多个记录时,id也可以为null,其中params.query包含查询条件。

注意:成功的update方法调用会发出updated服务事件。


.patch(id, data, params)

patch(id, data, params) -> Promise - 将id标识的资源的现有数据与新data合并。id也可以为null,表使用查询条件的params.query修补多个资源。

该方法应返回完整的更新资源数据。如果要区分部分更新和完全更新并支持PATCHHTTP方法,请另外使用(或代替)update实现patch

注意:成功的patch方法调用会发出patched服务事件。


.remove(id, params)

service.remove(id, params) -> Promise - 删除id标识的资源。该方法应返回带有已删除资源的Promiseid也可以为null,表示删除多个资源,其中params.query包含查询条件。

注意:成功的removed方法调用会发出removed服务事件。


.setup(app, path)

service.setup(app, path) -> Promise是一个特殊方法,用于初始化服务。其传递参数为Feathers应用程序的实例app及其已注册的路径path

对于在调用app.listen之前注册的服务,会在调用app.listen时调用每个注册服务的setup函数;对于调用app.listen后注册的服务,在注册服务时Feathers会自动调用setup

setup是一个很好的位置,可以使用任何特殊配置初始化你的服务,或者连接非常紧密耦合的服务(见下文),而不是使用钩子

// app.js
'use strict';

const feathers = require('@feathersjs/feathers');
const rest = require('@feathersjs/express/rest');

class MessageService {
  get(id, params) {
    return Promise.resolve({
      id,
      read: false,
      text: `Feathers is great!`,
      createdAt: new Date.getTime()
    });
  }
}

class MyService {
  setup(app) {
    this.app = app;
  }

  get(name, params) {
    const messages = this.app.service('messages');

    return messages.get(1)
      .then(message => {
        return { name, message };
      });
  }
}

const app = feathers()
  .configure(rest())
  .use('/messages', new MessageService())
  .use('/my-service', new MyService())

app.listen(3030);


Feathers 功能

注册服务时,Feathers(或其插件)也可以将自己的方法添加到服务中。最值得注意的是,每个服务都将自动成为NodeJS EventEmitter的一个实例。

.hooks(hooks)

为服务注册钩子


.publish([event, ] publisher)

注册事件发布回调。 有关更多信息,请参阅channels章节。


.mixin(mixin)

service.mixin(mixin) -> service - 扩展了服务的功能。 有关更多信息,请参阅Uberproto项目页面。


.on(eventname, listener)

为指定的事件eventname注册一个listener监听器方法(function(data) {})。其由NodeJS EventEmitter .on提供。

更多关系服务事件的介绍请参阅事件章节


.emit(eventname, data)

向所有监听器发送eventname事件。其由NodeJS EventEmitter .emit提供。

更多关系服务事件的介绍请参阅事件章节


.removeListener(eventname, [ listener ])

移除所有或指定eventname的事件监听器。其由NodeJS EventEmitter .removeListener提供。

更多关系服务事件的介绍请参阅事件章节


1.3 Hooks


示例

在以下示例中,会在保存数据到数据时添加createdAtupdatedAt属性,并记录服务上的任何错误:

const feathers = require('@feathersjs/feathers');

const app = feathers();

app.service('messages').hooks({
  before: {
    create(context) {
      context.data.createdAt = new Date();
    },

    update(context) {
      context.data.updatedAt = new Date();
    },

    patch(context) {
      context.data.updatedAt = new Date();
    }
  },

  error(context) {
    console.error(`Error in ${context.path} calling ${context.method} method`, context.error);
  }
});


钩子函数

钩子函数可以是普通或async函数或箭头函数,它将钩子上下文作为参数并且可以:

  • 返回 context 对象
  • 返回 (undefined)
  • 返回 feathers.SKIP 以跳过所有 further 钩子
  • throw 一个错误
  • 对于异步操作会返回一个Promise,可以是:
    • resolves 一个 context 对象
    • resolves 一个 undefined
    • rejects 一个错误

更多详细介绍请参考钩子流异步钩子

// 普通钓子函数
function(context) {
  return context;
}

// 异步钩子函数与 promise
function(context) {
  return Promise.resolve(context);
}

// async 钩子函数
async function(context) {
  return context;
}

// 普通箭头函数
context => {
  return context;
}

// 异步箭头函数与 promise
context => {
  return Promise.resolve(context);
}

// async 箭头函数
async context => {
  return context;
}

// 跳过 further 钩子
const feathers = require('@feathersjs/feathers');

async context => {
  return feathers.SKIP;
}


钩子上下文

钩子context被传递给一个钩子函数,并包含有关服务方法调用的信息。它具有不可修改的只读属性和可以为后续挂钩修改的可写属性。

在整个服务方法调用中,context对象是相同的,因此可以添加属性并在其后的其他挂钩中使用它们。


context.app

context.app是包含Feathers应用对象只读属性,可用于检索其他服务(通过context.app.service('name'))或配置值。


context.service

context.service是一个只读属性,包含此钩子当前运行的服务。


context.path

context.path是一个只读属性,包含没有前导或尾部斜杠的服务名称(或路径)。


context.method

context.method是一个只读属性,带有服务方法的名称(findgetcreateupdatepatchremove之一)。


context.type

context.type是一个只读属性,包含此钩子的类型(beforeaftererror之前)。


context.params

context.params是一个可写属性,包含服务方法参数(包括params.query)。更多有关信息,请参阅service params文档。


context.id

context.id是一个可写属性,id用于getupdatepatchremove方法的调用。对于updatepatchremove方法在操作多个实体时context.id可以是null。除此以外则是undefined

注意,context.id仅对getupdatepatchremove方法有效。


context.data

context.data是一个可写属性,包含用于createupdatepatch服务方法调用的数据。

注意,context.id仅对createupdatepatch方法有效。


context.error

context.error是一个可写属性,带有在失败的方法调用中抛出的错误对象。它仅在错误钩子中可用(即,仅context.typeerror时)。


context.result

context.result是一个可写属性,包含成功的服务方法调用的结果。其仅在after钓子中有效。context.result同样了可以设置在:

  • before钩子中,以跳过实际的服务方法(数据库)调用
  • error钩子中,用于隐藏错误并返回结果

注意,context.result仅在context.typeaftercontext.result被设置时有效。


context.dispatch

context.dispatch是一个可写可选属性,包含应发送给任何客户端的数据的“安全”版本。 如果尚未设置context.dispatch,则context.result将被发送到客户端。

注意,context.dispatch仅影响通过Feathers Transport(如REST或Socket.io)发送的数据。内部方法调用仍将获取context.result中的数据集。


context.statusCode

context.dispatch是一个可写可选属性,它使你可以重写要返回的标准的HTTP状态码


钩子流

通常,钩子按照它们在所有before钩子之后调用的原始服务方法注册的顺序执行。该流程可以影响如下。


抛出错误

当抛出错误(或 rejected 状态的Promise)时,将跳过所有后续挂钩,并将仅运行错误挂钩。

以下示例会在创建新消息的文本为空时引发错误。还可以创建与之类似的钩子以使用你选择的Node验证库。

app.service('messages').hooks({
  before: {
    create: [
      function(context) {
        if(context.data.text.trim() === '') {
          throw new Error('Message text can not be empty');
        }
      }
    ]
  }
});


设置context.result

context.resultbefore钓子中设置,原始的服务方法调用会被跳过。

所有其它钩子仍将按正常顺序执行。以下示例始终返回当前经过身份验证的用户,而不是所有get方法调用的实际用户:

app.service('users').hooks({
  before: {
    get: [
      function(context) {
        // Never call the actual users service
        // just use the authenticated user
        context.result = context.params.user;
      }
    ]
  }
});


返回feathers.SKIP

require('@feathersjs/feathers').SKIP可以返回应该跳过的钩子,其指示后所有钩子都会被跳过。如果由before钩子返回,则跳过剩余的before钩子; 任何after钩子仍将运行。 如果尚未运行,则仍将调用服务方法,除非已设置了context.result


异步钩子

如果钓子函数是async或返回Promise时,它将等待所有异步操作解析或拒绝,然后继续下一个钩子。

钩子函数部分所述,promise必须使用content对象(通常在promise链的末尾使用.then(()=> context)或使用undefined来解析。


async/await

使用Node v8.0.0或更高版本时,强烈建议使用async/await。这将避免使用Promises和异步钩子流时的许多常见问题。任何钩子函数都可以是async的,在这种情况下,它将等待所有await操作完成。就像普通的钩子一样,它应该返回context对象或undefined

以下示例显示了一个async/await钩子,它使用另一个服务在获取单个消息时检索并填充user消息:

app.service('messages').hooks({
  after: {
    get: [
      async function(context) {
        const userId = context.result.userId;

        // Since context.app.service('users').get returns a promise we can `await` it
        const user = await context.app.service('users').get(userId);

        // Update the result (the message)
        context.result.user = user;

        // Returning will resolve the promise with the `context` object
        return context;
      }
    ]
  }
});


返回 promises

以下示例显示了一个异步挂钩,它使用另一个服务在获取单个消息时检索并填充user消息。

app.service('messages').hooks({
  after: {
    get: [
      function(context) {
        const userId = context.result.userId;

        // context.app.service('users').get returns a Promise already
        return context.app.service('users').get(userId).then(user => {
          // Update the result (the message)
          context.result.user = user;

          // Returning will resolve the promise with the `context` object
          return context;
        });
      }
    ]
  }
});

注意:

  • 当钩子未按预期顺序运行时,常见问题是钩子函数顶层的promise的缺少return语句。
  • 大多数据Feathers服务调用和较新的Node包已经返回Promises。 它们可以直接退回和链接。在这些情况下,无需实例化你自己的新Promise实例。


转换回调

当异步操作使用回调而不是返回promise时,你必须创建并返回一个新的Promise(new Promise((resolve,reject)=> {}))或使用util.promisify

以下示例读取使用util.promisify转换fs.readFile的JSON文件:

const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);

app.service('messages').hooks({
  after: {
    get: [
      function(context) {
        return readFile('./myfile.json').then(data => {
          context.result.myFile = data.toString();

          return context;
        });
      }
    ]
  }
});

除此之外,还可以使用Bluebird等库在回调及Promise之间进行转换。


注册钩子

钩子函数通过app.service(<servicename>l;).hooks(hooks)方法在服务中注册。可以作为hooks传递的内容有几种选择:

// The standard all at once way (also used by the generator)
// an array of functions per service method name (and for `all` methods)
// 标准 all,一次性(也由生成器使用)传入每个服务方法名称的函数数组(以及“all”方法)
app.service('servicename').hooks({
  before: {
    all: [
      // 使用普通函数
      function(context) { console.log('before all hook ran'); }
    ],
    find: [
      // 使用 ES6 箭头函数
      context => console.log('before find hook 1 ran'),
      context => console.log('before find hook 2 ran')
    ],
    get: [ /* other hook functions here */ ],
    create: [],
    update: [],
    patch: [],
    remove: []
  },
  after: {
    all: [],
    find: [],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: []
  },
  error: {
    all: [],
    find: [],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: []
  }
});

// 为所有方法在错误之前,之后和错误处注册单个挂钩
app.service('servicename').hooks({
  before(context) {
    console.log('before all hook ran');
  },
  after(context) {
    console.log('after all hook ran');
  },
  error(context) {
    console.log('error all hook ran');
  }
});

使用完整对象时,all都是一个特殊关键字,这意味着此钩子将针对所有方法运行。all钩子都将在其他特定于方法的挂钩之前注册。

app.service(<servicename>l;).hooks(hooks)可以多次调用,钩子将按顺序注册。通常情况下,所有钩子都应该立即注册,然后一目了然地看看服务将要做什么。


应用的钩子

要为每个服务添加钩子,可以使用app.hooks(hooks)。应用程序钩子以与服务钓子相同的格式注册,并且工作方式完全相同。

注意,何时会执行应用程序挂钩:

  • before 应用级的钩子会在所有服务的before钩子之前执行
  • after 应用级的钩子会在所有服务的after钩子之后执行
  • error 应用级的钩子会在所有服务的error钩子之后执行

下面是一个应用程序钩子的示例,它使用服务和方法名称以及错误堆栈记录每个服务方法错误:

app.hooks({
  error(context) {
    console.error(`Error in '${context.path}' service method '${context.method}'`, context.error.stack);
  }
});


1.4 Events

事件是Feathers实时功能的关键部分。Feathers中的所有事件都通过NodeJS EventEmitter接口提供。

EventEmitters

注册后,任何服务都会变成标准的NodeJS EventEmitter,并可以相应地使用。

const messages = app.service('messages');

// Listen to a normal service event
messages.on('patched', message => console.log('message patched', message));

// Only listen to an event once
messsages.once('removed', message =>
  console.log('First time a message has been removed', message)
);

// A reference to a handler
const onCreatedListener = message => console.log('New message created', message);

// Listen `created` with a handler reference
messages.on('created', onCreatedListener);

// Unbind the `created` event listener
messages.removeListener('created', onCreatedListener);

// Send a custom event
messages.emit('customEvent', {
  type: 'customEvent',
  data: 'can be anything'
});


Service Events

当相应的服务方法成功返回时,所有服务都会自动发出createdupdatedpatchedremoved的事件。这适用于客户端以及服务器。当客户端使用Socket.io或Primus时,事件将自动从服务器推送到所有连接的客户端。 这实际上是Feathers实现实时性的方式。

在所有钩子都执行之前,不会触发事件。

有关如何发布这些事件以实现对已连接客户端的实时更新的信息,请参阅channels章节。

除了事件data之外,所有事件还从它们作为第二个参数传递的方法调用中获取钓子上下文


created

当服务的create成功返回时,将使用结果数据发送created事件。

const feathers = require('@feathersjs/feathers');
const app = feathers();

app.use('/messages', {
  create(data, params) {
    return Promise.resolve(data);
  }
});

// Retrieve the wrapped service object which will be an event emitter
const messages = app.service('messages');

messages.on('created', (message, context) => console.log('created', message));

messages.create({
  text: 'We have to do something!'
});


updated, patched

updatedpatched成功回调时,updatedpatched事件将使用回调数据触发。

const feathers = require('@feathersjs/feathers');
const app = feathers();

app.use('/my/messages/', {
  update(id, data) {
    return Promise.resolve(data);
  },

  patch(id, data) {
    return Promise.resolve(data);
  }
});

const messages = app.service('my/messages');

messages.on('updated', (message, context) => console.log('updated', message));
messages.on('patched', message => console.log('patched', message));

messages.update(0, {
  text: 'updated message'
});

messages.patch(0, {
  text: 'patched message'
});


removed

当服务remove方法成功回调时,removed事件将使用回调数据触发。

const feathers = require('@feathersjs/feathers');
const app = feathers();

app.use('/messages', {
  remove(id, params) {
    return Promise.resolve({ id });
  }
});

const messages = app.service('messages');

messages.on('removed', (message, context) => console.log('removed', message));
messages.remove(1);


Custom events

默认情况下,实时客户端仅接收标准事件。但是,可以将服务上的自定义事件列表定义为service.events,这些事件也会在服务器上调用service.emit('customevent', data)时传递。自定义事件的content不是完整的钩子上下文,而只是包含{app, service, path, result}的对象。

自定义事件只能从服务器发送到客户端,而不是其他方式(客户端到服务器)

例如,在处理付款时向客户端发送状态事件的付款服务类似如下:

class PaymentService {
  constructor() {
    this.events = ['status'];
  },

  create(data, params) {
    createStripeCustomer(params.user).then(customer => {
      this.emit('status', { status: 'created' });
      return createPayment(data).then(result => {
        this.emit('status', { status: 'completed' });
      });
    });
  }
}

数据库适配器还将自定义事件列表作为初始化选项

const service = require('feathers-'); // e.g. `feathers-mongodb`

app.use('/payments', service({
  events: [ 'status' ],
  Model
});

使用service.emit自定义的事件同样可以钓子中发出:

app.service('payments').hooks({
  after: {
    create(context) {
      context.service.emit('status', { status: 'completed' });
    }
  }
});

自定义事件可以像标准事件一样通过通道(channels)发布,并在Feathers客户端或直接在套接字连接上收听:

client.service('payments').on('status', data => {});

socket.on('payments status', data => {});


1.5 Channels-通道

在设置了实时传输(Socket.ioPrimus)的Feathers服务器上,事件通道确定哪些连接的客户端发送实时事件以及应该如何发送数据。

如果没有使用实时传输服务器(如,仅使用REST API或在客户端上使用Feathers时),则通道功能将无法使用。

通的一些应用场景:

  • 实时事件应仅发送给经过身份验证的用户
  • 用户只有在加入某个聊天室时才能获得有关消息的更新
  • 只有同一组织中的用户才能收到有关其数据更改的实时更新
  • 创建新用户时,只应通知管理员
  • 创建,修改或删除用户时,非管理员应仅接收用户对象的“安全”版本(如,仅限emailidavatar


示例

下面的示例显示了生成的channels.js文件,说明了不同部分如何组合在一起:

module.exports = function(app) {
  app.on('connection', connection => {
    // On a new real-time connection, add it to the
    // anonymous channel
    app.channel('anonymous').join(connection);
  });

  app.on('login', (payload, { connection }) => {
    // connection can be undefined if there is no
    // real-time connection, e.g. when logging in via REST
    if(connection) {
      const { user } = connection;

      // The connection is no longer anonymous, remove it
      app.channel('anonymous').leave(connection);

      // Add it to the authenticated user channel
      app.channel('authenticated').join(connection);

      // Channels can be named anything and joined on any condition 
      // E.g. to send real-time events only to admins use

      // if(user.isAdmin) { app.channel('admins').join(connection); }

      // If the user has joined e.g. chat rooms

      // user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(connection))
    }
  });

  // A global publisher that sends all events to all authenticated clients
  app.publish((data, context) => {
    return app.channel('authenticated');
  });
};


Connections-连接

connection是表示实时连接的对象。它与Socket.io中的socket.feathers及Primus中间件中的socket.request.feathers相同。你可以向其添加任何类型的信息,但最值得注意的是,在使用身份验证时,它将包含经过身份验证的用户。默认情况下,一旦客户端在套接字上进行了身份验证(通常通过在客户端上调用app.authenticate()),其位于connection.user中。

可以通过监听app.on('connection', connection => {})app.on('login', (payload,{connection}) => {})来访问连接对象。

当连接终止时,它将自动从所有通道中删除。


app.on('connection')

每次建立新的实时连接时都会触发app.on('connection', connection => {})。这是为匿名用户添加通道连接的好地方(如,我们想向他们发送任何实时更新):

app.on('connection', connection => {
  // On a new real-time connection, add it to the
  // anonymous channel
  app.channel('anonymous').join(connection);
});


app.on('login')

app.on('login', (payload, info) => {})身份验证模块发送,并且还包含作为第二个参数传递的info对象中的连接。请注意,如果通过例如登录进行登录,也可以是undefined。REST不支持实时连接。

这是添加与用户相关的通道的连接的好地方(如:聊天室,管理状态等):

app.on('login', (payload, { connection }) => {
  // connection can be undefined if there is no
  // real-time connection, e.g. when logging in via REST
  if(connection) {
    // The user attached to this connection
    const { user } = connection;

    // The connection is no longer anonymous, remove it
    app.channel('anonymous').leave(connection);

    // Add it to the authenticated user channel
    app.channel('authenticated').join(connection);

    // Channels can be named anything and joined on any condition `
    // E.g. to send real-time events only to admins use
    if(user.isAdmin) {
      app.channel('admins').join(connection);
    }

    // If the user has joined e.g. chat rooms
    user.rooms.forEach(room => {
      app.channel(`rooms/${room.id}`).join(connection);
    });
  }
});

注意,(user, { connection })是ES6标准的(user, meta) => { const connection = meta.connection; },参见解构赋值


app.on('logout')

app.on('logout', (payload, info) => {})身份验证模块发送,同样包含info对象,其会在退出登录时做为第二个参数传入。

如果套接字在注销时也没有断开连接,那么用户应从其channel中删除:

app.on('logout', (payload, { connection }) => {
  if(connection) {
    //When logging out, leave all channels before joining anonymous channel
    app.channel(app.channels).leave(connection);
    app.channel('anonymous').join(connection);
  }
});


Channels-通道/频道

通道是包含许多连接的对象。它可以通过app.channel创建,并允许连接加入或离开。


app.channel(...names)

app.channel(name) -> Channel,当给出单个名称时,返回现有或新命名的频道:

app.channel('admins') // the admin channel
app.channel('authenticated') // the authenticated channel

app.channel(name1, name2, ... nameN) -> Channel,当指定多个名称时,将返回组合通道。组合频道包含所有连接的列表(没有重复),并将channel.join和channel.leave调用重定向到其所有子频道。

// Combine the anonymous and authenticated channel
const combinedChannel = app.channel('anonymous', 'authenticated')

// Join the `anonymous` and `authenticated` channel
combinedChannel.join(connection);

// Join the `admins` and `chat` channel
app.channel('admins', 'chat').join(connection);

// Leave the `admins` and `chat` channel
app.channel('admins', 'chat').leave(connection);

// Make user with `_id` 5 leave the admins and chat channel
app.channel('admins', 'chat').leave(connection => {
  return connection.user._id === 5;
});


app.channels

app.channels -> [string]返回所有已存在的通道名称列表。

app.channel('authenticated');
app.channel('admins', 'users');

app.channels // [ 'authenticated', 'admins', 'users' ]

app.channel(app.channels) // will return a channel with all connections

这对于例如从所有频道中删除连接很有用:

// When a user is removed, make all their connections leave every channel
app.service('users').on('removed', user => {
  app.channel(app.channels).leave(connection => {
    return user._id === connection.user._id;
  });
});


channel.join(connection)

channel.join(connection) -> Channel添加连接到此通道的。如果通道是组合通道,请将连接添加到其所有子通道。如果连接已经在通道中,它什么都不做。其返回值是通道对象。

app.on('login', (payload, { connection }) => {
  if(connection && connection.user.isAdmin) {
    // Join the admins channel
    app.channel('admins').join(connection);

    // Calling a second time will do nothing
    app.channel('admins').join(connection);
  }
});


channel.leave(connection|fn)

channel.leave(connection|fn) -> Channel将一个连接从此通道中移除。如果频道是组合频道,将从其所有子频道中删除该连接。还可以传递为每个连接运行的回调,如果应该删除连接则返回。其返回值是通道对象。

// Make the user with `_id` 5 leave the `admins` channel
app.channel('admins').leave(connection => {
  return connection.user._id === 5;
});


channel.filter(fn)

channel.filter(fn) -> Channel返回通过给定函数过滤的新通道,该函数将传入连接。

// Returns a new channel with all connections of the user with `_id` 5
const userFive = app.channel(app.channels)
  .filter(connection => connection.user._id === 5);


channel.send(data)

channel.send(data) -> Channel返回此通道的副本,其中包含应为此事件发送的自定义数据。通常,这应该通过修改服务方法结果或在context.dispatch中设置客户端“安全”数据来处理,但在某些情况下,仍然可以更改某些通道的事件数据。

将按以下顺序由第一个可用的事件数据确定将发送哪些数据:

  1. 来自channel.send(data)data
  2. context.dispatch
  3. context.result
app.on('connection', connection => {
  // On a new real-time connection, add it to the
  // anonymous channel
  app.channel('anonymous').join(connection);
});

// Send the `users` `created` event to all anonymous
// users but use only the name as the payload
app.service('users').publish('created', data => {
  return app.channel('anonymous').send({
    name: data.name
  });
});

如果连接在多个通道(如:用户和管理员)中,则它将从其所在的第一个通道获取数据。


channel.connections

channel.connections -> [ object ]返回本通道中所有连接的列表


channel.length

channel.length -> integer返回本通道中的连接数


发布

发布者是回调函数,用于返回将事件发送到的通道。它们可以在应用程序和服务级别以及所有或特定事件中注册。发布函数获取事件数据和上下文对象((data, context) => {})并返回命名或组合通道,通道数组或null。一种类型只能注册一个发布者。除标准服务事件名称外,事件名称也可以是自定义事件。context是服务调用的上下文对象,或者是包含自定义事件的{path, service, app, result}的对象。


service.publish([event,] fn)

service.publish([event,] fn) -> service为指定事件或所有事件注册特定服务的发布功能(如果没有给出事件名称)。

app.on('login', (payload, { connection }) => {
  // connection can be undefined if there is no
  // real-time connection, e.g. when logging in via REST
  if(connection && connection.user.isAdmin) {
    app.channel('admins').join(connection);
  }
});

// Publish all messages service events only to its room channel
app.service('messages').publish((data, context) => {
  return app.channel(`rooms/${data.roomId}`);
});

// Publish the `created` event to admins and the user that sent it
app.service('users').publish('created', (data, context) => {
  return [
    app.channel('admins'),
    app.channel(app.channels).filter(connection =>
      connection.user._id === context.params.user._id
    )
  ];
});

// Prevent all events in the `password-reset` service from being published
app.service('password-reset').publish(() => null);


app.publish([event,] fn)

app.publish([event,] fn) -> app为特定事件或所有事件的所有服务注册发布功能(如果没有给出事件名称)。

app.on('login', (payload, { connection }) => {
  // connection can be undefined if there is no
  // real-time connection, e.g. when logging in via REST
  if(connection) {
    app.channel('authenticated').join(connection);
  }
});

// Publish all events to all authenticated users
app.publish((data, context) => {
  return app.channel('authenticated');
});

// Publish the `log` custom event to all connections
app.publish('log', (data, context) => {
  return app.channel(app.channels);
});


发布者优先级

将会按以下顺序找到的第一个发布者回调:

  1. 特定事件的服务发布者
  2. 所有事件的服务发布者
  3. 特定事件的应用发布者
  4. 适用于所有事件的应用发布者


保持通道更新

每个应用程序都不同,因此保持分配给通道的连接是最新的(如,如果用户加入/离开房间)取决于你。

通常,通道应反映你的持久性应用程序数据。这意味着通常它不需要例如要求直接加入通道的用户。这在运行多个应用程序实例时尤其重要,因为通道仅是当前的本地实例。

相反,相关信息(例如,用户当前所在的房间)应该存储在数据库中,然后可以将活动连接重新分配到收听适当服务事件的适当通道中。

以下示例在更新或删除用户对象(假定其房间数组是用户已加入的房间ID列表)时更新指定用户的所有活动连接:

// Join a channel given a user and connection
const joinChannels = (user, connection) => {
  app.channel('authenticated').join(connection);
  // Assuming that the chat room/user assignment is stored
  // on an array of the user
  user.rooms.forEach(room =>
    app.channel(`rooms/${roomId}`).join(connection)
  );
}

// Get a user to leave all channels
const leaveChannels = user => {
  app.channel(app.channels).leave(connection =>
    connection.user._id === user._id
  );
};

// Leave and re-join all channels with new user information
const updateChannels = user => {
  // Find all connections for this user
  const { connections } = app.channel(app.channels).filter(connection =>
    connection.user._id === user._id
  );

  // Leave all channels
  leaveChannels(user);

  // Re-join all channels with the updated user information
  connections.forEach(connection => joinChannels(user, connection));
}

app.on('login', (payload, { connection }) => {
  if(connection) {
    // Join all channels on login
    joinChannels(connection.user, connection);
  }
});

// On `updated` and `patched`, leave and re-join with new room assignments
app.service('users').on('updated', updateChannels);
app.service('users').on('patched', updateChannels);
// On `removed`, remove the connection from all channels
app.service('users').on('removed', leaveChannels);

注意:活动连接数通常是一个(或没有),但除非明确阻止,否则Feathers不会阻止同一用户的多次登录(例如,有两个打开的浏览器窗口或移动设备)。


1.6 错误-Errors

$ npm install @feathersjs/errors --save

@feathersjs/errors模块包含一组标准错误类,所有其他Feathers模块都使用它们以及一个Express错误处理程序来格式化这些错误,并为REST调用设置正确的HTTP状态代码。


Feathers 错误

可以使用以下错误类型,所有这些都是FeathersError的实例:

  • 400: BadRequest
  • 401: NotAuthenticated
  • 402: PaymentError
  • 403: Forbidden
  • 404: NotFound
  • 405: MethodNotAllowed
  • 406: NotAcceptable
  • 408: Timeout
  • 409: Conflict
  • 411: LengthRequired
  • 422: Unprocessable
  • 429: TooManyRequests
  • 500: GeneralError
  • 501: NotImplemented
  • 502: BadGateway
  • 503: Unavailable

所有Feathers插件都会自动发出相应的Feathers错误。例如,大多数数据库适配器已经发送了带有ORM验证错误的冲突或不可处理错误。

Feathers错误会包含以下字段:

  • name - 错误名 (如 "BadRequest", "ValidationError" 等)
  • message - 错误消息字符串
  • code - HTTP 状态码
  • className - 一个CSS类名,可以根据错误类型设置样式错误。 (例如"bad-request" 等)
  • data - 包含传递给Feathers错误的任何对象的对象,除了errors对象。
  • errors - 包含任何内容的对象传递给errors中的Feathers错误。通常是验证错误,或者你想要将多个错误组合在一起。

要将Feathers错误转换回对象,可以调用error.toJSON()。JavaScript Error对象的正常console.log不会自动显示上述的其他属性(即使可以直接访问它们)。


自定义错误

可以通过扩展FeathersError来创建自定义错误,其构造函数为(msg, name, code, className, data)

  • message - 错误消息
  • code - HTTP 状态码
  • className - 错误CSS类名
  • data - 包含在错误中的额外数据。
const { FeathersError } = require('@feathersjs/errors');

class UnsupportedMediaType extends FeathersError {
  constructor(message, data) {
    super(message, 'unsupported-media-type', 415, 'UnsupportedMediaType', data);
  }
}

const error = new UnsupportedMediaType('Not supported');

console.log(error.toJSON());


示例

可以使用以下几种方法:

const errors = require('@feathersjs/errors');

// If you were to create an error yourself.
const notFound = new errors.NotFound('User does not exist');

// You can wrap existing errors
const existing = new errors.GeneralError(new Error('I exist'));

// You can also pass additional data
const data = new errors.BadRequest('Invalid email', {
  email: 'sergey@google.com'
});

// You can also pass additional data without a message
const dataWithoutMessage = new errors.BadRequest({
  email: 'sergey@google.com'
});

// If you need to pass multiple errors
const validationErrors = new errors.BadRequest('Invalid Parameters', {
  errors: { email: 'Email already taken' }
});

// You can also omit the error message and we'll put in a default one for you
const validationErrors = new errors.BadRequest({
  errors: {
    email: 'Invalid Email'
  }
});


服务端错误

如果没有添加catch()语句,则Pomise会吞下错误。因此,你应该确保始终在的Promise上调用.catch()。要在全局级别捕获未捕获的错误,可以将以下代码添加到最顶层的文件中:

process.on('unhandledRejection', (reason, p) => {
  console.log('Unhandled Rejection at: Promise ', p, ' reason: ', reason);
});


错误处理

确保在返回客户端之前清除错误非常重要。Express错误处理中间件仅适用于REST调用。如果想同时处理ws错误,则需要使用App Hooks。App Error Hooks会在每次服务调用出错时调用,而无论使用何种传输。

以下是一个错误处理程序示例,你可以添加到app.hooks错误:

const errors = require("@feathersjs/errors");
const errorHandler = ctx => {
  if (ctx.error) {
    const error = ctx.error;
    if (!error.code) {
      const newError = new errors.GeneralError("server error");
      ctx.error = newError;
      return ctx;
    }
    if (error.code === 404 || process.env.NODE_ENV === "production") {
      error.stack = null;
    }
    return ctx;
  }
};

添加到error.all钩子中:

module.exports = {
  //...
  error: {
    all: [errorHandler],
    find: [],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: []
  }
};


1.7 Configuration

$ npm install @feathersjs/configuration --save

@feathersjs/configurationnode-config的包装器,允许配置服务器端Feathers应用程序。

默认情况下,此实现将在config/*中查找default.json,它保留约定。根据配置文档,可以组织“应用程序部署的分层配置”。有关如何实现此操作的更多信息,请参阅下面的使用部分。


用法

@feathersjs/configuration模块是一个应用程序配置函数,它接受根目录(类似于__dirname)和配置文件夹(默认设置为config):

const feathers = require('@feathersjs/feathers');
const configuration = require('@feathersjs/configuration');

// Use the application root and `config/` as the configuration folder
let app = feathers().configure(configuration())


更改配置目录的位置

默认情况下,Feathers会使用项目源码根目录下config/目录做为配置目录。如果要修改这一配置,可以在app.js中引入@feathersjs/configuration之前配置NODE_CONFIG_DIR环境变量:

process.env['NODE_CONFIG_DIR'] = path.join(__dirname, 'config/')
const configuration = require('@feathersjs/configuration')

@feathersjs/configuration并不直接使用NODE_CONFIG_DIR环境变量,而是由它使用的node-config模块使用。有关配置node-config设置的更多信息,请参阅Configuration Files Wiki页面。


变量类型

@feathersjs/configuration使用以下变量机制:

  • 通过指定的根路径和配置路径加载default.json
  • 还会尝试在该路径中加载<NODE_ENV>.json,如果找到,则扩展默认配置
  • 浏览每个配置值并在应用程序上设置它(通过app.set(name, value)):
    • 如果该值是有效的环境变量(如NODE_ENV),则使用其值
    • 如果值以./../开头,则将其转换为相对于配置文件路径的绝对路径
    • 如果值被转义(以\开头),则始终使用该值(例如\\ NODE_ENV将变为NODE_ENV
  • default<env>配置都可以是模块,它们使用module.exports = {...}.js文件后缀提供计算设置。有关示例,请参阅test/config/testing.js

以上所有规则都适用于.js模块。


示例

config/default.json中,我们要使用本地开发环境和默认的MongoDB连接字符串:

{
  "frontend": "../public",
  "host": "localhost",
  "port": 3030,
  "mongodb": "mongodb://localhost:27017/myapp",
  "templates": "../templates"
}

config/production.json中,我们将使用环境变量(例如由Heroku设置)并使用public/dist来加载前端生成构建:

{
  "frontend": "./public/dist",
  "host": "myapp.com",
  "port": "PORT",
  "mongodb": "MONGOHQ_URL"
}

现在可以在app.js像下面这样使用:

const feathers = require('@feathersjs/feathers');
const configuration = require('@feathersjs/configuration');

let conf = configuration();

let app = feathers()
  .configure(conf);

console.log(app.get('frontend'));
console.log(app.get('host'));
console.log(app.get('port'));
console.log(app.get('mongodb'));
console.log(app.get('templates'));
console.log(conf());

如果运行:

node app
// -> path/to/app/public
// -> localhost
// -> 3030
// -> mongodb://localhost:27017/myapp
// -> path/to/templates

或者通过自定义环境变量在config/custom-environment-variables.json中设置:

{
  "port": "PORT",
  "mongodb": "MONGOHQ_URL"
}
{
  "port": "PORT",
  "mongodb": "MONGOHQ_URL"
}

还可以通过参数重写这些变量,请参阅node-config


2. Transports-将 Feathers 应用公开为API服务器

2.1 Express

$ npm install @feathersjs/express --save

@feathersjs/expres
模块包含了Express框架与Feathers的集成:

const express = require('@feathersjs/express');


express(app)

express(app) -> app是一个将Feathers应用转换为完全与Express(4+)兼容应用程序的功能,除了Feathers功能之外,还允许你使用Express API

const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');

// Create an app that is a Feathers AND Express application
const app = express(feathers());

@feathersjs/express (express) 同样也暴露了标准的Expressk中间件:

  • express.json - JSON 请求体解析
  • express.urlencoded - URL编码的请求体解析器
  • express.static - 静态托管文件夹中的文件
  • express.Router - 创建Express路由器对象


express()

如果未传入Feathers应用,则express() -> app会返回一个普通的Express应用,就像正常调用Express一样。


app.use(path, service|mw|[mw])

app.use(path, service|mw|[mw]) -> app在指定路径上注册一个服务对象Experss中间件或Experss中间件列表。如果传入服务对象,它将使用Feathers注册机制,用于中间件函数Express。

// Register a service
app.use('/todos', {
  get(id) {
    return Promise.resolve({ id });
  }
});

// Register an Express middleware
app.use('/test', (req, res) => {
  res.json({
    message: 'Hello world from Express middleware'
  });
});

// Register multiple Express middleware functions
app.use('/test', (req, res, next) => {
  res.data = 'Step 1 worked';
  next();
}, (req, res) => {
  res.json({
    message: 'Hello world from Express middleware ' + res.data
  });
});


app.listen(port)

app.listen(port) -> HttpServer将首先调用Express的app.listen,然后在内部也调用Feathers 的app.setup(server)

// Listen on port 3030
const server = app.listen(3030);

server.on('listening', () => console.log('Feathers application started'));


app.setup(server)

app.setup(server) -> app通常在app.listen内部调用,但在下面描述的情况下需要显式调用。


Sub-Apps-子应用

将应用程序注册为子应用程序时,必须调用app.setup(server)来初始化子应用程序服务。

const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');

const api = express(feathers())
  .configure(express.rest())
  .use('/service', myService);

const mainApp = express().use('/api/v1', api);

const server = mainApp.listen(3030);

// Now call setup on the Feathers app with the server
api.setup(server);

建议避免使用复杂的子应用程序设置,因为内置身份验证的websockets和Feathers目前还不完全支持子应用程序。


HTTPS

HTTPS需要创建单独的服务器,在这种情况下,还必须显式调用app.setup(server)

const fs = require('fs');
const https  = require('https');

const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');

const app = express(feathers());

const server = https.createServer({
  key: fs.readFileSync('privatekey.pem'),
  cert: fs.readFileSync('certificate.pem')
}, app).listen(443);

// Call app.setup to initialize all services and SocketIO
app.setup(server);


虚拟主机

vhostExpress中间件可用于在虚拟主机上运行Feathers应用程序,但同样需要显式调用app.setup(server)

const vhost = require('vhost');

const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');

const app = express(feathers());

app.use('/todos', todoService);

const host = express().use(vhost('foo.com', app));
const server = host.listen(8080);

// Here we need to call app.setup because .listen on our virtal hosted
// app is never called
app.setup(server);


express.rest()

express.rest注册了一个Feathers传输机制,允许您通过RESTful API公开和使用服务。这意味着你可以通过GETPOSTPUTPATCHDELETE HTTP方法调用服务方法:

服务方法 HTTP方法 路径
.find() GET /messages
.get() GET /messages/1
.create() POST /messages
.update() PUT /messages/1
.patch() PATCH /messages/1
.remove() DELETE /messages/1

要通过RESTful API公开服务,则必须配置express.rest并提供我们自己的正文解析器中间件(通常是标准的Express 4 body-parser)来使REST .create.update.patch调用解析HTTP中的数据体 如果想在REST处理程序之前添加其他中间件,需要在注册任何服务之前调用app.use(middleware)

body-parser中间件必须在任何服务之前注册。否则,服务方法将抛出No data provided或者First parameter for 'create' must be an object错误。


app.configure(express.rest())

使用标准格式化程序通过res.json发送JSON响应来配置传输提供程序。

const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');

// Create an Express compatible Feathers application
const app = express(feathers());

// Turn on JSON parser for REST services
app.use(express.json())
// Turn on URL-encoded parser for REST services
app.use(express.urlencoded({ extended: true }));
// Set up REST transport
app.configure(express.rest())


app.configure(express.rest(formatter))

默认的REST响应格式化程序是一个中间件,它将服务检索的数据格式化为JSON。如果您想配置自己的formatter中间件,请使用formatter(req, res)函数。该中间件可以访问res.data,它是服务返回的数据。res.format可用于内容协商。

const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');

const app = express(feathers());

// Turn on JSON parser for REST services
app.use(express.json())
// Turn on URL-encoded parser for REST services
app.use(express.urlencoded({ extended: true }));
// Set up REST transport
app.configure(express.rest(function(req, res) {
  // Format the message as text/plain
  res.format({
    'text/plain': function() {
      res.end(`The Message is: "${res.data.text}"`);
    }
  });  
}))


自定义服务中间件

只会在指定服务之前或之后运行的自定义Express中间件,可以按其应运行的顺序传递给app.use

const todoService = {
  get(id) {
    return Promise.resolve({
      id,
      description: `You have to do ${id}!`
    });
  }
};

app.use('/todos', ensureAuthenticated, logRequest, todoService, updateData);

在服务之后运行的中间件具有可用的服务调用信息:

  • res.data - 将要发送的数据
  • res.hook - 服务方法调用的钩子上下文

例如,updateData可以像下面这样:

function updateData(req, res, next) {
  res.data.updateData = true;
  next();
}

如果在服务之后在自定义中间件中运行res.send并且不调用next,则将跳过其他中间件(如REST格式化程序)。这可以用于如为某些服务方法调用呈现不同的视图。


params

在REST传输之后注册的所有中间件都可以访问req.feathers对象以设置服务方法的params属性:

const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const bodyParser = require('body-parser');

const app = express(feathers());

app.configure(express.rest())
  .use(bodyParser.json())
  .use(bodyParser.urlencoded({extended: true}))
  .use(function(req, res, next) {
    req.feathers.fromMiddleware = 'Hello world';
    next();
  });

app.use('/todos', {
  get(id, params) {
    console.log(params.provider); // -> 'rest'
    console.log(params.fromMiddleware); // -> 'Hello world'

    return Promise.resolve({
      id, params,
      description: `You have to do ${id}!`
    });
  }
});

app.listen(3030);

可以通过运行示例和访问来查看设置的参数:

http://localhost:3030/todos/test

应该避免直接设置req.feathers = something,因为它可能已包含其他Feathers插件所依赖的信息。添加单个属性或使用bject.assign(req.feathers, something)是更可靠的选择。

注意:由于Express中间件的顺序很重要,任何设置服务参数的中间件都必须在服务之前注册(在app.configure(services)middleware/index.js之前的生成应用程序中)。


params.query

params.query包含来自客户端的URL请求参数。对REST传输,将使用qs模块进行解析查询字符串。查询字符串示例,请参阅数据库查询章节。

只有params.query在服务器和客户端之间传递,而params的其他部分则不是。这是出于安全原因,因此客户端无法设置params.user或数据库选项等内容。你始终可以将params.query映射到before钩子中的其他params属性。

示例:

GET /messages?read=true&$sort[createdAt]=-1

将会设置params.query为:

{
  "read": "true",
  "$sort": { "createdAt": "-1" }
}

注意:如果请求中的数组包含20个以上的项,则qs解析器会将其隐式转换为索引为键的对象。要扩展此限制,可以设置自定义查询解析器:app.set('query parser', str => qs.parse(str, {arrayLimit: 1000}))


params.provider

对于通过REST params.provider进行的任何服务方法调用将被设置为rest。在钩子中,这可以用于例如防止外部用户进行服务方法调用:

app.service('users').hooks({
  before: {
    remove(context) {
      // check for if(context.params.provider) to prevent any external call
      if(context.params.provider === 'rest') {
        throw new Error('You can not delete a user via REST');
      }
    }
  }
});


params.route

参阅:路由章节


express.notFound(options)

express.notFound()会返回一个返回NotFound(404)Featers错误的中间件。应该用作错误处理程序之前的最后一个中间件。可以使用以下选项:

  • verbose:如果URL应包含在错误消息中,则设置为true(默认值:false
// Return errors that include the URL
app.use(express.notFound({ verbose: true });
app.use(errorHandler());


express.errorHandler()

express.errorHandler()是一个Express错误处理中间件,它将对REST调用的任何错误响应格式化为JSON(或HTML,如直接在浏览器中访问我们的API)并设置相应的错误代码。

你仍然可以使用任何其他与Express兼容的Express兼容错误中间件。实际上,express.errors只是一个微定制的。

就像在Express中一样,错误处理程序必须在所有中间件和服务之后注册。


app.use(express.errorHandler())

使用默认配置设置错误处理程序。

const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');

const app = express(feathers());

// before starting the app
app.use(express.errorHandler())


app.use(express.errorHandler(options))
const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');

const app = express(feathers());

// Just like Express your error middleware needs to be
// set up last in your middleware chain.
app.use(express.errorHandler({
    html: function(error, req, res, next) {
      // render your error view with the error object
      res.render('error', error);
    }
}));

app.use(errorHandler({
    html: {
      404: 'path/to/notFound.html',
      500: 'there/will/be/robots.html'
    }
}));

如果希望以json格式获得响应,应该确保将请求中的Accept标头设置为application/json,否则默认错误处理程序将返回HTML。

创建新的localstorage服务时,可以传递以下选项:

  • html (Function|Object) [可选] - 自定义格式化程序函数或包含自定义html错误页面路径的对象。
  • logger (Function|false) (默认: console) - 设置记录器对象以记录错误 (将会使用 logger.error(error)


Routing-路由

服务URL中的Express路径占位符将添加到服务params.route中。

有关何时使用及何时不使用嵌套路由的详细信息,请参阅嵌套路由的FAQ

const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');

const app = express(feathers());

app.configure(express.rest())
  .use(function(req, res, next) {
    req.feathers.fromMiddleware = 'Hello world';
    next();
  });

app.use('/users/:userId/messages', {
  get(id, params) {
    console.log(params.query); // -> ?query
    console.log(params.provider); // -> 'rest'
    console.log(params.fromMiddleware); // -> 'Hello world'
    console.log(params.route.userId); // will be `1` for GET /users/1/messages

    return Promise.resolve({
      id,
      params,
      read: false,
      text: `Feathers is great!`,
      createdAt: new Date().getTime()
    });
  }
});

app.listen(3030);


2.2 Socket.io

$ npm install @feathersjs/socketio --save

@feathersjs/socketio模块允许通过Socket.io调用服务方法并接收实时事件,Socket.io是一个NodeJS库,支持实时双向、基于事件的通信。


配置

@feathersjs/socketio可以单独使用,也可以与Express之类的Feathers框架集成一起使用。>


app.configure(socketio())

使用app.listen提供的服务器或在app.setup(server)中传递的默认配置设置Socket.io传输。

const feathers = require('@feathersjs/feathers');
const socketio = require('@feathersjs/socketio');

const app = feathers();

app.configure(socketio());

app.listen(3030);

一旦服务器通过app.listenapp.setup(server)启动,Socket.io对象就会以app.io形式提供。


app.configure(socketio(callback))

使用默认配置设置Socket.io传输,并使用Socket.io服务器对象调用callback。这是收听自定义事件或添加授权的合适位置:

const feathers = require('@feathersjs/feathers');
const socketio = require('@feathersjs/socketio');

const app = feathers();

app.configure(socketio(function(io) {
  io.on('connection', function(socket) {
    socket.emit('news', { text: 'A client connected!' });
    socket.on('my other event', function (data) {
      console.log(data);
    });
  });

  // Registering Socket.io middleware
  io.use(function (socket, next) {
    // Exposing a request property to services and hooks
    socket.feathers.referrer = socket.request.referrer;
    next();
  });
}));

app.listen(3030);


app.configure(socketio(options [, callback]))

使用指定的Socket.io选项对象设置Socket.io传输,并可选的调用上面所述的回调。

这可以用于例如配置Socket.io初始化的路径(默认为socket.io/)。以下更改了ws/的路径:

const feathers = require('@feathersjs/feathers');
const socketio = require('@feathersjs/socketio');

const app = feathers();

app.configure(socketio({
  path: '/ws/'
}, function(io) {
  // Do something here
  // This function is optional
}));

app.listen(3030);


app.configure(socketio(port, [options], [callback]))

在独立端口上创建新的Socket.io服务器。选项(Options)和回调(callback)是可选的,并按上述方式工作。

const feathers = require('@feathersjs/feathers');
const socketio = require('@feathersjs/socketio');

const app = feathers();

app.configure(socketio(3031));
app.listen(3030);


params

Socket.io中间件可以修改socket上的feathers属性,然后将其用作服务调用的params

app.configure(socketio(function(io) {
  io.use(function (socket, next) {
    socket.feathers.user = { name: 'David' };
    next();
  });
}));

app.use('messages', {
  create(data, params, callback) {
    // When called via SocketIO:
    params.provider // -> socketio
    params.user // -> { name: 'David' }
  }
});

socket.feathers通道中的connection是同一个对象。socket.requestsocket.handshake包含发起连接的HTTP请求的信息(请参阅Socket.io文档)。


params.provider

对于通过Socket.io params.provider进行的任何服务方法调用都将设置为socketio。在钩子中,这可以用于像防止外部用户进行服务方法调用:

app.service('users').hooks({
  before: {
    remove(context) {
      // check for if(context.params.provider) to prevent any external call
      if(context.params.provider === 'socketio') {
        throw new Error('You can not delete a user via Socket.io');
      }
    }
  }
});


params.query

params.query会包含从客户端发送的查询参数。


params.connection

params.connection是可以与通道一起使用的连接对象。它与Socket.io中间件中的socket.feathers是同一个对象,如params部分所示。


2.3 Primus

$ npm install @feathersjs/primus --save

@feathersjs/primus模块允许通过Primus调用服务方法并接收实时事件,Primus是支持Engine.IO,WebSockets,Faye,BrowserChannel,SockJS和Socket.IO的实时框架的通用包装器。


配置

@feathersjs/primus模块外,还需要安装你所需要的websocket库:

$ npm install ws --save


app.configure(primus(options))

通过Primus选项设置Primus传输。

使用app.listen()app.setup(server)启动服务器后,Primus服务器对象将通过app.primus提供。

const feathers = require('@feathersjs/feathers');
const primus = require('@feathersjs/primus');

const app = feathers();

// Set up Primus with SockJS
app.configure(primus({ transformer: 'ws' }));

app.listen(3030);


app.configure(primus(options, callback))

通过Primus选项设置Primus传输,并使用Primus服务器实例调用回调。

const feathers = require('@feathersjs/feathers');
const primus = require('@feathersjs/primus');

const app = feathers();

// Set up Primus with SockJS
app.configure(primus({
  transformer: 'ws'
}, function(primus) {
  // Do something with primus object
}));

app.listen(3030);


params-参数

Primus请求对象有一个feathers属性,可以在授权期间使用其他服务的params进行扩展:

app.configure(primus({
  transformer: 'ws'
}, function(primus) {
  // Do something with primus
  primus.use('feathers-referrer', function(req, res){
    // Exposing a request property to services and hooks
    req.feathers.referrer = request.referrer;
  });
}));

app.use('messages', {
  create(data, params, callback) {
    // When called via Primus:
    params.referrer // referrer from request
  }
});


params.provider

对于通过Primus套接字进行的任何服务方法调用,params.provider将被设置为primus。在钩子中,这可以用于例如防止外部用户进行服务方法调用:

app.service('users').hooks({
  before: {
    remove(context) {
      // check for if(context.params.provider) to prevent any external call
      if(context.params.provider === 'primus') {
        throw new Error('You can not delete a user via Primus');
      }
    }
  }
});


params.query

params.query会包含从客户端发送的查询参数。


params.connection

params.connection是可以与通道一起使用的连接对象。它与Primus中间件中的socket.feathers是同一个对象,如params部分所示。


3. Client-关于如何在客户端上使用Feathers的详细介绍

3.1 用法

Feathers最显著的一点是它也可以用作客户端。与大多数其他框架相比,它并不是一个单独的库;相反,你可以客户端和服务器获得完全相同的功能。也就是说你可以使用服务钩子并配置插件。默认情况下,Feathers客户端会自动创建与Feathers服务器通信的服务。

为了连接到Feathers服务器,客户端创建使用REST或websocket连接来中继方法调用并允许监听服务器上的事件Serivce。客户端的Feathers应用实例的使用方式与服务器上的完全相同。

与客户最相关的模块是:

提示:不用在客户端上使用Feathers连接到Feathers服务器。前面的客户端章节还描述了如何在客户端没有Feathers的情况下直接使用REST HTTP、Socket.io或Primus连接。有关验证的信息,请参阅验证客户端章节

本章介绍如何使用Webpack或Browserify等模块加载器或通过<script>标记将Feathers设置为Node、React Native和浏览器中的客户端。这些示例使用的是Socket.io客户端。有关其他连接方法,请参阅上面链接的章节。

可以通过各个模块或@feathersjs/rest-client模块在客户端上使用Feathers。后者将上述所有模块组合成一个ES5转换版本。


Node

要连接到NodeJS中的Feathers服务器,请安装所需的客户端连接库(此处为socket.io-client)、Feathers核心库以及特定于连接的库:

npm install @feathersjs/feathers @feathersjs/socketio-client socket.io-client --save

然后初始化:

const io = require('socket.io-client');
const feathers = require('@feathersjs/feathers');
const socketio = require('@feathersjs/socketio-client');

const socket = io('http://api.my-feathers-server.com');
const client = feathers();

client.configure(socketio(socket));

const messageService = client.service('messages');

messageService.on('created', message => console.log('Created a message', message));

// Use the messages service from the server
messageService.create({
  text: 'Message from client'
});


React Native

React Native使用与Node客户端相同。将所需的软件包安装到React Native项目中。

$ npm install @feathersjs/feathers @feathersjs/socketio-client socket.io-client

然后在主应用程序文件中:

import io from 'socket.io-client';
import feathers from '@feathersjs/feathers';
import socketio from '@feathersjs/socketio-client';

const socket = io('http://api.my-feathers-server.com', {
  transports: ['websocket'],
  forceNew: true
});
const client = feathers();

client.configure(socketio(socket));

const messageService = client.service('messages');

messageService.on('created', message => console.log('Created a message', message));

// Use the messages service from the server
messageService.create({
  text: 'Message from client'
});

由于React Native for Android不会处理超过一分钟的超时,因此需要考虑在服务器上设置较低的pingInterval值和feather-socketiopingTimeout。这将停止与此类警告的相关issue。例如:

const app = feathers();
const socketio = require('feathers-socketio');

app.configure(socketio({
  pingInterval: 10000,
  pingTimeout: 50000
}));


模块加载器

@feathersjs命名空间中的所有模块都使用了ES6。不完全支持ES6的浏览器中,必须对它们进行转换为。大多数客户端模块加载器排除了node_modules文件夹的转换,必须配置为包含@feathersjs命名空间和调试模块中的模块。


Webpack

对于Webpack,推荐的babel-loader规则通常会排除node_modules中的所有内容。需要对它调整以跳过node_modules/@feathersjsnode_modules/debug。在webpack.config.js的模块规则中,将babel-loader部分更新为:

{
  test: /\.jsx?$/,
  exclude: /node_modules(\/|\\)(?!(@feathersjs|debug))/,
  loader: 'babel-loader'
}


create-react-app

create-react-app使用Webpack,但除非弹出,否则不允许修改配置。如果不想弹出,请改用@feathersjs/client模块。

npm i --save @feathersjs/client

然后,可以从这个包导入已转换的库:

import feathers from "@feathersjs/client";


Browserify

在Browserify中,必须使用babelify变换。所有Feathers包都会示明他们需要转换,应该自动进行转换。


其它

如上所述,当使用任何使用了转换器的模块加载器时,node_modules/@feathersjs及其所有子文件夹必须包含在 ES6+ 的转换步骤。对于非CommonJS格式(如AMD)和兼容ES5的Feathers及其客户端模块,可以使用@feathersjs/client模块。


@feathersjs/client

$ npm install @feathersjs/client --save
@feathersjs/client是一个模块,它会将单独的Feathers客户端模块捆绑为一个模块,并提供与ES5兼容的代码,与现代浏览器(IE10 +)兼容。它也可以通过<script>标签直接在浏览器中使用。这是一个包含Feathers客户端模块的表:
Feathers module @feathersjs/client
@feathersjs/feathers feathers (default)
@feathersjs/errors feathers.errors
@feathersjs/rest-client feathers.rest
@feathersjs/socketio-client feathers.socketio
@feathersjs/primus-client feathers.primus
@feathersjs/authentication-client feathers.authentication

Feathers客户端库被转换为ES5,需要通过babel-polyfill模块或在旧版浏览器中包含core.js来提供ES6填充程序,如:通过<script type =“text / javascript”src =“// cdnjs.cloudflare.com/ajax/libs/core-js/2.1.4/core.min.js”></ script>

当你加载@feathersjs/client时,不必安装或加载上表中列出的任何其他模块。


何时使用

@feathersjs/client可以直接在浏览器中使用<script>标签而不使用模块加载器,也可以使用不支持CommonJS的模块加载器(如RequireJS)或使用默认的create-react-app创建的React应用程序。

如果使用带有Node或React Native的Feathers客户端,则应遵循NodeReact Native部分中描述的步骤,而不是使用@feathersjs/client


使用模块加载器加载

可以将@feathersjs/client与其他浏览器模块加载器/捆绑器一起使用(而不是直接使用模块),但它可能会包含你不会用到的软件包,并导致软件包较大。

import io from 'socket.io-client';
import feathers from '@feathersjs/client';

// Socket.io is exposed as the `io` global.
const socket = io('http://localhost:3030');
// @feathersjs/client is exposed as the `feathers` global.
const app = feathers();

app.configure(feathers.socketio(socket));
app.configure(feathers.authentication());

app.service('messages').create({
  text: 'A new message'
});

// feathers.errors is an object with all of the custom error types.


使用<script>从CDN加载

如下所示,你可以从unpkg.com加载@feathersjs/client

<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/core-js/2.1.4/core.min.js"></script>
<script src="//unpkg.com/@feathersjs/client@^3.0.0/dist/feathers.js"></script>
<script src="//unpkg.com/socket.io-client@1.7.3/dist/socket.io.js"></script>
<script>
  // Socket.io is exposed as the `io` global.
  var socket = io('http://localhost:3030');
  // @feathersjs/client is exposed as the `feathers` global.
  var app = feathers();

  app.configure(feathers.socketio(socket));
  app.configure(feathers.authentication());

  app.service('messages').create({
    text: 'A new message'
  });

  // feathers.errors is an object with all of the custom error types.
</script>


RequireJS

以下是使用RequireJS语法加载@feathersjs/client的方法:

define(function (require) {
  const feathers = require('@feathersjs/client');
  const io = require('socket.io-client');

  const socket = io('http://localhost:3030');
  // @feathersjs/client is exposed as the `feathers` global.
  const app = feathers();

  app.configure(feathers.socketio(socket));
  app.configure(feathers.authentication());

  app.service('messages').create({
    text: 'A new message'
  });

  return const;
});


3.2 REST

要在客户端上不使用Feathers而直接使用Feathers REST API(通过HTTP),请参阅HTTP API部分。


@feathersjs/rest-client

$ npm install @feathersjs/rest-client --save

@feathersjs/rest-client允许使用jQueryrequestSuperagentAxiosFetch作为AJAX库连接到通过Express REST API公开的服务。

REST客户端服务会发出createdupdatedpatchedremoved,但仅在本地为其自身的实例发出。来自其他客户端的实时事件只能通过websocket连接接收。

客户端应用程序只能使用一种传输(REST、Socket.io或Primus)。通常不需要在同一客户端应用程序中使用两个传输。


rest([baseUrl])

可以通过加载@feathersjs/rest-client,并使用基本URL初始化客户端对象来初始化REST客户端服务:

const myService = {
  find(params) {
    return Promise.resolve([]);
  },
  get(id, params) {},
  create(data, params) {},
  update(id, data, params) {},
  patch(id, data, params) {},
  remove(id, params) {},
  setup(app, path) {}
}

app.use('/my-service', myService);
const myService = {
  async find(params) {
    return [];
  },
  async get(id, params) {},
  async create(data, params) {},
  async update(id, data, params) {},
  async patch(id, data, params) {},
  async remove(id, params) {},
  setup(app, path) {}
}

app.use('/my-service', myService);

在浏览器中,基本URL与注册服务的位置相关。意味着http://api.feathersjs.com/api/v1/messages上的基本URL为http://api.feathersjs.com,其服务将以app.service('api/v1/messages')的形式提供。使用http://api.feathersjs.com/api/v1为基本URL,其服务将是app.service('messages')


params.headers

获取指定的请求头可以通过服务调用中的params.headers

app.service('messages').create({
  text: 'A message from a REST client'
}, {
  headers: { 'X-Requested-With': 'FeathersJS' }
});


params.connection

可以传递特定于AJAX库的其他选项。params.connection.headers将与params.headers合并:

app.configure(restClient.request(request));

app.service('messages').get(1, {
  connection: {
    followRedirect: false
  }
});

使用fetchforkyetch,它也可以用于中止请求:

const yetch = require('yetch');
const controller = new AbortController();

app.configure(restClient.fetch(yetch));

const promise = app.service('messages').get(1, {
  connection: {
    signal: controller.signal
  }
});

promise.abort();


jQuery

restClient.jquery传递jQuery($)实例:

app.configure(restClient.jquery(window.jQuery));

或使用模块加载器:

import $ from 'jquery';

app.configure(restClient.jquery($));


Request

需要将request对象显式传递给feathers.request。使用feathers.defaults(创建新的请求对象)是设置默认标头或身份验证信息的好方式:

const request = require('request');
const requestClient = request.defaults({
  'auth': {
    'user': 'username',
    'pass': 'password',
    'sendImmediately': false
  }
});

app.configure(restClient.request(requestClient));


Superagent

目前使用默认配置:

const superagent = require('superagent');

app.configure(restClient.superagent(superagent));


Axios

Axios目前使用默认配置:

const axios = require('axios');

app.configure(restClient.axios(axios));


Fetch

Fetch同样使用默认配置:

// In Node
const fetch = require('node-fetch');

app.configure(restClient.fetch(fetch));

// In modern browsers
app.configure(restClient.fetch(window.fetch));


HTTP API

可以使用任何其他HTTP REST客户端与Feathers REST API进行通信。以下部分描述了HTTP请求方法、请求正文和查询参数属于哪个服务方法调用。

在服务器端,URL中的所有查询参数都将设置为params.query。其他服务参数可以通过hooksExpress中间件设置。URL查询参数值始终为字符串。转换(如:字符串'true'转换为布尔值true)也可以在钩子中完成。

POSTPUTPATCH的请求体类型由Expressbody-parser中间件确定,该中间件必须在所有服务前注册。还应该确保将Accept标头设置为application/json


Authentication

验证HTTP(REST)请求需要两步。首先,必须POST使用的策略从身份验证服务获取JWT:

// POST /authentication the Content-Type header set to application/json
{
  "strategy": "local",
  "email": "your email",
  "password": "your password"
}

使用CURL像下面这样:

curl -H "Content-Type: application/json" -X POST -d '{"strategy":"local","email":"your email","password":"your password"}' http://localhost:3030/authentication

然后,对于要验证的后续请求,需要将返回的accessToken添加到Authorization请求头:

curl -H "Content-Type: application/json" -H "Authorization: <your access token>" -X POST http://localhost:3030/authentication

详细信息参阅JWTLocal验证。


find

从服务中检索所有匹配资源的列表:

GET /messages?status=read&user=10

以上请求,在服务端将调用messages.find({ query: { status: 'read', user: '10' } })

如果想使用任何内置的查找操作数($le$lt$ne$eq$in等),一般格式如下:

GET /messages?field[$operand]=value&field[$operand]=value2

例如,想查找status字段值为active的记录:

GET /messages?status[$ne]=active

更多关于官方数据库适配器可用参数的信息,请参阅数据库查询部分


get

从服务中检索单条资源:

GET /messages/1

以上请求,在服务端将调用messages.get(1, {})

GET /messages/1?fetch=all

在服务端将调用messages.get(1, { query: { fetch: 'all' } })


create

使用data创建新资源,其也可能是个数组:

POST /messages
{ "text": "I really have to iron" }

以上在服务端将调用messages.create({ "text": "I really have to iron" }, {})

POST /messages
[
  { "text": "I really have to iron" },
  { "text": "Do laundry" }
]


update

完整更新1或多条资源:

PUT /messages/2
{ "text": "I really have to do laundry" }

在服务端将调用messages.update(2, { "text": "I really have to do laundry" }, {})。通过直接向服务端发送请求,而没有给出id,如:

PUT /messages?complete=false
{ "complete": true }

其在服务器端将调用messages.update(null, { "complete": true }, { query: { complete: 'false' } })

通常update会替换整个资源,这就是数据库适配器仅支持patch更新多条记录的原因。


patch

将已存在的一或多条记录与data合并:

PATCH /messages/2
{ "read": true }

其在服务器端将调用messages.patch(2, { "read": true }, {})。通过直接向服务端发送请求,而没有给出id,如:

PATCH /messages?complete=false
{ "complete": true }

将会在服务器端调用messages.patch(null, { complete: true }, { query: { complete: 'false' } })以修改所有读取到的消息的状态。


remove

删除一或多条资源:

DELETE /messages/2?cascade=true

将会调用messages.remove(2, { query: { cascade: 'true' } })

如果未指定id直接请求,如:

DELETE /messages?read=true

则会调用messages.remove(null, { query: { read: 'true' } })以删除所有已读消息。


3.3 Socket.io

如果可能,建议在客户端上使用Feathers@feathersjs/socketio-client模块。但是,如果要在客户端上不使用Feathers而直接使用Socket.io连接,请参阅直接连接部分。


@feathersjs/socketio-client

$ npm install @feathersjs/socketio-client --save

@feathersjs/socketio-client模块允许通过Socket.io套接字连接到使用Socket.io服务器公开的服务。

Socket.io也用于调用服务方法。使用socket来调用方法和接收实时事件通常比使用REST更快。因此,无需在同一客户端应用程序中同时使用REST和Socket.io。


socketio(socket)

使用指定socket和默认选项初始化Socket.io客户端:

const feathers = require('@feathersjs/feathers');
const socketio = require('@feathersjs/socketio-client');
const io = require('socket.io-client');

const socket = io('http://api.feathersjs.com');
const app = feathers();

// Set up Socket.io client with the socket
app.configure(socketio(socket));

// Receive real-time events through Socket.io
app.service('messages')
  .on('created', message => console.log('New message created', message));

// Call the `messages` service
app.service('messages').create({
  text: 'A message from a REST client'
});
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/core-js/2.1.4/core.min.js"></script>
<script src="//unpkg.com/@feathersjs/client@^3.0.0/dist/feathers.js"></script>
<script src="//unpkg.com/socket.io-client@1.7.3/dist/socket.io.js"></script>
<script>
  // Socket.io is exposed as the `io` global.
  var socket = io('http://api.feathersjs.com');
  // @feathersjs/client is exposed as the `feathers` global.
  var app = feathers();

  // Set up Socket.io client with the socket
  app.configure(feathers.socketio(socket));

  // Receive real-time events through Socket.io
  app.service('messages')
    .on('created', message => console.log('New message created', message));

  // Call the `messages` service
  app.service('messages').create({
    text: 'A message from a REST client'
  });

  // feathers.errors is an object with all of the custom error types.
</script>


socketio(socket, options)

使用指定socket和选项初始化Socket.io客户端。其中,选项可以是:

  • timeout(默认5000ms) - 方法调用失败并超时的时间。通常会在调用不存在的服务或服务方法时发生。
const feathers = require('@feathersjs/feathers');
const socketio = require('@feathersjs/socketio-client');
const io = require('socket.io-client');

const socket = io('http://api.feathersjs.com');
const app = feathers();

// Set up Socket.io client with the socket
// And a timeout of 2 seconds
app.configure(socketio(socket, {
  timeout: 2000
}));

要设置指定服务的超时,可以使用:

app.service('messages').timeout = 3000;


直接连接

Feathers设置一个普通的Socket.io服务器,可以通过加载socket.io-client模块或加载服务端的/socket.io/socket.io.js来连接任何与Socket.io兼容的客户端,通常是Socket.io客户端。与HTTP调用不同,websockets在浏览器中没有固有的跨源限制,因此可以连接到任何Feathers服务器。

套接字连接URL必须指向服务器根,这是Feathers将设置Socket.io的地方。

<!-- Connecting to the same URL -->
<script src="/socket.io/socket.io.js">
<script>
  var socket = io();
</script>

<!-- Connecting to a different server -->
<script src="http://localhost:3030/socket.io/socket.io.js">
<script>
  var socket = io('http://localhost:3030/');
</script>

可以通过发出<methodname>事件,然后发出服务路径和方法参数来调用服务方法。服务路径是服务已注册的名称(在app.use中),没有前导或尾部斜杠。将使用方法调用的结果或可能发生的任何错误调用function(error, data)节点约定之后的可选回调。

params将在服务方法调用中设置为params.query。其他服务参数可以通过Socket.io中间件设置。

如果服务路径或方法不存在,将返回相应的Feathers错误。


认证

可以通过使用strategy和有效负载发送authenticate事件来验证套接字。相关示例,请参阅localjwt身份验证章节中的“直接连接”部分。

const io = require('socket.io-client');
const socket = io('http://localhost:3030');

socket.emit('authenticate', {
  strategy: 'strategyname',
  ... otherData
}, function(message, data) {
  console.log(message); // message will be null
  console.log(data); // data will be {"accessToken": "your token"}
  // You can now send authenticated messages to the server
});


find

从服务中检索所有匹配资源的列表

socket.emit('find', 'messages', { status: 'read', user: 10 }, (error, data) => {
  console.log('Found all messages', data);
});

在服务端将调用app.service('messages').find({ query: { status: 'read', user: 10 } })


get

从服务中检索单个资源。

socket.emit('get', 'messages', 1, (error, message) => {
  console.log('Found message', message);
});

在服务端将调用app.service('messages').get(1, {})

socket.emit('get', 'messages', 1, { fetch: 'all' }, (error, message) => {
  console.log('Found message', message);
});

在服务端将调用app.service('messages').get(1, { query: { fetch: 'all' } })


create

使用data创建新资源:

socket.emit('create', 'messages', {
  text: 'I really have to iron'
}, (error, message) => {
  console.log('Todo created', message);
});

在服务端将调用app.service('messages').create({ text: 'I really have to iron' }, {})

socket.emit('create', 'messages', [
  { text: 'I really have to iron' },
  { text: 'Do laundry' }
]);

将调用app.service('messages').create使用数组


update

完全替换单个或多个资源。

socket.emit('update', 'messages', 2, {
  text: 'I really have to do laundry'
}, (error, message) => {
  console.log('Todo updated', message);
});

在服务端将调用app.service('messages').update(2, { text: 'I really have to do laundry' }, {})id可以为null以更新多个资源。

socket.emit('update', 'messages', null, {
  complete: true
}, { complete: false });

在服务端将调用app.service('messages').update(null, { complete: true }, { query: { complete: 'false' } })


patch

使用data与一或多个资源合并:

socket.emit('patch', 'messages', 2, {
  read: true
}, (error, message) => {
  console.log('Patched message', message);
});

在服务端将调用app.service('messages').patch(2, { read: true }, {})id可以为null以更新多个资源。

socket.emit('patch', 'messages', null, {
  complete: true
}, {
  complete: false
}, (error, message) => {
  console.log('Patched message', message);
});

在服务端将调用app.service('messages').patch(null, { complete: true }, { query: { complete: false } }),以修改所有通过app.service('messages')读取到的消息状态。

remove

删除一或多个资源:

socket.emit('remove', 'messages', 2, { cascade: true }, (error, message) => {
  console.log('Removed a message', message);
});

在服务端将调用app.service('messages').remove(2, { query: { cascade: true } })id可以为null以删除多个资源。

socket.emit('remove', 'messages', null, { read: true });

在服务端将调用app.service('messages').remove(null, { query: { read: 'true' } })


事件监听

通过侦听服务事件,可以在应用中实现实时行为。服务事件servicepath eventname.的形式发送到套接字。

created

当服务create成功返回时,created事件将与回调数据一起发送。

var socket = io('http://localhost:3030/');

socket.on('messages created', function(message) {
  console.log('Got a new Todo!', message);
});


updated,patched

当服务updatepatch成功返回时,updatedpatched事件将与回调数据一起发送。

var socket = io('http://localhost:3030/');

socket.on('my/messages updated', function(message) {
  console.log('Got an updated Todo!', message);
});

socket.emit('update', 'my/messages', 1, {
  text: 'Updated text'
}, {}, function(error, callback) {
 // Do something here
});


removed

当服务remove成功返回时,removed事件将与回调数据一起发送。

var socket = io('http://localhost:3030/');

socket.on('messages removed', function(message) {
  // Remove element showing the Todo from the page
  $('#message-' + message.id).remove();
});


3.4 Primus

如果可能,建议在客户端上使用Feathers和@feathersjs/primus-client模块。要在客户端上使用直接Primus连接而不使用Feathers,请参阅直接连接部分。


加载 Primus 客户端库

在浏览器端,Primus库总是通过<script>标签加载:

<script type="text/javascript" src="primus/primus.js"></script>

这将使Primus对象全局可用。模块加载器选项目前不可用。


在NodeJS中使用客户端

在NodeJS中可以像下面这样初始化Primus:

const Primus = require('primus');
const Emitter = require('primus-emitter');
const Socket = Primus.createSocket({
  transformer: 'websockets',
  plugin: {
    'emitter': Emitter
  }
});
const socket = new Socket('http://api.feathersjs.com');


@feathersjs/primus-client

$ npm install @feathersjs/primus-client --save

@feathersjs/primus-client模块允许通过所配置的websocket库连接到通过Primus服务器公开的服务。

Primus套接字也用于调用服务方法。对于两者,调用方法和接收实时事件通常比使用REST更快,并且不需要同时在同一客户端应用程序中同时使用REST和websockets。


primus(socket)

通过指定的socket及默认选项初始化Primus:

const feathers = require('@feathersjs/feathers');
const primusClient = require('@feathersjs/primus-client');
const socket = new Primus('http://api.my-feathers-server.com');

const app = feathers();

app.configure(primusClient(socket));

// Receive real-time events through Primus
app.service('messages')
  .on('created', message => console.log('New message created', message));

// Call the `messages` service
app.service('messages').create({
  text: 'A message from a REST client'
});
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/core-js/2.1.4/core.min.js"></script>
<script src="//unpkg.com/@feathersjs/client@^3.0.0/dist/feathers.js"></script>
<script type="text/javascript" src="primus/primus.js"></script>
<script>
  // Socket.io is exposed as the `io` global.
  var socket = new Primus('http://api.my-feathers-server.com');
  // @feathersjs/client is exposed as the `feathers` global.
  var app = feathers();

  app.configure(feathers.primus(socket));

  // Receive real-time events through Primus
  app.service('messages')
    .on('created', message => console.log('New message created', message));

  // Call the `messages` service
  app.service('messages').create({
    text: 'A message from a REST client'
  });
</script>


primus(socket, options)

通过指定的socket及指定选项初始化Primus,选项可以是:

  • timeout(默认:5000ms)-方法调用失败并超时的时间。通常在调用不存在的服务或服务方法时发生。
const feathers = require('@feathersjs/feathers');
const Primus = require('@feathersjs/primus-client');
const socket = new Primus('http://api.my-feathers-server.com');

const app = feathers();

app.configure(primus(socket, { timeout: 2000 }));

可以像下面这样修改每个服务的超时时间:

app.service('messages').timeout = 3000;


直接连接

在浏览器中,可以通过从primus/primus.js加载客户端并实例化新的Primus实例来建立连接。与HTTP调用不同,websockets在浏览器中没有跨源限制,因此可以连接到任何Feathers服务器。

详见参阅Primus文档

套接字连接URL必须指向服务器根目录,这是Feathers设置Primus的位置。

<script src="primus/primus.js">
<script>
  var socket = new Primus('http://api.my-feathers-server.com');
</script>

通过使用方法参数发出<servicepath>::<methodname>事件来调用服务方法。servicepath是服务已注册的名称(在app.use中),没有前导或尾部斜杠。function(error, data)之后的可选回调将按Node约定格式调用结果或可能发生的任何错误。

params将在服务方法调用中设置为params.query。其他服务参数可以通过Primus中间件设置。


验证

可以通过使用strategy和有效负载发送authenticate事件来验证套接字。有关具体示例,请参阅localjwt身份验证章节中的“直接连接”部分。

socket.send('authenticate', {
  strategy: 'strategyname',
  ... otherData
}, function(message, data) {
  console.log(message); // message will be null
  console.log(data); // data will be {"accessToken": "your token"}
  // You can now send authenticated messages to the server
});


find

从服务中检索所有匹配资源的列表

socket.send('find', 'messages', { status: 'read', user: 10 }, (error, data) => {
  console.log('Found all messages', data);
});

在服务端将调用app.service('messages').find({ query: { status: 'read', user: 10 } })


get

从服务中检索单个资源

socket.send('get', 'messages', 1, (error, message) => {
  console.log('Found message', message);
});

在服务端将调用app.service('messages').get(1, {})

socket.send('get', 'messages', 1, { fetch: 'all' }, (error, message) => {
  console.log('Found message', message);
});

在服务端将调用app.service('messages').get(1, { query: { fetch: 'all' } })


create

使用data创建一个新资源,其还可以是一个数组。

socket.send('create', 'messages', {
  text: 'I really have to iron'
}, (error, message) => {
  console.log('Message created', message);
});

在服务端将调用app.service('messages').create({ "text": "I really have to iron" }, {})

socket.send('create', 'messages', [
  { text: 'I really have to iron' },
  { text: 'Do laundry' }
]);

在服务调将传入的使用数组调用app.service('messages').create


update

完全替换一或多个资源

socket.send('update', 'messages', 2, {
  text: 'I really have to do laundry'
}, (error, message) => {
  console.log('Message updated', message);
});

在服务端将调用app.service('messages').update(2, { "text": "I really have to do laundry" }, {})id可以是null以更新多个资源。

socket.send('update', 'messages', null, {
  complete: true
}, { complete: false });

在服务端将调用app.service('messages').update(null, { complete: true }, { query: { complete: false } })


patch

将指定的一或多条资源与data合并。

socket.send('patch', 'messages', 2, {
  read: true
}, (error, message) => {
  console.log('Patched message', message);
});

在服务端将调用app.service('messages').patch(2, { "read": true }, {})id可以是null以更新多个资源。

socket.send('patch', 'messages', null, {
  complete: true
}, {
  complete: false
}, (error, message) => {
  console.log('Patched message', message);
});

在服务端将调用app.service('messages').patch(null, { complete: true }, { query: { complete: false } })以更改app.service('messages')读取的所有消息的状态。

这还支持Feathers数据库适配器相关参数。


remove

删除一或多个资源

socket.send('remove', 'messages', 2, { cascade: true }, (error, message) => {
  console.log('Removed a message', message);
});

在服务端将调用app.service('messages').remove(2, { query: { cascade: true } })id可以是null以删除多个资源。

socket.send('remove', 'messages', null, { read: true });

在服务端将调用app.service('messages').remove(null, { query: { read: 'true' } })以删除app.service('messages')读取的所有消息。


事件监听

通过监听服务事件,可以在应用程序中实现实时行为。服务事件servicepath eventname的形式发送到套接字。

created

当服务create成功返回时,created事件将与回调数据一起发送。

socket.on('messages created', function(message) {
  console.log('Got a new Message!', message);
});


updated,patched

当服务updatepatch成功返回时,updatedpatched事件将与回调数据一起发送。

socket.on('my/messages updated', function(message) {
  console.log('Got an updated Message!', message);
});

socket.send('update', 'my/messages', 1, {
  text: 'Updated text'
}, {}, function(error, callback) {
 // Do something here
});


removed

当服务remove成功返回时,removed事件将与回调数据一起发送。

socket.on('messages removed', function(message) {
  // Remove element showing the Message from the page
  $('#message-' + message.id).remove();
});


4. Authentication-Feathers 认证机制

4.1 Server-服务器端

$ npm install @feathersjs/authentication --save

@feathersjs/authentication用于JWT身份验证,它主要有三个目的:

  1. 设置/authentication端点,以创建JSON Web Tokens (JWT)。JWT用作访问令牌。可以在jwt.io上了解更多信息。
  2. 为所有Feathers传输提供一致的身份验证API
  3. 为使用Passport策略保护端点的身份验证插件提供框架。


互补插件

以下插件是互补的,但完全是可选的:


app.configure(auth(options))

使用指定选项配置身份验证插件。对于未提供的选项,将使用默认选项。

const auth = require('@feathersjs/authentication');

// Available options are listed in the "Default Options" section
app.configure(auth(options))

验证插件必须在其他任何服务之前配置插件。


Options-选项

以下默认选项将与配置文件中的全局auth对象混合使用。它会将混合选项设置回应用程序,以便通过调用app.get('authentication')随时可用。它们都可以被覆盖,并且是某些身份验证插件所必需的。

{
 path: '/authentication', // 身份验证服务器路径
 header: 'Authorization', // 使用JWT身份验证时要使用的Header
 entity: 'user', // 将要添加到请求,套接字和context.params的实体。 (即req.user,socket.user,context.params.user)
 secret: 'supersecret', // 要么是HMAC算法的秘密,要么是用于RSA和ECDSA的PEM编码私钥。
 service: 'users', // 查找实体的服务
 passReqToCallback: true, // 其他请求对象应该传递给策略`verify`函数
 session: false, // 是否使用 session
 cookie: {
  enabled: false, // 是否启用了cookie创建
  name: 'feathers-jwt', // cookie 名
  httpOnly: false, // 启用时,阻止客户端读取cookie
  secure: true // cookie是否只能通过HTTPS获得
 },
 jwt: {
  header: { typ: 'access' }, // 默认情况下是访问令牌,但可以是任何类型。这不是拼写错误!
  audience: 'https://yourdomain.com', // 处理令牌的资源服务器
  subject: 'anonymous', // 通常是与JWT关联的实体ID
  issuer: 'feathers', // 发布服务器,应用程序或资源
  algorithm: 'HS256', // 要使用的算法
  expiresIn: '1d' // 访问令牌到期
 }
}

以上,JWT标题选项中的typ不是拼写错误。它是JWT规范中定义的typ参数


app.service('authentication')

这个插件的核心是一个创建JWT的服务。这是一个普通的Feathers服务,它只实现了createremove方法。/authentication服务提供/auth/local/auth/token端点所具有的所有功能。要选择策略,客户端必须在请求正文中传递strategy名称。根据使用的插件,这会有所不同。有关详细信息,请参阅前面列出的插件的文档。


service.create(data)

几乎每个Feathers应用程序都会使用create方法。它基于插件上配置的jwt选项创建JWT。此方法的API使用content对象。

如果手动生成JWT,例如,想要使用有效负载{userId: "abc123"}创建JWT:

const data = {payload: {userId: "abc123"}};
service.create(data);

data.payload对象中包含的任何内容都将位于JWT的有效负载中。如果在params中包含payload对象,则其属性将优先于data


service.remove(data)

remove方法使用频率较低。其的主要目的是为“logout”过程添加钩子。例如,在需要高安全控制的服务中,开发人员可以在执行令牌黑名单的remove方法上注册挂钩。


service.hooks({ before })

可以修改下面这些属性以更改/authentication服务的行为:

  • context.data.payload {Object} - 确定JWT的有效负载
  • context.params.payload {Object} - 同样确定了JWT的有效载负载。context.data.payload中的任何匹配属性都将被这些覆盖。在after中会保持。
  • context.params.authenticated {Boolean} - 验证成功后将设置为true,除非在before挂钓中设置为false。如果你在before挂钓中设置为false,会阻止websocket被标记为已通过身份验证。在after中会保持。


service.hooks({ after })
  • context.params[entity] {Object} - 身份验证成功后,将从此处填充从数据库中查找的entity。(默认选项是user。)


app.passport

app.passport.createJWT(payload, options)

app.passport.createJWT(payload, options) -> Promise身份验证服务使用它来生成JSON Web Token。

  • payload {Object} - 成为JWT有效负载。还将包含表示到期时间戳的exp属性。
  • options {Object} - 传给jsonwebtoken sign()的选项
    • secret {String | Buffer} - HMAC算法的密钥,或用于RSA和ECDSA的PEM编码私钥。
    • jwt - 其它可用选项,参阅jsonwebtoken。authenticate方法使用默认jwt选项。直接使用此包时,必须手动传递它们。

返回的promise将使用JWT解析或导致失败的错误。


app.passport.verifyJWT(token, options)

使用options验证传入的JWTtoken的签名和有效负载。

返回的promise将使用JWT解析或导致失败的错误。


auth.hooks.authenticate(strategies)

@feathersjs/authentication仅包含一个钩子。此绑定的authenticate挂钩用于在服务方法上注册一组身份验证策略。

注意:这通常应该用在/authentication服务上。没有它,可以点击authentication服务并生成JWTaccessToken而无需身份验证(即匿名身份验证)。

app.service('authentication').hooks({
 before: {
  create: [
   // You can chain multiple strategies
   auth.hooks.authenticate(['jwt', 'local']),
  ],
  remove: [
   auth.hooks.authenticate('jwt')
  ]
 }
});


事件验证

只要客户端成功进行身份验证或“注销”,就会在app对象上发出loginlogout事件。(使用JWT时,注销不会使JWT无效。(有关详细信息,请阅读JWT部分。)这些事件仅在服务器上发出。


app.on('login', callback))app.on('logout', callback))

这两个事件使用具有相同签名的callback函数。

  • result {Object} - 来自authentication服务的最终context.result。除非你在after钩子中自定义context.response,其将包含一个唯一表示JWT的属性accessToken
  • meta {Object} - 请求相关的信息。metatransport/provider而异,如下所示:
    • 使用 @feathersjs/express/rest
      • provider {String} - 为 "rest"
      • req {Object} - Express 的请求对象
      • res {Object} - Express 的响应对象
    • 使用 feathers-socketiofeathers-primus:
      • provider {String} - transport 名为: socketioprimus
      • connection {Object} - 与钓子上下文中的 params 相同
      • socket {SocketObject} - 当前用户的 WebSocket 对象。同样包含 feathers 属性,其与与钓子上下文中的 params 相同


Express 中间件

有一个authenticate的中间件。它的使用方法与普通的Passport express中间件完全相同:

const cookieParser = require('cookie-parser');

app.post('/protected-route', cookieParser(), auth.express.authenticate('jwt'));
app.post('/protected-route-that-redirects', cookieParser(), auth.express.authenticate('jwt', {
  failureRedirect: '/login'
}));

详细介绍请参阅Express middleware recipe

Express 中包含并公开了其他中间件,通常不需要特别处理:

  • emitEvents - 发送 loginlogout 事件
  • exposeCookies - 将Cookie暴露给Feathers,以便它们可用于钩子和服务。默认情况下不使用,因为它的使用会将API暴露给CSRF漏洞。只有在你确认自己需要时才使用。
  • exposeHeaders - 将头信息暴露给Feathers,以便它们可用于钩子和服务。 默认情况下不使用它,因为它的使用会将API暴露给CSRF漏洞。只有在你确认自己需要时才使用。
  • failureRedirect - 支持重定向auth失败。仅在设置了hook.redirect时触发。
  • successRedirect - 支持重定向auth成功。仅在设置了hook.redirect时触发。
  • setCookie - 支持在cookie中设置JWT访问令牌。仅在启用cookie时启用。注意:Feathers不会从cookie中读取访问令牌。这会将API暴露给CSRF攻击。setCookie功能主要用于帮助进行服务器端渲染。


完整示例

以下是使用@feathersjs/authentication进行本地身份验证的Feathers服务器的示例。

const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const socketio = require('@feathersjs/socketio');
const auth = require('@feathersjs/authentication');
const local = require('@feathersjs/authentication-local');
const jwt = require('@feathersjs/authentication-jwt');
const memory = require('feathers-memory');

const app = express(feathers());
app.configure(express.rest())
 .configure(socketio())
 .use(express.json())
 .use(express.urlencoded({ extended: true }))
 .configure(auth({ secret: 'supersecret' }))
 .configure(local())
 .configure(jwt())
 .use('/users', memory())
 .use('/', express.static(__dirname + '/public'))
 .use(express.errorHandler());

app.service('users').hooks({
  // Make sure `password` never gets sent to the client
  after: local.hooks.protect('password')
});

app.service('authentication').hooks({
 before: {
  create: [
   // You can chain multiple strategies
   auth.hooks.authenticate(['jwt', 'local'])
  ],
  remove: [
   auth.hooks.authenticate('jwt')
  ]
 }
});

// Add a hook to the user service that automatically replaces
// the password with a hash of the password, before saving it.
app.service('users').hooks({
 before: {
  find: [
   auth.hooks.authenticate('jwt')
  ],
  create: [
   local.hooks.hashPassword({ passwordField: 'password' })
  ]
 }
});

const port = 3030;
let server = app.listen(port);
server.on('listening', function() {
 console.log(`Feathers application started on localhost:${port}`);
});


4.2 Client


@feathersjs/authentication-client模块使你可以轻松地对Feathers服务器进行身份验证。它不是必需的,但通过自动存储和发送JWT访问令牌并在websocket断开连接时处理重新身份验证,可以更轻松地在客户端中实现身份验证。

这个模块包括:


app.configure(auth(options))

安装与所有Feathers插件完全相同,使用configure方法:

const feathers = require('@feathersjs/feathers');
const socketio = require('@feathersjs/socketio-client');
const io = require('socket.io-client');
const auth = require('@feathersjs/authentication-client');

const socket = io('http://api.feathersjs.com');
const app = feathers();

// Setup the transport (Rest, Socket, etc.) here
app.configure(socketio(socket));

// Available options are listed in the "Options" section
app.configure(auth(options))

传输插件(Rest,Socket,Primus ...)必须事先已经初始化为身份验证插件。


Options-选项

在配置身份验证时,以下默认选项将与你传入的设置混合在一起。并将混合选项设置回应用程序,以便app.get('auth')随时可以使用它们。以下设置都可以被覆盖:

{
  header: 'Authorization', // REST的默认授权标头
  prefix: '', // 设置是否为标头值添加前缀。例如,如果前缀是'JWT',那么标题将是'授权:JWT eyJ0eXAiOiJKV1QiLCJhbGciOi ......'
  path: '/authentication', // 服务器端认证服务路径
  jwtStrategy: 'jwt', // JWT身份验证策略的名称
  entity: 'user', // 要验证的实体(即:用户)
  service: 'users', // 查找实体的服务
  cookie: 'feathers-jwt', // 用于在服务器端启用cookie时解析JWT的cookie的名称
  storageKey: 'feathers-jwt', // 在localstorage中存储accessToken或在React Native上存储AsyncStorage的键
  storage: undefined // 传入与WebStorage兼容的对象以在客户端上启用自动存储。
}

要启用JWT,应确保在配置插件时提供storage。可以使用以下存储选项:

  • window.localStorage 在浏览器中使用浏览器的localStorage
  • AsyncStorage 用于React Native
  • localForage 可以帮助你处理旧版浏览器兼容及浏览器的隐身/私密浏览模式。
  • cookie-storage 使用Cookies。这对于不支持localStorage的设备非常有用。


app.authenticate()

app.authenticate() -> Promise没有参数时,将尝试使用存储中的JWT进行身份验证。通常调用此方法来显示你的应用页面(成功时)或显示登录页面或重定向到相应的oAuth链接。

app.authenticate().then(() => {
  // show application page
}).catch(() => {
  // show login page
})

注意:当您想要从存储中使用Token时,必须调用app.authenticate(),并且在应用程序初始化时只需调用一次。一旦成功,所有后续请求将自动发送其身份验证信息。


app.authenticate(options)

app.authenticate(options) -> Promise将通过传递strategy和其他属性作为凭证来尝试使用Feathers服务器进行身份验证。它将使用客户端上设置的任何传输(@feathersjs/rest-client@feathersjs/socketio-client@feathersjs/primus-client)。

// 使用本地 email/password 进行验证 
app.authenticate({
  strategy: 'local',
  email: 'my@email.com',
  password: 'my-password'
}).then(() => {
  // Logged in
}).catch(e => {
  // Show login page (potentially with `e.message`)
  console.error('Authentication error', e);
});

app.authenticate({
  strategy: 'jwt', 
  accessToken: '<the.jwt.token.string>'
}).then(() => {
  // JWT authentication successful
}).catch(e => {
  console.error('Authentication error', e);
  // Show login page
});
  • data {Object} - 格式 {strategy [, ...otherProps]}
    • strategy {String} - 用于进行身份验证的策略的名称。必须。
    • ...otherProps {Properties} 取决于所选择的策略。以上是使用 jwt策略的示例。下面会有一个使用local策略的示例。


app.logout()

从客户端的存储中删除JWT accessToken。它还在在Feathers服务器上调用/authentication服务remove方法。


app.passport

app.passport包含帮助函数以使用JWT。


app.passport.getJWT()

storage或cookie中拉出JWT。返回一个Promise。


app.passport.verifyJWT(token)

验证JWT是否未过期并对其进行解码以获取有效负载。返回一个Promise。


app.passport.payloadIsValid(token)

同步验证令牌是否已过期。返回一个布尔值。


认证事件

在客户端上,只要客户端成功进行身份验证或“注销”,就会在应用程序对象上发出身份验证事件。 这些事件在客户端上发出。


app.on('authenticated', callback)

app.on('logout', callback)

app.on('reauthentication-error', errorHandler)

如果你的服务器出现故障或客户端失去连接,当客户端重新获得与服务器的连接时,它将自动处理尝试重新验证套接字的问题。为了在自动重新身份验证期间处理身份验证失败,需要实现以下事件侦听器:

const errorHandler = error => {
  app.authenticate({
    strategy: 'local',
    email: 'admin@feathersjs.com',
    password: 'admin'
  }).then(response => {
    // You are now authenticated again
  });
};

// Handle when auth fails during a reconnect or a transport upgrade
app.on('reauthentication-error', errorHandler)


钩子

有3个钩子非常适合内部使用,但你不必经常关注它们。

  • populateAccessToken - 获取令牌并输入 hooks.params.accessToken 以使你可以在其中一个客户端服务或挂钩中使用它
  • populateHeader - 将accessToken添加到授权标头
  • populateEntity -实验。根据JWT有效负载填充实体


完整示例

以下是一个使用@feathersjs/authentication-client的Feathers客户端示例:

const feathers = require('@feathersjs/feathers');
const rest = require('@feathersjs/rest-client');
const auth = require('@feathersjs/authentication-client');

const superagent = require('superagent');
const localStorage = require('localstorage-memory');

const feathersClient = feathers();

feathersClient.configure(rest('http://localhost:3030').superagent(superagent))
  .configure(auth({ storage: localStorage }));

feathersClient.authenticate({
  strategy: 'local',
  email: 'admin@feathersjs.com',
  password: 'admin'
})
.then(response => {
  console.log('Authenticated!', response);
  return feathersClient.passport.verifyJWT(response.accessToken);
})
.then(payload => {
  console.log('JWT Payload', payload);
  return feathersClient.service('users').get(payload.userId);
})
.then(user => {
  feathersClient.set('user', user);
  console.log('User', feathersClient.get('user'));
})
.catch(function(error){
  console.error('Error authenticating!', error);
});


4.3 Local

$ npm install @feathersjs/authentication-local --save

@feathersjs/authentication-local是一个服务端模块,它是对passport-local验证策略的封装,它使你可以实现基于用户名、密码对Feathers应用验证。

该模块包括3个核心部分:

  1. 主要初始化功能
  2. hashPassword钩子
  3. Verifier


配置

大多数情况下,只需要像下面这样简单初始化即可:

const feathers = require('@feathersjs/feathers');
const authentication = require('@feathersjs/authentication');
const local = require('@feathersjs/authentication-local');
const app = feathers();

// Setup authentication
app.configure(authentication(settings));
app.configure(local());

// Setup a hook to only allow valid JWTs or successful 
// local auth to authenticate and get new JWT access tokens
app.service('authentication').hooks({
  before: {
    create: [
      authentication.hooks.authenticate(['local', 'jwt'])
    ]
  }
});

这将从配置文件中的全局身份验证对象中提取。还将混合以下默认值,并可以自定义。

Options-选项

{
    name: 'local', // 调用身份验证策略时使用的名称
    entity: 'user', // 正在比较用户名/密码的实体
    service: 'users', // 查找实体的服务
    usernameField: 'email', // 用户名字段的键名
    passwordField: 'password', // 密码字段的键名
    entityUsernameField: 'email', // 实体上用户名字段的键名(默认为`usernameField`)
    entityPasswordField: 'password', // 实体上密码字段的键名(默认为`passwordField`)
    passReqToCallback: true, // 是否应将请求对象传递给 `verify`
    session: false, // 是否使用session
    Verifier: Verifier, // Verifier 类。默认为内置的,但可以是自定义的。请参考下文介绍
}

注意:在配置中将usernameField设置为username时,必须将值转义为\\username,否则它将使用Windows系统上的username环境变量的值。


hooks

hashPassword

该钩子用于在将纯文本密码保存到数据库之前对其进行哈希处理。它默认使用bcrypt算法,但可以通过传递自己的options.hash函数来自定义。

注意:@feathersjs/authentication-local不允许存储明文密码。这意味着在使用标准验证程序时必须使用hashPassword钩子。

有效选项:

  • passwordField (默认: 'password') - 要在context.data上查找的密码字段的键名称
  • hash (默认: bcrypt哈希函数) - 接受一个密码,并返回一个哈希
const local = require('@feathersjs/authentication-local');

app.service('users').hooks({
  before: {
    create: [
      local.hooks.hashPassword()
    ]
  }
});


protect

protect钩子用于确保受保护的字段不会发送给客户端。

const local = require('@feathersjs/authentication-local');

app.service('users').hooks({
  after: local.hooks.protect('password')
});


Verifier

这是一个验证类,它通过usernameField在给定服务上查找实体(通常是user)并使用bcrypt比较散列密码来执行实际的用户名和密码验证。它具有以下可以覆盖的方法。除验证之外,所有方法都返回一个promise,它与passport-local具有完全相同的签名。

{
    constructor(app, options) // 类构造函数
    _comparePassword(entity, password) // 使用 bcrypt 来比较密码
    _normalizeResult(result) // 规范化服务的结果以考虑分页
    verify(req, username, password, done) // 查询服务并调用其他内部函数
}

可以扩展Verifier类,以便自定义其行为,而无需重写和测试完全自定义的本地Passport实现。即使你不想使用这个插件,但这始终是一个选项。

自定义Verifier示例:

const { Verifier } = require('@feathersjs/authentication-local');

class CustomVerifier extends Verifier {
  // The verify function has the exact same inputs and 
  // return values as a vanilla passport strategy
  verify(req, username, password, done) {
    // do your custom stuff. You can call internal Verifier methods
    // and reference this.app and this.options. This method must be implemented.

    // the 'user' variable can be any truthy value
    // the 'payload' is the payload for the JWT access token that is generated after successful authentication
    done(null, user, payload);
  }
}

app.configure(local({ Verifier: CustomVerifier }));


客户端使用

authentication-client

当该模块在服务端注册时,使用默认配置值就可以使用@feathersjs/authentication-client进行身份验证:

app.authenticate({
  strategy: 'local',
  email: 'your email',
  password: 'your password'
}).then(response => {
  // You are now authenticated
});


HTTP Request

如果不使用@feathersjs/authentication-client模块,并已在服务端注册此模块,那么应POST如下负载到/authentication

// POST /authentication the Content-Type header set to application/json
{
  "strategy": "local",
  "email": "your email",
  "password": "your password"
}

使用curl示例类似如下:

curl -H "Content-Type: application/json" -X POST -d '{"strategy":"local","email":"your email","password":"your password"}' http://localhost:3030/authentication


Sockets

通过套接字使用本地策略进行身份验证是通过发出以下消息来完成的:

const io = require('socket.io-client');
const socket = io('http://localhost:3030');

socket.emit('authenticate', {
  strategy: 'local',
  email: 'your email',
  password: 'your password'
}, function(message, data) {
  console.log(message); // message will be null
  console.log(data); // data will be {"accessToken": "your token"}
  // You can now send authenticated messages to the server
});


4.4 JWT

$ npm install @feathersjs/authentication-jwt --save

@feathersjs/authentication-jwt是一个服务端模块,它是对passport-jwtl验证策略的封装,它使你可以实现基于JSON Web Token对Feathers应用验证。

该模块包括3个核心部分:

  1. 主要初始化功能
  2. Verifier
  3. passport-jwt中的ExtractJwt对象


配置

大多数情况下,只需要像下面这样简单初始化即可:

const feathers = require('@feathersjs/feathers');
const authentication = require('@feathersjs/authentication');
const jwt = require('@feathersjs/authentication-jwt');
const app = feathers();

// Setup authentication
app.configure(authentication(settings));
app.configure(jwt());

// Setup a hook to only allow valid JWTs to authenticate
// and get new JWT access tokens
app.service('authentication').hooks({
  before: {
    create: [
      authentication.hooks.authenticate(['jwt'])
    ]
  }
});

这将从配置文件中的全局身份验证对象中提取。还将混合以下默认值,并可以自定义。

Options-选项
{
    name: 'jwt', // 调用身份验证策略时使用的名称
    entity: 'user', // 如果有效负载中存在'id',则从中提取的实体
    service: 'users', // 查找实体的服务
    passReqToCallback: true, // 是否应将请求对象传递给 `verify`
    jwtFromRequest: [ // 一个passport-jwt选项,用于确定解析JWT的位置
      ExtractJwt.fromHeader, // "Authorization" 标头
      ExtractJwt.fromAuthHeaderWithScheme('Bearer'), // 允许 "Bearer" 前缀
      ExtractJwt.fromBodyField('body') // 请求正文
    ],
    secretOrKey: auth.secret, // 向passport-jwt提供的主要秘密(字符串或缓冲区)
    secretOrKeyProvider: auth.secret, // 提供给passport-jwt的主要秘密提供者(功能)
    session: false, // 是否使用 session
    Verifier: Verifier // Verifier 类。默认为内置的,但可以是自定义的。请参考下文介绍
}

可以提供给passport-jwt的额外选项。


Verifier

接收JWT中有效负载的验证类(如果验证成功)并且返回有效负载,如果有效负载中存在id,则填充实体(通常是user)并返回实体和有效负载。它有以下可以覆盖的方法,其中verity函数与passport-jwt有完全相同的签名。

{
    constructor(app, options) // the class constructor
    verify(req, payload, done) // queries the configured service
}
自定义 Verifier

可以扩展Verifier类,以便自定义其的行为而无需重写和测试完全自定义的本地Passport实现。你可不使用该插件,但这始终是一个可选项。

自定义 Verifier的示例:

const { Verifier } = require('@feathersjs/authentication-jwt');

class CustomVerifier extends Verifier {
  // The verify function has the exact same inputs and 
  // return values as a vanilla passport strategy
  verify(req, payload, done) {
    // do your custom stuff. You can call internal Verifier methods
    // and reference this.app and this.options. This method must be implemented.

    // the 'user' variable can be any truthy value
    // the 'payload' is the payload for the JWT access token that is generated after successful authentication
    done(null, user, payload);
  }
}

app.configure(jwt({ Verifier: CustomVerifier }));


客户端使用

authentication-client

当该模块在服务端注册时,使用默认配置值就可以使用@feathersjs/authentication-client进行身份验证:

app.authenticate({
  strategy: 'jwt',
  accessToken: 'your access token'
}).then(response => {
  // You are now authenticated
});


HTTP

如果不使用@feathersjs/authentication-client模块进行客户端验证,并已在服务端注册本模块,那么可以将访问Token放在Authorization头中。

使用curl类似如下:

curl -H "Content-Type: application/json" -H "Authorization: <your access token>" -X POST http://localhost:3030/authentication


Sockets

使用Socket验证访问Token,可以通过发下以下消息来完成:

const io = require('socket.io-client');
const socket = io('http://localhost:3030');

socket.emit('authenticate', {
  strategy: 'jwt',
  accessToken: 'your token'
}, function(message, data) {
  console.log(message); // message will be null
  console.log(data); // data will be {"accessToken": "your token"}
  // You can now send authenticated messages to the server
});


4.5 OAuth1

$ npm install @feathersjs/authentication-oauth1 --save

feathersjs / authentication-oauth1是一个服务器端模块,允许您在Feathers应用程序中使用任何Passport OAuth1身份验证策略,最明显的是Twitter。


该模块包括2个核心部分:

  1. 主要初始化功能
  2. Verifier

配置

大多数情况下,只需要像下面这样简单初始化该模块即可:

const feathers = require('@feathersjs/feathers');
const authentication = require('@feathersjs/authentication');
const jwt = require('@feathersjs/authentication-jwt');
const oauth1 = require('@feathersjs/authentication-oauth1');

const session = require('express-session');
const TwitterStrategy = require('passport-twitter').Strategy;
const app = feathers();

// Setup in memory session
app.use(session({
  secret: 'super secret',
  resave: true,
  saveUninitialized: true
}));

// Setup authentication
app.configure(authentication(settings));
app.configure(jwt());
app.configure(oauth1({
  name: 'twitter',
  Strategy: TwitterStrategy,
  consumerKey: '<your consumer key>',
  consumerSecret: '<your consumer secret>'
}));

// Setup a hook to only allow valid JWTs to authenticate
// and get new JWT access tokens
app.service('authentication').hooks({
  before: {
    create: [
      authentication.hooks.authenticate(['jwt'])
    ]
  }
});

这将从配置文件中的全局身份验证对象中提取。还将混合以下默认值,并可以自定义。

注册OAuth1插件后,会自动设置路由以处理OAuth重定向和授权。


配置选项

{
    idField: '<provider>Id', // 使用提供程序登录时查找实体的字段。默认为'<provider> Id'(如'twitterId')
    path: '/auth/<provider>', // 注册中间件的路由
    callbackURL: 'http(s)://hostame[:port]/auth/&;t;provider>/callback', // 回调URL,将自动根据的主机、端口以及应用是否使用生成环境来构建网址 (如,在开发中:http://localhost:3030/auth/twitter/callback)
    entity: 'user', // 正在查找的实体
    service: 'users', // 用于查找实体的服务
    passReqToCallback: true, // 是否应将请求对象传递给`verify`
    session: true, // 是否使用Session
    handler: function, // 用于处理oauth回调的Express中间件。默认为内置中间件
    formatter: function, // 响应格式化程序。默认为内置的feather-rest,它会返回JSON
    Verifier: Verifier, // Verifier 类。默认为内置的,但可以自定义。请参阅下文了解详情
    makeQuery: function // 查询查找现有用户。默认为 (profile, options) => ({ [options.idField]: profile.id })
}

可以根据正在配置的OAuth1策略,提供其他Passport策略选项。


Verifier

通过在指定服务上查找实体(通常是user)来处理OAuth1验证的验证类,以创建或更新实体并返回它们。它具有以下可以覆盖的方法。除了verify之外,所有方法都返回一个promise,它与passport-oauth1具有完全相同的签名。

{
    constructor(app, options) // 类构造函数
    _updateEntity(entity) // 更新已存在的实体
    _createEntity(entity) // 如果不存在,则创建一个实体
    _normalizeResult(result) // 规范化服务的结果,以处理分页
    verify(req, accessToken, refreshToken, profile, done) // 查询服务并调用其他内部函数
}

可以扩展Verifier类,以使你可以自定义其行为,而无需重写和测试完全自定义的本地Passport实现。你可不使用该插件,但这始终是一个可选项。

自定义 Verifier的示例:

import oauth1, { Verifier } from '@feathersjs/authentication-oauth1';

class CustomVerifier extends Verifier {
  // The verify function has the exact same inputs and 
  // return values as a vanilla passport strategy
  verify(req, accessToken, refreshToken, profile, done) {
    // do your custom stuff. You can call internal Verifier methods
    // and reference this.app and this.options. This method must be implemented.

    // the 'user' variable can be any truthy value
    // the 'payload' is the payload for the JWT access token that is generated after successful authentication
    done(null, user, payload);
  }
}

app.configure(oauth1({
  name: 'twitter'
  Strategy: TwitterStrategy,
  consumerKey: '<your consumer key>',
  consumerSecret: '<your consumer secret>',
  Verifier: CustomVerifier
}));


自定义 OAuth 响应

当你使用Twitter等OAuth1提供程序进行身份验证时,提供程序会根据你的请求授权OAuth1scope,并返回一个accessTokenrefreshToken和一个profile,其中包含经过身份验证的实体的信息。

默认情况下,Verifier接受提供程序返回的所有内容,并将其附加到提供程序名称下的entity(即用户对象)。如果希望自定义返回的数据,则可以通过在entity服务上的updatecreate服务方法中添加before钩子来完成。

app.configure(oauth1({
  name: 'twitter',
  entity: 'user',
  service: 'users',
  Strategy,
  consumerKey: '<your consumer key>',
  consumerSecret: '<your consumer secret>',
}));

function customizeTwitterProfile() {
  return function(context) {
    console.log('Customizing Twitter Profile');
    // If there is a twitter field they signed up or
    // signed in with twitter so let's pull the email. If
    if (context.data.twitter) {
      context.data.email = context.data.twitter.email; 
    }

    // If you want to do something whenever any OAuth
    // provider authentication occurs you can do this.
    if (context.params.oauth) {
      // do something for all OAuth providers
    }

    if (context.params.oauth.provider === 'twitter') {
      // do something specific to the twitter provider
    }

    return Promise.resolve(context);
  };
}


app.service('users').hooks({
  before: {
    create: [customizeTwitterProfile()],
    update: [customizeTwitterProfile()]
  }
});


客户端使用

当模块在服务端注册时,无论你是否使用feathers-authentication-client,用户都必须定向到身份验证策略URL。这可以通过设置window.location或通过应用程序中的链接来实现。

例如,你可能会使用Twitter的登录按钮:

<a href="/auth/twitter" class="button">Login With Twitter</a>


4.6 OAuth2

$ npm install @feathersjs/authentication-oauth2 --save

@feathersjs/authentication-oauth2是一个服务端模块,它让你可以在Feathers应用中使用任何Passport OAuth2验证策略。很多网站都使用该验证策略,如:

该模块包括2个核心部分:

  1. 主要初始化功能
  2. Verifier


配置

大多数情况下,只需要像下面这样简单初始化该模块即可:

const feathers = require('@feathersjs/feathers');
const authentication = require('@feathersjs/authentication');
const jwt = require('@feathersjs/authentication-jwt');
const oauth2 = require('@feathersjs/authentication-oauth2');
const FacebookStrategy = require('passport-facebook').Strategy;
const app = feathers();

// Setup authentication
app.configure(authentication({ secret: 'super secret' }));
app.configure(jwt());
app.configure(oauth2({
  name: 'facebook',
  Strategy: FacebookStrategy,
  clientID: '<your client id>',
  clientSecret: '<your client secret>',
  scope: ['public_profile', 'email']
}));

// Setup a hook to only allow valid JWTs to authenticate
// and get new JWT access tokens
app.service('authentication').hooks({
  before: {
    create: [
      authentication.hooks.authenticate(['jwt'])
    ]
  }
});

这将从配置文件中的全局身份验证对象中提取。还将混合以下默认值,并可以自定义。

类似OAuth1,注册OAuth2插件也会自动设置路由以处理OAuth重定向和授权。


配置选项

{
    idField: '<provider>Id', // 使用提供程序登录时查找实体的字段。默认为:'<provider>Id' (如,'facebookId').
    path: '/auth/<provider>', // 注册中间件的路由
    callbackURL: 'http(s)://hostname[:port]/auth/<provider>/callback', // 回调Url,将自动根据的主机、端口以及应用是否使用生成环境来构建网址。(如,在开发环境中 http://localhost:3030/auth/facebook/callback)
    successRedirect: undefined,
    failureRedirect: undefined,
    entity: 'user', // 正在查找的实体
    service: 'users', // 用于查找实体的服务
    passReqToCallback: true, // 是否应将请求对象传递给 `verify`
    session: false, // 是否使用Session
    handler: middleware, // 用于处理oauth回调的Express中间件。默认为内置中间件
    errorHandler: middleware, // 用于处理错误的Express中间件。默认为内置中间件
    formatter: middleware, // 响应格式化程序中间件。默认为内置的feather-rest,仅处理JSON
    Verifier: Verifier, // Verifier 类。默认为内置的,但可以是自定义的。请参考下文介绍
    makeQuery: function // 查询查找现有用户。默认为 (profile, options) => ({ [options.idField]: profile.id })
}

可以根据你正在配置的OAuth2策略提供其他安全策略选项。


Verifier

通过在指定服务上查找实体(通常是user)来处理OAuth2验证的验证类,以创建或更新实体并返回它们。它具有以下可以覆盖的方法。除了verify之外,所有方法都返回一个promise,它与passport-oauth2具有完全相同的签名。

{
    constructor(app, options) // the class constructor
    _updateEntity(entity) // updates an existing entity
    _createEntity(entity) // creates an entity if they didn't exist already
    _normalizeResult(result) // normalizes result from service to account for pagination
    verify(req, accessToken, refreshToken, profile, done) // queries the service and calls the other internal functions.
}

可以对Verifier类进行扩展,以使你可以自定义其行为,而无需重写和测试完全自定义的本地Passport实现。你可以不使用此插件,但这始终是一个选项。

自定义Verifier示例:

import oauth2, { Verifier } from '@feathersjs/authentication-oauth2';

class CustomVerifier extends Verifier {
  // The verify function has the exact same inputs and 
  // return values as a vanilla passport strategy
  verify(req, accessToken, refreshToken, profile, done) {
    // do your custom stuff. You can call internal Verifier methods
    // and reference this.app and this.options. This method must be implemented.

    // the 'user' variable can be any truthy value
    // the 'payload' is the payload for the JWT access token that is generated after successful authentication
    done(null, user, payload);
  }
}

app.configure(oauth2({
  name: 'facebook',
  Strategy: FacebookStrategy,
  clientID: '<your client id>',
  clientSecret: '<your client secret>',
  scope: ['public_profile', 'email'],
  Verifier: CustomVerifier
}));


自定义 OAuth 响应

每当你使用OAuth2提供程序(如,Facebook)进行身份验证时,提供程序都会发返回一个accessTokenrefreshToken和一个profile文件,其包含了基于你请求并已授予的OAuth2范围的经过身份验证的实体的信息。

默认情况下,Verifier接收提供程序返回的所有内容,并将其附加到提供程序名称下的实体(即用户对象)。如果希望自定义返回的数据,则可以通过在entity服务上的updatecreate服务方法中添加before钩子来完成。

app.configure(oauth2({
  name: 'github',
  entity: 'user',
  service: 'users',
  Strategy,
  clientID: 'your client id',
  clientSecret: 'your client secret'
}));

function customizeGithubProfile() {
  return function(context) {
    console.log('Customizing Github Profile');
    // If there is a github field they signed up or
    // signed in with github so let's pull the primary account email.
    if (context.data.github) {
      context.data.email = context.data.github.profile.emails.find(email => email.primary).value;
    }

    // If you want to do something whenever any OAuth
    // provider authentication occurs you can do this.
    if (context.params.oauth) {
      // do something for all OAuth providers
    }

    if (context.params.oauth.provider === 'github') {
      // do something specific to the github provider
    }

    return Promise.resolve(context);
  };
}


app.service('users').hooks({
  before: {
    create: [customizeGithubProfile()],
    update: [customizeGithubProfile()]
  }
});


客户端使用

当此模块在服务器端注册时,无论你是否使用feathers-authentication-client,用户都必须导航到身份验证策略URL。这可以通过设置window.location或通过应用程序中的链接来实现。

例如,你可能会有Facebook的登录按钮:

<a href="/auth/facebook" class="button">Login With Facebook</a>


5. Database-Feathers公用数据库适配器API及查询机制

5.1 Adapters

Feathers的数据库适配器是一些模块,它们使用通用API进行初始化和设置并提供通用查询语法,从而为特定数据库提供实现标准CRUD功能的服务

Service可以实现对任何数据库的访问,此处列出的数据库适配器只是具有通用API的便捷包装器。你仍可以为此处未列出的数据库获取Feathers功能。另请参阅社区数据库适配器列表

Feathers支付以下数据库:

Database Adapter
In memory-内存 feathers-memory, feathers-nedb
Localstorage, AsyncStorage- feathers-localstorage
Filesystem-文件系统 feathers-nedb
MongoDB feathers-mongodb, feathers-mongoose
MySQL, PostgreSQL, MariaDB, SQLite, MSSQL feathers-knex, feathers-sequelize
Elasticsearch feathers-elasticsearch
RethinkDB feathers-rethinkdb


Memory/Filesystem - 内存/文件系统


SQL


NoSQL


5.2 Common API

所有数据库适配器都实现了用于初始化、分页、扩展和查询的通用接口。本节介绍常见的适配器初始化和选项,如何启用和使用分页,有关特定服务方法的行为以及如何使用自定义功能扩展适配器的详细信息。

每个数据库适配器都是Feathers服务接口的实现。在使用数据库适配器之前应该熟悉服务、服务事件和钩子。


service([options])

返回通过指定选项初始化的新服务实例。

const service = require('feathers-');

app.use('/messages', service());
app.use('/messages', service({ id, events, paginate }));

Options

  • id (可选) - id 字段属性的名称(通常默认设置为id_id
  • events (可选) - 此服务发送的自定义服务事件列表
  • paginate (可选) - 分页对象,包含defaultmax页大小
  • whitelist (可选) - 允许的其他非标准查询参数列表 (如 [ '$regex', '$populate' ])
  • multi (可选, 默认: false) - 允许create使用数组,updateremove使用null对于所有方法或一系列允许的方法都可以是true(如 [ 'remove', 'create' ])


Pagination-分页

初始化适配器时,可以在paginate对象中设置以下分页选项:

  • default - 设置未设置$limit时的默认项目数
  • max - 设置每页允许的最大项目数(即使$limit查询参数设置得更高)

设置paginate.default后,find会返回一个page对象(以替代普通的数组)

{
  "total": "<total number of records>",
  "limit": "<max number of items per page>",
  "skip": "<number of skipped items (offset)>",
  "data": [/* data */]
}

分页选项可以像下面这样设置:

const service = require('feathers-<db-name>');

// Set the `paginate` option during initialization
app.use('/todos', service({
  paginate: {
    default: 5,
    max: 25
  }
}));

// override pagination in `params.paginate` for this call
app.service('todos').find({
  paginate: {
    default: 100,
    max: 200
  }
});

// disable pagination for this call
app.service('todos').find({
  paginate: false
});

客户端中没有提供禁用或更改默认分页的功能。只有通过params.query传递给服务器(另请参考此处的解决方法

要将可用记录的数量$limit设置为0,这样只会对数据库运行(快速)计数查询。


服务方法

本节介绍有关如何为所有适配器实现服务方法的详细信息。


adapter.Model

如果ORM或数据库支持模型,则可以在adapter.Model中找到属于此适配器的集合的模型实例或引用。这样就可以使用该模型轻松地进行自定义查询,例如, 在钩子里:

// Make a MongoDB aggregation (`messages` is using `feathers-mongodb`)
app.service('messages').hooks({
  before: {
    async find(context) {
      const results = await service.Model.aggregate([
        { $match: {item_id: id} }, {
          $group: {_id: null, total_quantity: {$sum: '$quantity'} }
        }
      ]).toArray();

      // Do something with results

      return context;
    }
  }
});


adapter.find(params)

adapter.find(params) -> Promise使用公共查询机制返回params.query中与查询匹配的所有记录的列表。如果启用了分页,将返回包含结果的数组或页面对象。

注意,通过REST URL使用时,所有查询值都是字符串。 根据数据库的不同,params.query中的值需要在before钩子中转换为正确的类型。

// Find all messages for user with id 1
app.service('messages').find({
  query: {
    userId: 1
  }
}).then(messages => console.log(messages));

// Find all messages belonging to room 1 or 3
app.service('messages').find({
  query: {
    roomId: {
      $in: [ 1, 3 ]
    }
  }
}).then(messages => console.log(messages));

查询用户id为1的所有消息:

GET /messages?userId=1

查找属于房间1或3的所有消息:

GET /messages?roomId[$in]=1&roomId[$in]=3


adapter.get(id, params)

adapter.get(id, params) -> Promise通过其唯一标识符(初始化期间在id选项中设置的字段)检索单条记录。

app.service('messages').get(1)
  .then(message => console.log(message));
GET /messages/1


adapter.create(data, params)

adapter.create(data, params) -> Promise使用data创建一条新记录。data还可以是数组,以创建多条记录。

app.service('messages').create({
    text: 'A test message'
  })
  .then(message => console.log(message));

app.service('messages').create([{
    text: 'Hi'
  }, {
    text: 'How are you'
  }])
  .then(messages => console.log(messages));
POST /messages
{
  "text": "A test message"
}


adapter.update(id, data, params)

adapter.update(id, data, params) -> Promise使用data完全替换id标识的记录。不允许替换多个记录(id不能为null)。id无法更改。

app.service('messages').update(1, {
    text: 'Updates message'
  })
  .then(message => console.log(message));
PUT /messages/1
{ "text": "Updated message" }


adapter.patch(id, data, params)

adapter.patch(id, data, params) -> Promisedataid标识的记录合并。id可以为null,以替换多条记录(所有记录都通过qarams.query匹配,与.find类似)。id无法更改。

app.service('messages').patch(1, {
  text: 'A patched message'
}).then(message => console.log(message));

const params = {
  query: { read: false }
};

// Mark all unread messages as read
app.service('messages').patch(null, {
  read: true
}, params);
PATCH /messages/1
{ "text": "A patched message" }

将所有未读消息标记为已读

PATCH /messages?read=false
{ "read": true }


adapter.remove(id, params)

adapter.remove(id, params) -> Promise移除id标识的记录。id可以为null,以删除多条记录(所有记录都通过qarams.query匹配,与.find类似)。

app.service('messages').remove(1)
  .then(message => console.log(message));

const params = {
  query: { read: true }
};

// Remove all read messages
app.service('messages').remove(null, params);
DELETE /messages/1

移除所有已读消息:

DELETE /messages?read=true


扩展适配器

有两种方法可以扩展现有的数据库适配器:通过扩展ES6基类或通过钩子添加功能。

需要注意,调用原始服务方法将返回一个使用该值解析的Promise。


钩子

最灵活的选择是通过钩子功能。例如,createdAtupdatedAt时间戳可以像这样添加:

const feathers = require('@feathersjs/feathers');

// Import the database adapter of choice
const service = require('feathers-');

const app = feathers().use('/todos', service({
  paginate: {
    default: 2,
    max: 4
  }
}));

app.service('todos').hooks({
  before: {
    create: [
      (context) => context.data.createdAt = new Date()
    ],

    update: [
      (context) => context.data.updatedAt = new Date()
    ]
  }
});

app.listen(3030);


类 (ES6)

所有模块还将ES6类导出为可以直接扩展的Service,如下所示:

'use strict';

const { Service } = require( 'feathers-');

class MyService extends Service {
  create(data, params) {
    data.created_at = new Date();

    return super.create(data, params);
  }

  update(id, data, params) {
    data.updated_at = new Date();

    return super.update(id, data, params);
  }
}

app.use('/todos', new MyService({
  paginate: {
    default: 2,
    max: 4
  }
}));

所有官方数据库适配器都支持查询、排序、限制条数和选择find方法调用的常用方法,作为params.query的一部分。如果id设置为null,则Query还可以应用updatepatchremove方法调用。

注意,通过REST URL使用时,所有查询值都是字符串。 根据数据库的不同,params.query中的值需要在before钩子中转换为正确的类型。


5.3 Querying


等于

不包含特殊查询参数的所有字段将直接进行相等性比较:

// Find all unread messages in room #2
app.service('messages').find({
  query: {
    read: false,
    roomId: 2
  }
});
GET /messages?read=false&roomId=2


$limit

$limit将仅返回指定数量的结果:

// Retrieves the first two unread messages
app.service('messages').find({
  query: {
    $limit: 2,
    read: false
  }
});
GET /messages?$limit=2&read=false

启用分页后,如果只需获取可用记录的数量,则将$limit设置为0。这将仅对数据库运行(快速)计数查询,并返回带有total和空data数组的page对象。


$skip

$skip会跳过指定数量的结果:

// Retrieves the next two unread messages
app.service('messages').find({
  query: {
    $limit: 2,
    $skip: 2,
    read: false
  }
});
GET /messages?$limit=2&$skip=2&read=false


$sort

$sort会根据你提供的对象进行排序。它可以包含一个属性列表,通过该列表对映射到顺序进行排序(1升序,-1降序)。

// Find the 10 newest messages
app.service('messages').find({
  query: {
    $limit: 10,
    $sort: {
      createdAt: -1
    }
  }
});
/messages?$limit=10&$sort[createdAt]=-1


$select

$select用于选择要包含在结果中的字段。这适用于任何服务方法。

// Only return the `text` and `userId` field in a message
app.service('messages').find({
  query: {
    $select: [ 'text', 'userId' ]
  }
});

app.service('messages').get(1, {
  query: {
    $select: [ 'text' ]
  }
});
GET /messages?$select[]=text&$select[]=userId
GET /messages/1?$select[]=text


$in, $nin

查找属性匹配($in)或不匹配($ nin)任何指定值的所有记录。

// Find all messages in room 2 or 5
app.service('messages').find({
  query: {
    roomId: {
      $in: [ 2, 5 ]
    }
  }
});
GET /messages?roomId[$in]=2&roomId[$in]=5


$lt, $lte

查找值小于($lt)或小于等于($lte)指定值的所有记录。

// Find all messages older than a day
const DAY_MS = 24 * 60 * 60 * 1000;

app.service('messages').find({
  query: {
    createdAt: {
      $lt: new Date().getTime() - DAY_MS
    }
  }
});
GET /messages?createdAt[$lt]=1479664146607


$gt, $gte

查找值大于($gt)或大于等于($gte)指定值的所有记录。

// Find all messages within the last day
const DAY_MS = 24 * 60 * 60 * 1000;

app.service('messages').find({
  query: {
    createdAt: {
      $gt: new Date().getTime() - DAY_MS
    }
  }
});
GET /messages?createdAt[$gt]=1479664146607


$ne

查找值等于指定值的所有记录。

// Find all messages that are not marked as archived
app.service('messages').find({
  query: {
    archived: {
      $ne: true
    }
  }
});
GET /messages?archived[$ne]=true


$or

查找符合任何给定条件的所有记录。

// Find all messages that are not marked as archived
// or any message from room 2
app.service('messages').find({
  query: {
    $or: [
      { archived: { $ne: true } },
      { roomId: 2 }
    ]
  }
});
GET /messages?$or[0][archived][$ne]=true&$or[1][roomId]=2


搜索不是常见查询语法的一部分,因为它特定于正在使用的数据库。许多数据库已经支持自己的搜索语法:

有关进一步讨论,请参阅此issue