Feathers 指南 - 基本使用。


本指南涵盖了Feathers应用所有的基础知识和核心概念。

  1. 配置
  2. 入门
  3. 服务
  4. 钩子
  5. REST APIs
  6. 数据库
  7. 实时 APIs
  8. 客户端
  9. 生成器(CLI)

1. 配置

在本节中,将介绍学习Feathers所需的工具和初步知识。

先决条件

Feathers及其大多数插件工作于 NodeJSv6.0.0及以上。而在本指南将使用仅适用于Node v8.0.0及更高版本的语法。在MacOS和其他类Unix系统上,Node Version Manager是快速安装最新版NodeJS并使其保持最新的好方法。

成功安装后,就可以在控制台使用nodenpm命令,其使用方式类似如下:

$ 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。强烈建议熟悉Promisesasync/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,当修改数据(createupdatepatchremove)的服务方法返回时,它会发送带有新数据的事件。可以使用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钩子的地方。钩子是可插入的中间件功能,可以注册在服务方法beforeaftererror上。你可以注册单个钩子函数或创建钩子函数链,以创建复杂的工作流程。

就像服务本身一样,钩子与传输无关。它们通常也是服务不可知的,这意味着它们可以与任何服务一起使用。这一模式使你应用程序逻辑保持灵活、可组合、并且更容易跟踪和调试。

钩子通常用于处理诸如验证、授权、日志记录、填充相关实体、发送通知等。

备注:钩子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, aftererror)

可写属性包括:

  • context.params - 服务方法调用的params。对于外部调用而言,params通常包含:
    • context.params.query - 服务调用的查询(如,REST的查询字符串)
    • context.params.provider - 己完调用传输方式的名称。一般是restsocketioprimus。内部调用时为undefined
  • context.id - 服务方法调用get, remove, updatepatchid
  • context.data - create, updatepatch服务方法调用中用户发送的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);

各个钩子的执行顺序如下图所示:

Feathers 钩子执行顺序


验证数据

如果一个钩子发生错误,其后所有的钩子将会跳过执行,错误会返回给用户。 这使before钩子成为通过抛出无效数据错误来验证传入数据的好地方。我们可以抛出一个普通的JavaScript错误或者Feathers错误,Feathers错误会有一些额外的功能(比如为REST调用返回正确的错误代码)。

@feathersjs/errors是一个独立的模块,你需要像下面这样安装它:

npm install @feathersjs/errors --save

我们只需要用于createupdatepatch的钩子,因为仅这些服务方法是允许用户提交数据的:

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服务方法和GETPOSTPATCHDELETE等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.jsonexpress.urlencodedexpress.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.queryfind方法调用中查询数据的常用方法。你可以在查询语法API中找到完整列表。

启用分页后,find方法将返回具有以下属性的对象:

  • data - 当前数据列表
  • limit - 每页大小
  • skip - 要跳过的条数
  • total - 此查询的总条数

以下示例自动创建100条消息并进行一些查询。可以在app.jspublic/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服务会在createupdatepatchremove服务方法返回时,自动发送createdupdatedpatchedremoved事件。实时意味着这些事件也会发送到所连接的客户端,以便他们可以做出相应的反应,例如, 更新UI等。

要实现与客户的实时通信,我们需要一种支持双向通信的传输。在Feathers中,这些是Socket.ioPrimus传输,它们都使用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库(如jQueryAxios)创建基于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.jsrequireapp.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' })
  }
});