本指南涵盖了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的idcontext.data-create,update和patch服务方法调用中用户发送的datacontext.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' })
}
});
