本指南涵盖了Feathers应用所有的基础知识和核心概念。
1. 配置
在本节中,将介绍学习Feathers所需的工具和初步知识。
先决条件
Feathers及其大多数插件工作于 NodeJSv6.0.0及以上。而在本指南将使用仅适用于Node v8.0.0及更高版本的语法。在MacOS和其他类Unix系统上,Node Version Manager是快速安装最新版NodeJS并使其保持最新的好方法。
成功安装后,就可以在控制台使用node
和npm
命令,其使用方式类似如下:
$ node --version v8.5.0
$ npm --version 5.5.1
Feathers可以工作于浏览器中并支持IE 10及更高版本。但是,本指南中所使用的示例仅适用于最新版本的Chrome,Firefox,Safari和Edge。
你应该知道的
你应该有较好的JavaScript使用经验及对ES6特性有足够了解,并有一些NodeJS的经验以及它所支持的JavaScript功能,如模块系统。另外,熟悉HTTP和REST API以及websockets也会很有帮助。
本指南中的示例使用async/await。强烈建议熟悉Promises和async/await
(以及它们如何交互)。有关JavaScript Promise的详细介绍请参阅Mpromisejs.org,以及这篇介绍async/await
的博客文章。
Feathers独立工作,但也提供与Express的集成。本指南不需要任何深入的Express知识,但有一些使用Express的经验将来会有所帮助(请参阅Express入门指南)。
本指南不会涉及的
虽然Feathers适用许多数据库,但本指南仅使用独立的数据库适配器示例,所以无需运行数据库服务器。
关于身份验证的介绍,会在chat application guide中。
所有示例都会放在在单个文件中。Feathers生成器(CLI)会为Feathers应用创建推荐的结构。你可以在Generator guide中查看如何构建应用,以及如何在聊天应用指南中使用它。
2. 入门-构建第一个Feathers应用
接下来,让我们创建第一个Feathers应用,其可以在NodeJS和浏览器端运行。首先,创建一个工作目录:
mkdir feathers-basics cd feathers-basics
由于所有Feathers应用都是Node应用,所以可以使用npm
创建一个默认的package.json:
npm init --yes
安装Feathers
通过npm
安装@feathersjs/feathers包,就可以像任何其他Node模块一样安装Feathers。相同的包也可以与Browserify或Webpack和React Native等模块加载器一起使用。
npm install @feathersjs/feathers --save
注意:所有Feathers核心模块都在@feathersjs
命令空间下。
第一个应用
所有Feathers应用的基础是app对象,可以像这样创建:
const feathers = require('@feathersjs/feathers'); const app = feathers();
在应用程序
对象中有几种方法,最重要的是它允许我们注册服务。我们将在后面介绍更多有关服务内容,现在我们通过创建app.js
文件(在当前文件夹中)注册并使用只有get
方法的简单服务,如下所示:
const feathers = require('@feathersjs/feathers'); const app = feathers(); // 注册一个简单的 todo 服务,其会返回名称和一些文本 app.use('todos', { async get(name) { // 返回一个对象,格式为:{name, text} return { name, text: `You have to do ${name}` }; } }); // 从服务获取并记录待办事项的函数 async function getTodo(name) { // 获取上面注册的服务 const service = app.service('todos'); // 通过名称调用`get`方法 const todo = await service.get(name); // 记录返回的待办事项 console.log(todo); } getTodo('dishes');
现在可以运行这个应用程序:
node app.js
然后会看到:
{ name: 'dishes', text: 'You have to do dishes' }
有关Feathers应用程序对象的更多信息,请参阅Application API文档。
浏览器端
上面创建的Feathers应用程序也可以在浏览器中运行。加载Feathers的最简单方法是通过<script>
标签指向一个CDN版本Feathers。加载后将使feathers
全局变量可用。
创建一个新文件夹:
mkdir public
我们还需要使用Web服务器托管该文件夹。这里可以通过像Apache这样的web服务器或者可以安装http-server模块并托管public/
来实现,如下所示:
npm install http-server -g http-server public/
然后,在public/
目录中添加两个文件。一个index.html
来加载Feathers:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Feathers Basics</title> </head> <body> <h1>Welcome to Feathers</h1> <p>Open up the console in your browser.</p> <script type="text/javascript" src="//unpkg.com/@feathersjs/client@^3.0.0/dist/feathers.js"></script> <script src="client.js"></script> </body> </html>
再添加一个clinet.js
文件:
const app = feathers(); // Register a simple todo service that return the name and a text app.use('todos', { async get(name) { // Return an object in the form of { name, text } return { name, text: `You have to do ${name}` }; } }); // A function that gets and logs a todo from the service async function logTodo(name) { // Get the service we registered above const service = app.service('todos'); // Call the `get` method with a name const todo = await service.get(name); // Log the todo we got back console.log(todo); } logTodo('dishes');你可能会注意到它与我们的Node版本的
app.js
几乎相同,只是缺少feathers
导入,因为它已经做为全局变量。
现在可以在浏览器中打开localhost:8080
,在浏览器控制台输入内容即可看到结果打印。
注意:还可以使用Webpack或Browserify等模块加载程序加载Feathers。有关更多信息,请参阅客户端API。
3. 服务
服务(Service
)是每个Feathers应用的核心,是JavaScript对象或实现某些方法的类的实例。服务提供了统一、协议独立的接口,用于与任何类型的数据进行交互,例如:
- 读写数据库
- 与文件系统交互
- 调用另一个API
- 调用其它服务,如:
- 发送邮件
- 处理付款
- 返回某地天气等
协议独立意味着对于Feathers服务而言,它的内部调用并不重要,可以通过REST API或websockets(稍后讨论)或其他方式调用。
服务方法
服务方法是服务对象可以实现的CRUD方法。Feathers服务的方法有:
find
- 查找所有数据(可与查询匹配的)get
- 通过唯一标识符获取单条数据create
- 创建新数据记录update
- 通过完全替换的方式来更新已存在的单条数据patch
- 通过与新数据合并来更新一个或多条数据remove
- 删除一个或多条已存在的数据
以下是一个Feathers服务接口示例,其可以是普通对象或JavaScript类:
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) {} } app.use('/my-service', 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) {} } app.use('/my-service', new myService());
服务方法的参数:
id
- 数据的唯一标识符data
- 用户发送的数据(用于创建和更新)params
(可选) - 其他参数,如:验证用户身份或查询
注意:服务不必实现所有方法,但至少实现一个。
更多关于服务、服务方法和参数的详细信息,请参阅Service API文档。
一个消息服务
接下来,实现一个自己的聊天消息服务,允许我们在内存中查找、创建、删除和更新消息。在这里,我们将使用JavaScript类来处理我们的消息,但正如我们在上面看到的,它也可以是一个普通的对象。
以下是完整的app.js
及注释:
const feathers = require('@feathersjs/feathers'); class Messages { constructor() { this.messages = []; this.currentId = 0; } async find(params) { // 返回所有消息的列表 return this.messages; } async get(id, params) { // 按id查找消息 const message = this.messages.find(message => message.id === parseInt(id, 10)); // 如果没找到抛出错误 if(!message) { throw new Error(`Message with id ${id} not found`); } // 返回消息 return message; } async create(data, params) { // 使用原始数据创建一个新对象,并从递增的`currentId`计数器中获取一个id const message = Object.assign({ id: ++this.currentId }, data); this.messages.push(message); return message; } async patch(id, data, params) { // 获取已存在的消息。未找到则抛出错误 const message = await this.get(id); // 使用新数据与已存在的消息合并 // 并返回结果 return Object.assign(message, data); } async remove(id, params) { // 通过id获取消息,未找到则抛出错误 const message = await this.get(id); // 在消息数组中查找消息的索引 const index = this.messages.indexOf(message); // 从我数组中删除找到的消息 this.messages.splice(index, 1); // 返回已删除的消息 return message; } } const app = feathers(); // 通过创建类的新实例来初始化消息服务 app.use('messages', new Messages());
使用服务
可以通过调用app.use(path, service)
)在Feathers应用上注册服务对象。其中,path
将做为服务的名称(以及URL,如果它作为API公开,这将在后面介绍)
我们可以通过app.service(path)
检索该服务,然后调用它的任何服务方法。 将以下内容添加到app.js
的末尾:
async function processMessages() { await app.service('messages').create({ text: 'First message' }); await app.service('messages').create({ text: 'Second message' }); const messageList = await app.service('messages').find(); console.log('Available messages', messageList); } processMessages();
然后运行:
node app.js
会看到以下输出:
Available messages [ { id: 1, text: 'First message' }, { id: 2, text: 'Second message' } ]
服务事件
服务注册后会自动成为NodeJS EventEmitter,当修改数据(create
、update
、patch
、remove
)的服务方法返回时,它会发送带有新数据的事件。可以使用app.service('messages').on('eventName',data => {})
监听事件。以下是服务方法及其相应事件的列表:
Service method | Service event |
---|---|
service.create() |
service.on('created') |
service.update() |
service.on('updated') |
service.patch() |
service.on('patched') |
service.remove() |
service.on('removed') |
接下来,我们会看到这些事件是如何使用,这也是Feathers实现实时功能的关键。现在,更新app.js
中的processMessages
函数,如下所示:
async function processMessages() { app.service('messages').on('created', message => { console.log('Created a new message', message); }); app.service('messages').on('removed', message => { console.log('Deleted message', message); }); await app.service('messages').create({ text: 'First message' }); const lastMessage = await app.service('messages').create({ text: 'Second message' }); // 删除刚创建的消息 await app.service('messages').remove(lastMessage.id); const messageList = await app.service('messages').find(); console.log('Available messages', messageList); } processMessages();
再次运行应用:
node app.js
然后就可以看到事件处理程序是如何记录创建和删除消息信息的,如下所示:
Created a new message { id: 1, text: 'First message' } Created a new message { id: 2, text: 'Second message' } Deleted message { id: 2, text: 'Second message' } Available messages [ { id: 1, text: 'First message' } ]
4. 钩子
在前面的介绍中,Feathers 服务是实现数据存储和修改的好方法。从技术上讲,我们可以在服务中实现所有应用程序逻辑,但通常应用程序会有跨多个服务的类似功能。例如,需要检查所有服务中允许用户调用的服务方法、或将当前日期添加到我们正在保存的所有数据,我们可能希望检查所有服务。只使用服务,就必须每次都重新实现这一点。
这就是需要引入Feathers钩子的地方。钩子是可插入的中间件功能,可以注册在服务方法before
、after
或error
上。你可以注册单个钩子函数或创建钩子函数链,以创建复杂的工作流程。
就像服务本身一样,钩子与传输无关。它们通常也是服务不可知的,这意味着它们可以与任何服务一起使用。这一模式使你应用程序逻辑保持灵活、可组合、并且更容易跟踪和调试。
钩子通常用于处理诸如验证、授权、日志记录、填充相关实体、发送通知等。
备注:钩子API完整文档请参阅钩子API文档。
示例
以下示例简单演示了一个在调用实际的create
服务方法前向数据添加createdAt
属性的钩子:
app.service('messages').hooks({ before: { create (context) { context.data.createdAt = new Date(); return context; } } })
钩子函数
钩子函数是一个函数,它将钩子上下文作为参数并返回该上下文或什么都不返回。钩子函数会按照它们注册的顺序运行,并且只有在当前钩子函数执行完后才会继续到下一个。如果钩子函数抛出错误,将跳过所有剩余的钩子(可能还有服务调用),并返回错误。
使钩子更易于复用的常见模式(例如,使上面的示例中的createdAt
属性名称可配置)是创建一个包装函数,它接受这些选项并返回一个钩子函数:
const setTimestamp = name => { return async context => { context.data[name] = new Date(); return context; } } app.service('messages').hooks({ before: { create: setTimestamp('createdAt'), update: setTimestamp('updatedAt') } });
如上所示,现在我们有了一个可重用的钩子,它可以在任何属性上设置时间戳。
钩子上下文
钩子上下文(content
)是一个对象,它包含有关服务方法调用的信息。具有只读和可写属性。只读属性包括:
context.app
- Feathers 应用对象context.service
- 钩子当前正在运行的服务context.path
- 服务的路径(名称)context.method
- 服务的方法context.type
- 钩子的类型(before
,after
或error
)
可写属性包括:
context.params
- 服务方法调用的params
。对于外部调用而言,params
通常包含:context.params.query
- 服务调用的查询(如,REST的查询字符串)context.params.provider
- 己完调用传输方式的名称。一般是rest
、socketio
、primus
。内部调用时为undefined
context.id
- 服务方法调用get
,remove
,update
和patch
的id
context.data
-create
,update
和patch
服务方法调用中用户发送的data
context.error
- 所抛出的错误 (error
钩子中)context.result
- 服务方法调用的结果 (after
钩子中)
注册钩子
注册钩子最常用的方法是在像这样的对象中:
const messagesHooks = { before: { all: [], find: [], get: [], create: [], update: [], patch: [], remove: [], }, after: { all: [], find: [], create: [], update: [], patch: [], remove: [], } }; app.service('messages').hooks(messagesHooks);
这样就可以一目了然地查看执行钩子的顺序以及使用哪种方法。
注意:all
是一个特殊的关键字,这意味着这些钩子将在此链中指定方法的钩子之前运行。
例如,如果钩子像下面这样注册:
const messagesHooks = { before: { all: [ hook01() ], find: [ hook11() ], get: [ hook21() ], create: [ hook31(), hook32() ], update: [ hook41() ], patch: [ hook51() ], remove: [ hook61() ], }, after: { all: [ hook05() ], find: [ hook15(), hook16() ], create: [ hook35() ], update: [ hook45() ], patch: [ hook55() ], remove: [ hook65() ], } }; app.service('messages').hooks(messagesHooks);
各个钩子的执行顺序如下图所示:
验证数据
如果一个钩子发生错误,其后所有的钩子将会跳过执行,错误会返回给用户。
这使before
钩子成为通过抛出无效数据错误来验证传入数据的好地方。我们可以抛出一个普通的JavaScript错误或者Feathers错误,Feathers错误会有一些额外的功能(比如为REST调用返回正确的错误代码)。
@feathersjs/errors
是一个独立的模块,你需要像下面这样安装它:
npm install @feathersjs/errors --save
我们只需要用于create
、update
和patch
的钩子,因为仅这些服务方法是允许用户提交数据的:
const { BadRequest } = require('@feathersjs/errors'); const validate = async context => { const { data } = context; // Check if there is `text` property if(!data.text) { throw new BadRequest('Message text must exist'); } // Check if it is a string and not just whitespace if(typeof data.text !== 'string' || data.text.trim() === '') { throw new BadRequest('Message text is invalid'); } // Change the data to be only the text // This prevents people from adding other properties to our database context.data = { text: data.text.toString() } return context; }; app.service('messages').hooks({ before: { create: validate, update: validate, patch: validate } });
应用的钩子
有时我们想在Feathers应用程序中为每个服务自动添加一个钩子。以下是应用程序可使用的钩子,它们的工作方式与服务的挂钩相同,但以更具体的顺序运行:
before
该应用级的钩子会在所有服务的before钩子之前执行after
该应用级的钩子会在所有服务的after钩子之后执行error
该应用级的钩子会在所有服务的error钩子之后执行
错误记录
应用程序挂钩的一个很好用途是记录任何服务方法调用错误。以下示例使用路径、方法名称以及错误堆栈记录了每个服务方法错误:
app.hooks({ error: async context => { console.error(`Error in '${context.path}' service method '${context.method}'`, context.error.stack); } });
更多示例
聊天应用指南将使用更多示例,例如如何关联数据以及为生成器创建的挂钩添加用户信息。
5. REST APIs
在前面的章节中,我们了解了Feathers服务和钩子,并创建了一个在NodeJS和浏览器中工作的消息服务。我们看到了Feathers如何自动发送事件,但到目前为止我们并没有真正创建其他人可以使用的Web API。
这就是Feathers 传输的目的。传输是一个插件,可以将Feathers应用程序转换为服务器,通过不同的协议公开我们的服务,以供其他客户端使用。由于传输涉及运行服务器,所以无法在浏览器中运行,但稍后我们会了解到,在浏览器Feathers应用程序中通过插件连接到Feathers服务器的介绍。
目前,Feathers拥有三种传输方式:
- 基于Express的HTTP REST - 用于通过JSON REST API公开服务
- Socket.io - 通过websockets连接服务并接收实时服务事件
- Primus - Socket.io的替代方案,支持几个实时事件的websocket协议
在本章中,我们将介绍HTTP REST传输和Feathers Express框架集成。
Feathers的目标之一是使构建REST API更容易,因为它是迄今为止最常用的Web API协议。例如,我们要发送像GET / messages / 1
这样的请求,并获得像{ "id": 1, "text": "The first message" }
的JSON响应。你可能已经注意到Feathers服务方法和GET
、POST
、PATCH
和DELETE
等HTTP方法相互补充:
Service method | HTTP method | Path |
---|---|---|
.find() | GET | /messages |
.get() | GET | /messages/1 |
.create() | POST | /messages |
.update() | PUT | /messages/1 |
.patch() | PATCH | /messages/1 |
.remove() | DELETE | /messages/1 |
Feathers REST传输的基本功能是自动将现有服务方法映射到这些请求点。
Express集成
Express是用于创建Web应用程序和API的很流行的一个Node框架。Feathers Express集成允许我们将Feathers应用程序转换为既是Feathers应用程序又是完全兼容的Express应用程序的应用。这样你可以使用诸如服务之类的Feathers功能以及任何现有的Express中间件。如前所述,Express框架集成仅适用于服务器端。
要添加集成需要安装@feathersjs/express
:
npm install @feathersjs/express --save
然后我们可以初始化一个Feathers and Express应用,它会将服务作为REST API并在端口3030
上公开,如下所示:
const feathers = require('@feathersjs/feathers'); const express = require('@feathersjs/express'); // 创建应用,其会是一个 Express和Feathers应用 const app = express(feathers()); // 为REST服务启用JSON正文解析 app.use(express.json()); // 为REST服务启用URL编码的正文解析 app.use(express.urlencoded({ extended: true })); // 使用Express设置REST传输 app.configure(express.rest()); // 设置一个错误处理程序,以提供更友好的错误 app.use(express.errorHandler()); // 在 3030 端口上启动服务器 app.listen(3030);
express.json
、express.urlencoded
和express.errorHandler
是普通的Express中间件。我们仍然可以使用app.use
来注册Feathers服务。
有关Express框架集成的更多信息,请参阅Express API章节
消息的REST API
上面的代码实际上是我们将消息服务转换为REST API所需的全部内容。以下是我们的app.js
的完整代码,它通过REST API从公开Service中的服务:
const feathers = require('@feathersjs/feathers'); const express = require('@feathersjs/express'); class Messages { constructor() { this.messages = []; this.currentId = 0; } async find(params) { // 返回所有消息的列表 return this.messages; } async get(id, params) { // 按id查找消息 const message = this.messages.find(message => message.id === parseInt(id, 10)); // 如果没找到抛出错误 if(!message) { throw new Error(`Message with id ${id} not found`); } // 返回消息 return message; } async create(data, params) { // 使用原始数据创建一个新对象,并从递增的`currentId`计数器中获取一个id const message = Object.assign({ id: ++this.currentId }, data); this.messages.push(message); return message; } async patch(id, data, params) { // 获取已存在的消息。未找到则抛出错误 const message = await this.get(id); // 使用新数据与已存在的消息合并 // 并返回结果 return Object.assign(message, data); } async remove(id, params) { // 通过id获取消息,未找到则抛出错误 const message = await this.get(id); // 在消息数组中查找消息的索引 const index = this.messages.indexOf(message); // 从我数组中删除找到的消息 this.messages.splice(index, 1); // 返回已删除的消息 return message; } } const app = express(feathers()); // 为REST服务启用JSON正文解析 app.use(express.json()) // 为REST服务启用URL编码的正文解析 app.use(express.urlencoded({ extended: true })); // 使用Express设置REST传输 app.configure(express.rest()); // 通过创建类的新实例来初始化消息服务 app.use('messages', new Messages()); // 设置一个错误处理程序,以提供更友好的错误 app.use(express.errorHandler()); // 在 3030 端口上启动服务器 const server = app.listen(3030); // 使用该服务在服务器上创建新消息 app.service('messages').create({ text: 'Hello from the server' }); server.on('listening', () => console.log('Feathers REST API started at http://localhost:3030'));
启动服务器:
node app.js
服务器启动后会保持动行,可以在控制台使用Control + C来停止服务器。每次修改app.js
后都需要停止并重新启动应用。
使用API
服务器启动后,可以在浏览器中输入localhost:3030/messages
。因为我们已经在服务器上创建了一条消息,所以收到以下JSON响应:
[{"id":1,"text":"Hello from the server"}]
也可以通过localhost:3030/messages/1
来获取这条消息。
现在可以在命令行上使用cURL命令将带有JSON数据的POST请求发送到同一URL来创建新消息,如下所示:
curl -X POST \ http://localhost:3030/messages/ \ -H 'Content-Type: application/json' \ -d '{ "text": "Hello from the command line!" }'
刷新localhost:3030/messages
即可看到新创建的消息。
删除消息可以通过向URL发送DELETE
命令实现:
curl -X DELETE \ http://localhost:3030/messages/1
6. 数据库
在Service章节中,我们创建了一个可以创建、更新和删除消息的自定义在内存中消息服务。可以想象我们是如何使用数据库实现相同的功能,而不是将消息存储在内存中,因为实际上没有Feathers不支持的数据库。
自己编写所有代码是非常重复和繁琐的,这就是为什么Feathers为不同的数据库提供了一系列预构建服务。它们提供了大多数基本功能,并且可以使用钩子根据你的要求进行定制。Feathers数据库适配器支持许多流行数据库和NodeJS ORM的常用API、分页和查询语法:
Database | Adapter |
---|---|
内存 | feathers-memory, feathers-nedb |
本地存储、异步存储 | feathers-localstorage |
文件系统 | feathers-nedb |
MongoDB | feathers-mongodb, feathers-mongoose |
MySQL, PostgreSQL, MariaDB, SQLite, MSSQL | feathers-knex, feathers-sequelize, feathers-objection |
Elasticsearch | feathers-elasticsearch |
RethinkDB | feathers-rethinkdb |
以上每个链接的适配器在其自述文件中都有一个完整的REST API示例。
在本章中,我们将了解内存数据库适配器的基本用法。
内存数据库
feathers-memory是一个Feathers数据库适配器 - 类似于我们的消息服务 - 将会其数据存储在内存中。我们用它来演示,是因为它也可以在浏览器中使用。
接下来安装它:
npm install feathers-memory --save
我们可以通过引用并使用我们所需的选项初始化来使用适配器。在这里,我们会启用分页,默认显示10条,最多25条(这样客户端不会因为一次请求所有数据导致服务器崩溃):
const feathers = require('@feathersjs/feathers'); const memory = require('feathers-memory'); const app = feathers(); app.use('messages', memory({ paginate: { default: 10, max: 25 } }));
这样,我们就为具有查询功能的消息提供了完整的CRUD服务。
浏览器端
我们还可以在浏览器中包含feathers-memory
,在浏览器中最简单的加载构建,将其添加为feathers.memory
。在public/index.html
中:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Feathers Basics</title> </head> <body> <h1>Welcome to Feathers</h1> <p>Open up the console in your browser.</p> <script type="text/javascript" src="//unpkg.com/@feathersjs/client@^3.0.0/dist/feathers.js"></script> <script type="text/javascript" src="//unpkg.com/feathers-memory@^2.0.0/dist/feathers-memory.js"></script> <script src="client.js"></script> </body> </html>
public/client.js
:
const app = feathers(); app.use('messages', feathers.memory({ paginate: { default: 10, max: 25 } }));
查询
如前所述,所有数据库适配器都支持使用params.query
在find
方法调用中查询数据的常用方法。你可以在查询语法API中找到完整列表。
启用分页后,find方法将返回具有以下属性的对象:
data
- 当前数据列表limit
- 每页大小skip
- 要跳过的条数total
- 此查询的总条数
以下示例自动创建100条消息并进行一些查询。可以在app.js
和public/client.js
的末尾添加它,以便在Node和浏览器中查看:
async function createAndFind() { // Stores a reference to the messages service so we don't have to call it all the time const messages = app.service('messages'); for(let counter = 0; counter < 100; counter++) { await messages.create({ counter, message: `Message number ${counter}` }); } // We show 10 entries by default. By skipping 10 we go to page 2 const page2 = await messages.find({ query: { $skip: 10 } }); console.log('Page number 2', page2); // Show 20 items per page const largePage = await messages.find({ query: { $limit: 20 } }); console.log('20 items', largePage); // Find the first 10 items with counter greater 50 and less than 70 const counterList = await messages.find({ query: { counter: { $gt: 50, $lt: 70 } } }); console.log('Counter greater 50 and less than 70', counterList); // Find all entries with text "Message number 20" const message20 = await messages.find({ query: { message: 'Message number 20' } }); console.log('Entries with text "Message number 20"', message20); } createAndFind();
做为REST API
在REST API章节中,我们从自定义消息服务创建了一个REST API。 使用数据库适配器将使我们的app.js
更短:
const feathers = require('@feathersjs/feathers'); const express = require('@feathersjs/express'); const memory = require('feathers-memory'); const app = express(feathers()); // 为REST服务启用JSON正文解析 app.use(express.json()) // 为REST服务启用URL编码的正文解析 app.use(express.urlencoded({ extended: true })); // 使用Express设置REST传输 app.configure(express.rest()); // 初始化消息服务 app.use('messages', memory({ paginate: { default: 10, max: 25 } })); // 设置错误处理程序 app.use(express.errorHandler()); // 在 3030 端口上启动服务器 const server = app.listen(3030); // 使用该服务在服务器上创建新消息 app.service('messages').create({ text: 'Hello from the server' }); server.on('listening', () => console.log('Feathers REST API started at http://localhost:3030'));
node app.js
启动服务后,可以使用查询,如localhost:3030/messages?$limit=2
更多关于URL查询语法的使用,请参阅查询语法API文档。
7. 实时 APIs
在Service章节中,我们看到了Feathers服务会在create
、update
、patch
或remove
服务方法返回时,自动发送created
、updated
、patched
和removed
事件。实时意味着这些事件也会发送到所连接的客户端,以便他们可以做出相应的反应,例如, 更新UI等。
要实现与客户的实时通信,我们需要一种支持双向通信的传输。在Feathers中,这些是Socket.io和Primus传输,它们都使用websockets来接收实时事件并调用服务方法。
在本章中,我们将使用Socket.io并创建一个仍支持REST端的数据库支持的实时API。
使用传输
安装:
npm install @feathersjs/socketio --save
可以配置Socket.io传输并使用标准配置,如下所示:
const feathers = require('@feathersjs/feathers'); const socketio = require('@feathersjs/socketio'); // 创建 Feathers 应用 const app = feathers(); // 配置 Socket.io 传输 app.configure(socketio()); // 在 3030 端口上启动服务器 app.listen(3030);
还可以与REST API设置结合使用:
const feathers = require('@feathersjs/feathers'); const express = require('@feathersjs/express'); const socketio = require('@feathersjs/socketio'); // 创建应用,其会一个 Express和Feathers const app = express(feathers()); // 为REST服务启用JSON正文解析 app.use(express.json()) // 为REST服务启用URL编码的正文解析 app.use(express.urlencoded({ extended: true })); // 使用Express设置REST传输 app.configure(express.rest()); // 配置 Socket.io 传输 app.configure(socketio()); // 设置错误处理程序 app.use(express.errorHandler()); // 在 3030 端口上启动服务器 app.listen(3030);
频道
通道确定应将哪些实时事件发送到哪个客户端。例如,我们可能只想向经过身份验证的用户或同一房间用户发送消息。但在此示例中,我们仅为所有连接启用实时功能:
// 在所有实时连接上,将其添加到`everybody`频道 app.on('connection', connection => app.channel('everybody').join(connection)); // 发布所有事件到`everybody`频道 app.publish(() => app.channel('everybody'));
更多关于频道地介绍,请参阅channel API文档
消息API
总而言之,我们的REST和带有消息服务app.js
的实时API类似如下:
const feathers = require('@feathersjs/feathers'); const express = require('@feathersjs/express'); const socketio = require('@feathersjs/socketio'); const memory = require('feathers-memory'); // 创建应用,其会一个 Express和Feathers const app = express(feathers()); // 为REST服务启用JSON正文解析 app.use(express.json()) // 为REST服务启用URL编码的正文解析 app.use(express.urlencoded({ extended: true })); // 使用Express设置REST传输 app.configure(express.rest()); // 配置 Socket.io 传输 app.configure(socketio()); // 在所有实时连接上,将其添加到`everybody`频道 app.on('connection', connection => app.channel('everybody').join(connection)); // 发布所有事件到`everybody`频道 app.publish(() => app.channel('everybody')); // 初始化消息服务 app.use('messages', memory({ paginate: { default: 10, max: 25 } })); // 设置错误处理程序 app.use(express.errorHandler()); // 在 3030 端口上启动服务器 const server = app.listen(3030); // 使用该服务在服务器上创建新消息 app.service('messages').create({ text: 'Hello from the server' }); server.on('listening', () => console.log('Feathers REST API started at http://localhost:3030'));
然后可以启动服务器:
node app.js
使用API
可以通过建立websocket连接来使用实时API。为此,我们需要Socket.io客户端,可以将public/index.html
更新为:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Feathers Basics</title> </head> <body> <h1>Welcome to Feathers</h1> <p>Open up the console in your browser.</p> <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js"></script> <script type="text/javascript" src="//unpkg.com/@feathersjs/client@^3.0.0/dist/feathers.js"></script> <script type="text/javascript" src="//unpkg.com/feathers-memory@^2.0.0/dist/feathers-memory.js"></script> <script src="client.js"></script> </body> </html>
然后更新public/client.js
来初始化并使用Socket来进行一些调用并监听实时事件:
/* global io */ // Create a websocket connecting to our Feathers server const socket = io('http://localhost:3030'); // Listen to new messages being created socket.on('messages created', message => console.log('Someone created a message', message) ); socket.emit('create', 'messages', { text: 'Hello from socket' }, (error, result) => { if (error) throw error socket.emit('find', 'messages', (error, messageList) => { if (error) throw error console.log('Current messages', messageList); }); });
8. 客户端使用
到目前为止,我们已经看到Feathers及其服务,事件和钩子也可以在浏览器中使用,这是一个非常独特的功能。通过在浏览器中实现与API通信的自定义服务,Feathers允许我们使用任何框架构建任何客户端应用。
这正是Feathers客户端服务所做的。为了连接到Feathers服务器,客户端创建使用REST或websocket连接来中继方法调用并允许从服务器监听事件的服务。这意味着我们可以使用客户端Feathers应用程序透明地与Feathers服务器通信,就像在本地使用一样。
下面的示例演示如何通过<script>标记使用Feathers客户端。有关使用Webpack或Browserify等模块加载程序以及加载单个模块的更多信息,请参阅客户端API文档。
实时客户端
在实时章节中,我们看到了一个如何直接使用websocket连接进行服务调用和监听事件的示例。 我们还可以使用浏览器Feathers应用和使用此连接的客户端服务。让我们将public/client.js
更新为:
// 创建 websocket 连接到我们的 Feathers 服装 const socket = io('http://localhost:3030'); // 创建 Feathers 应用 const app = feathers(); // 配置 Socket.io 客户端服务 app.configure(feathers.socketio(socket)); app.service('messages').on('created', message => { console.log('Someone created a message', message); }); async function createAndList() { await app.service('messages').create({ text: 'Hello from Feathers browser client' }); const messages = await app.service('messages').find(); console.log('Messages', messages); } createAndList();
实时客户端
还可以使用不同的Ajax库(如jQuery或Axios)创建基于REST进行通信的服务。在本示例中,我们将使用fetch,因为它是现代浏览器所内置的。
由于需要进行跨域请求,所以首先必须在服务器上启用跨源资源共享(CORS)。将app.js
更新为:
const feathers = require('@feathersjs/feathers'); const express = require('@feathersjs/express'); const socketio = require('@feathersjs/socketio'); const memory = require('feathers-memory'); // 创建应用,其会一个 Express和Feathers const app = express(feathers()); // 启动 CORS app.use(function(req, res, next) { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); next(); }); // 为REST服务启用JSON正文解析 app.use(express.json()) // 为REST服务启用URL编码的正文解析 app.use(express.urlencoded({ extended: true })); // 使用Express设置REST传输 app.configure(express.rest()); // 配置 Socket.io 传输 app.configure(socketio()); // 在所有实时连接上,将其添加到`everybody`频道 app.on('connection', connection => app.channel('everybody').join(connection)); // 发布所有事件到`everybody`频道 app.publish(() => app.channel('everybody')); // 初始化消息服务 app.use('messages', memory({ paginate: { default: 10, max: 25 } })); // 设置错误处理程序 app.use(express.errorHandler()); // 在 3030 端口上启动服务器 const server = app.listen(3030); // 使用该服务在服务器上创建新消息 app.service('messages').create({ text: 'Hello from the server' }); server.on('listening', () => console.log('Feathers REST API started at http://localhost:3030'));
将public/client.js
更新为:
// 创建 Feathers 应用 const app = feathers(); // 初始化 REST 连接 const rest = feathers.rest('http://localhost:3030'); // 使用 'window.fetch' 配置 REST 客户端 app.configure(rest.fetch(window.fetch)); app.service('messages').on('created', message => { console.log('Created a new message locally', message); }); async function createAndList() { await app.service('messages').create({ text: 'Hello from Feathers browser client' }); const messages = await app.service('messages').find(); console.log('Messages', messages); } createAndList();
9. 生成器(CLI)
到目前为止,我们都是在一个文件中手工编写代码,以便更好地了解Feathers的工作原理。Feathers CLI允许我们使用推荐的结构初始化新的Feathers应用。它还可以帮助我们:
- 配置验证
- 生成数据库支持的服务
- 设置数据库连接
- 生成钩子(带测试)
- 添加Express中间件
在本章中,将介绍如何安装CLI以及生成器构建服务器应用程序的常用模式。用户可以聊天应用指南中进一步了解CLI使用。
使用CLI需要全局安装:
npm install @feathersjs/cli -g
安装成功后,就可以命令行中使用feathers
命令,可以像下面这样检查:
需要使用3.8.2
及以上版本
配置函数
生成的应用中最常使用的模式是配置函数,函数可以得到Feathersapp对象并可以使用使用它,例如,注册服务。然后将这些函数传递给app.configure。
我们来看下基本的数据库示例:
const feathers = require('@feathersjs/feathers'); const memory = require('feathers-memory'); const app = feathers(); app.use('messages', memory({ paginate: { default: 10, max: 25 } }));
其可以使用配置函数像下面这样拆分:
const feathers = require('@feathersjs/feathers'); const memory = require('feathers-memory'); const configureMessages = function(app) { app.use('messages', memory({ paginate: { default: 10, max: 25 } })); }; const app = feathers(); app.configure(configureMessages);
现在我们可以将该函数移动到一个单独的文件中,如messages.service.js
,并将其设置为该文件的模块默认导出:
const memory = require('feathers-memory'); module.exports = function(app) { app.use('messages', memory({ paginate: { default: 10, max: 25 } })); };
然后以app.js
中导入:
const feathers = require('@feathersjs/feathers'); const configureMessages = require('./messages.service.js'); const app = feathers(); app.configure(configureMessages);
这是生成器将事物拆分为单独文件的最常见模式,并且任何使用app
对象的文档示例都可以在配置函数中使用。你可以创建自己的文件,导出配置功能,并在app.js
中require
和app.configure
它们。
钓子函数
在前面对钩子的介绍中,我们看到了如何创建一个包装器函数,该函数允许使用setTimestamp
示例自定义钩子的选项:
const setTimestamp = name => { return async context => { context.data[name] = new Date(); return context; } } app.service('messages').hooks({ before: { create: setTimestamp('createdAt'), update: setTimestamp('updatedAt') } });
这也是钩子生成器使用的模式,但在它自己的文件中,如hooks/set-timestamp.js
。其可能如下所示:
module.exports = ({ name }) => { return async context => { context.data[name] = new Date(); return context; } }
现在,可以像下面这样使用钩子:
const setTimestamp = require('./hooks/set-timestamp.js'); app.service('messages').hooks({ before: { create: setTimestamp({ name: 'createdAt' }), update: setTimestamp({ name: 'updatedAt' }) } });