Feathers 指南 - 基本使用

 2019年06月08日    41     声明


本指南涵盖了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' })
  }
});