本文译自Feathers官方API,介绍Feathers的各个模块及其所有API。
- Core: Feathers 核心功能
- Application - Feathers应用程序API
- Services - Service 对象及其方法和Feathers相关功能
- Hooks - 用于服务方法的可插拔中间件
- Events -Feathers服务方法发送的事件
- Channels - 确定要发送给连接的实时客户端的事件
- Errors - Feathers中使用的错误类集合
- Configuration - 用于初始化服务器端应用配置的
node-config
包装器
- Transports: 将 Feathers 应用公开为API服务器
- Client: 关于如何在客户端上使用Feathers的详细介绍
- Authentication: Feathers 认证机制
- Database: Feathers公用数据库适配器API及查询机制
- Adapters - 支持的数据库适配器列表
- Common API - 数据库适配器常用初始化和配置API
- Querying - 常见的查询机制
1. Core
-Feathers 核心功能
1.1 Application
- .use(path, service)
- .service(path)
- .hooks(hooks)
- .publish([event, ] publisher)
- .configure(callback)
- .listen(port)
- .setup([server])
- .set(name, value)
- .get(name)
- .on(eventname, listener)
- .emit(eventname, data)
- .removeListener(eventname, [ listener ])
- .mixins
- .services
- .defaultService
$ 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.io或Primus)传递。params.provider
- 调用本服务所使用的传输 (rest
、socketio
或primus
)。对于服务器的内部调用,将是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
创建一个新资源。该方法应返回带有新创建数据的Promise
。data
也可能是一个数组。
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
修补多个资源。
该方法应返回完整的更新资源数据。如果要区分部分更新和完全更新并支持PATCH
HTTP方法,请另外使用(或代替)update
实现patch
。
注意:成功的patch
方法调用会发出patched
服务事件。
.remove(id, params)
service.remove(id, params) -> Promise
- 删除id
标识的资源。该方法应返回带有已删除资源的Promise
。id
也可以为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
示例
在以下示例中,会在保存数据到数据时添加createdAt
和updatedAt
属性,并记录服务上的任何错误:
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 一个错误
- resolves 一个
// 普通钓子函数 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
是一个只读属性,带有服务方法的名称(find
、get
、create
、update
、patch
、remove
之一)。
context.type
context.type
是一个只读属性,包含此钩子的类型(before
、after
、error
之前)。
context.params
context.params
是一个可写属性,包含服务方法参数(包括params.query
)。更多有关信息,请参阅service params文档。
context.id
context.id
是一个可写属性,id
用于get
、update
、patch
、remove
方法的调用。对于update
、patch
、remove
方法在操作多个实体时context.id
可以是null
。除此以外则是undefined
。
注意,context.id
仅对get
、update
、patch
、remove
方法有效。
context.data
context.data
是一个可写属性,包含用于create
、update
、patch
服务方法调用的数据。
注意,context.id
仅对create
、update
、patch
方法有效。
context.error
context.error
是一个可写属性,带有在失败的方法调用中抛出的错误对象。它仅在错误钩子中可用(即,仅context.type
为error
时)。
context.result
context.result
是一个可写属性,包含成功的服务方法调用的结果。其仅在after
钓子中有效。context.result
同样了可以设置在:
before
钩子中,以跳过实际的服务方法(数据库)调用error
钩子中,用于隐藏错误并返回结果
注意,context.result
仅在context.type
是after
或context.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.result
在before
钓子中设置,原始的服务方法调用会被跳过。
所有其它钩子仍将按正常顺序执行。以下示例始终返回当前经过身份验证的用户,而不是所有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
当相应的服务方法成功返回时,所有服务都会自动发出created
、updated
、patched
和removed
的事件。这适用于客户端以及服务器。当客户端使用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
updated
和patched
成功回调时,updated
和patched
事件将使用回调数据触发。
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.io或Primus)的Feathers服务器上,事件通道确定哪些连接的客户端发送实时事件以及应该如何发送数据。
如果没有使用实时传输服务器(如,仅使用REST API或在客户端上使用Feathers时),则通道功能将无法使用。
通的一些应用场景:
- 实时事件应仅发送给经过身份验证的用户
- 用户只有在加入某个聊天室时才能获得有关消息的更新
- 只有同一组织中的用户才能收到有关其数据更改的实时更新
- 创建新用户时,只应通知管理员
- 创建,修改或删除用户时,非管理员应仅接收用户对象的“安全”版本(如,仅限
email
、id
和avatar
)
示例
下面的示例显示了生成的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
中设置客户端“安全”数据来处理,但在某些情况下,仍然可以更改某些通道的事件数据。
将按以下顺序由第一个可用的事件数据确定将发送哪些数据:
- 来自
channel.send(data)
的data
- context.dispatch
- 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); });
发布者优先级
将会按以下顺序找到的第一个发布者回调:
- 特定事件的服务发布者
- 所有事件的服务发布者
- 特定事件的应用发布者
- 适用于所有事件的应用发布者
保持通道更新
每个应用程序都不同,因此保持分配给通道的连接是最新的(如,如果用户加入/离开房间)取决于你。
通常,通道应反映你的持久性应用程序数据。这意味着通常它不需要例如要求直接加入通道的用户。这在运行多个应用程序实例时尤其重要,因为通道仅是当前的本地实例。
相反,相关信息(例如,用户当前所在的房间)应该存储在数据库中,然后可以将活动连接重新分配到收听适当服务事件的适当通道中。
以下示例在更新或删除用户对象(假定其房间数组是用户已加入的房间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/configuration
是node-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
- express(app)
- express()
- app.use(path, service|mw|[mw])
- app.listen(port)
- app.setup(server)
- express.rest()
- express.notFound(options)
- express.errorHandler()
- Routing-路由
$ npm install @feathersjs/express --save
@feathersjs/expres模块包含了Express框架与Feathers的集成:
- Express框架绑定使Feathers应用与Express兼容
- 基于Express的传输,通过REST API公开服务
- 基于Feathers错误的Express错误处理程序
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公开和使用服务。这意味着你可以通过GET
、POST
、PUT
、PATCH
和DELETE
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.listen
或app.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.request
和socket.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应用实例的使用方式与服务器上的完全相同。
与客户最相关的模块是:
@feathersjs/feathers
初始化一个新的Feathers应用@feathersjs/rest-client
通过REST HTTP连接服务。@feathersjs/socketio-client
通过Socket.io连接服务。@feathersjs/primus-client
通过Primus连接服务。@feathersjs/authentication-client
用于验证客户端。
提示:不用在客户端上使用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-socketio
的pingTimeout
。这将停止与此类警告的相关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/@feathersjs
和node_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客户端,则应遵循Node和React 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
允许使用jQuery、request、Superagent、Axios或Fetch作为AJAX库连接到通过Express REST API公开的服务。
REST客户端服务会发出created
、updated
、patched
和removed
,但仅在本地为其自身的实例发出。来自其他客户端的实时事件只能通过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 } });
使用fetch
forkyetch,它也可以用于中止请求:
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
。其他服务参数可以通过hooks和Express中间件设置。URL查询参数值始终为字符串。转换(如:字符串'true'
转换为布尔值true
)也可以在钩子中完成。
POST
、PUT
和PATCH
的请求体类型由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
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
事件来验证套接字。相关示例,请参阅local和jwt身份验证章节中的“直接连接”部分。
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
当服务update
或patch
成功返回时,updated
和patched
事件将与回调数据一起发送。
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
事件来验证套接字。有关具体示例,请参阅local和jwt身份验证章节中的“直接连接”部分。
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
当服务update
或patch
成功返回时,updated
和patched
事件将与回调数据一起发送。
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-服务器端
- 互补插件
app.configure(auth(options))
- Options-选项
app.service('authentication')
app.passport
auth.hooks.authenticate(strategies)
- 事件验证
- Express 中间件
- 完整示例
$ npm install @feathersjs/authentication --save
@feathersjs/authentication用于JWT身份验证,它主要有三个目的:
- 设置
/authentication
端点,以创建JSON Web Tokens (JWT)。JWT用作访问令牌。可以在jwt.io上了解更多信息。 - 为所有Feathers传输提供一致的身份验证API
- 为使用Passport策略保护端点的身份验证插件提供框架。
互补插件
以下插件是互补的,但完全是可选的:
- 在客户端上使用身份验证服务器:@feathersjs/authentication-client
- 本地(用户名/密码)身份验证:@feathersjs/authentication-local
- JWT身份验证:@feathersjs/authentication-jwt
- OAuth1身份验证:@feathersjs/authentication-oauth1
- OAuth2身份验证:@feathersjs/authentication-oauth2
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服务,它只实现了create
和remove
方法。/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}
- 传给jsonwebtokensign()
的选项secret {String | Buffer}
- HMAC算法的密钥,或用于RSA和ECDSA的PEM编码私钥。jwt
- 其它可用选项,参阅jsonwebtoken
。authenticate方法使用默认jwt选项。直接使用此包时,必须手动传递它们。
返回的promise
将使用JWT解析或导致失败的错误。
app.passport.verifyJWT(token, options)
使用options
验证传入的JWTtoken
的签名和有效负载。
token {JWT}
- 待验证的 JWToptions {Object}
传给 jsonwebtokenverify()
的选项参数secret {String | Buffer}
- HMAC算法的密钥,或用于RSA和ECDSA的PEM编码私钥。- 其它有效选项参阅
jsonwebtoken
文档
返回的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
对象上发出login
和logout
事件。(使用JWT时,注销不会使JWT无效。(有关详细信息,请阅读JWT部分。)这些事件仅在服务器上发出。
app.on('login', callback))
、app.on('logout', callback))
这两个事件使用具有相同签名的callback
函数。
result
{Object} - 来自authentication
服务的最终context.result
。除非你在after钩子中自定义context.response
,其将包含一个唯一表示JWT的属性accessToken
。meta
{Object} - 请求相关的信息。meta
因transport/provider
而异,如下所示:- 使用
@feathersjs/express/rest
provider
{String} - 为"rest"
req
{Object} - Express 的请求对象res
{Object} - Express 的响应对象
- 使用
feathers-socketio
和feathers-primus
:provider
{String} - transport 名为:socketio
或primus
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
- 发送login
和logout
事件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
app.configure(auth(options))
- Options-选项
app.authenticate()
app.authenticate(options)
app.logout()
app.passport
- 认证事件
app.on('authenticated', callback)
app.on('logout', callback)
app.on('reauthentication-error', errorHandler)
- 钩子
- 完整示例
@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个核心部分:
- 主要初始化功能
hashPassword
钩子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个核心部分:
- 主要初始化功能
Verifier
类- 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个核心部分:
- 主要初始化功能
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
,并返回一个accessToken
、refreshToken
和一个profile
,其中包含经过身份验证的实体的信息。
默认情况下,Verifier
接受提供程序返回的所有内容,并将其附加到提供程序名称下的entity
(即用户对象)。如果希望自定义返回的数据,则可以通过在entity
服务上的update
和create
服务方法中添加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个核心部分:
- 主要初始化功能
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)进行身份验证时,提供程序都会发返回一个accessToken
、refreshToken
和一个profile
文件,其包含了基于你请求并已授予的OAuth2范围的经过身份验证的实体的信息。
默认情况下,Verifier
接收提供程序返回的所有内容,并将其附加到提供程序名称下的实体(即用户对象)。如果希望自定义返回的数据,则可以通过在entity
服务上的update
和create
服务方法中添加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 - 内存/文件系统
- feathers-memory - 内存数据库适配器
- feathers-localstorage - 客户端Feathers的适配器,可以使用浏览器LocalStorage或ReactNative的AsyncStorage。
- feathers-nedb - NeDB数据库适配器,基于内存或文件系统的独立数据库。
SQL
- feathers-knex - KnexJS适配器,NodeJS的SQL查询构建器,支持PostgreSQL,MySQL,SQLite和MSSQL
- feathers-sequelize -Sequelize适配器,NodeJS的ORM,支持PostgreSQL,MySQL,SQLite和MSSQL
- feathers-objection - Objection.js服务适配器 - 在Knex之上构建的最小SQL ORM。
NoSQL
- feathers-mongoose - Mongoose数据库适配器,NodeJS和MongoDB的对象建模库
- feathers-mongodb - 使用官方数据库驱动程序的NodeJSMongoDB数据库适配器。
- feathers-elasticsearch - Elasticsearch数据库适配器
- feathers-rethinkdb - RethinkDB数据库适配器,实时数据库
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
(可选) - 分页对象,包含default
和max
页大小whitelist
(可选) - 允许的其他非标准查询参数列表 (如[ '$regex', '$populate' ]
)multi
(可选, 默认:false
) - 允许create
使用数组,update
和remove
使用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) -> Promise
将data
与id
标识的记录合并。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。
钩子
最灵活的选择是通过钩子功能。例如,createdAt
和updatedAt
时间戳可以像这样添加:
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还可以应用update
、patch
和remove
方法调用。
注意,通过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
Search-搜索
搜索不是常见查询语法的一部分,因为它特定于正在使用的数据库。许多数据库已经支持自己的搜索语法:
- Mongoose,MongoDB和NeDB的正则表达式(在钩子中转换)。参阅这个评论
- 对于MongoDB,可以参阅feather-mongodb-fuzzy-search
- 对于NeDB,可以参阅feather-nedb-fuzzy-search
- Sequelize的$like,可以在params.sequelize中设置
- 某些数据库适配器(如KnexJS,RethinkDB和Elasticsearch)也支持非标准查询参数,这些参数在其文档页面中进行了说明。
- 有关其他搜索功能,请参阅feathers-solr
有关进一步讨论,请参阅此issue。