node.js 企业级 web 框架有很多,例如阿里的 eggjs、IBM 的 loopback、风格类似 Spring 的 nestjs 等,今天介绍另一种选择,那就是 feathersjs,虽然在国内比较小众,但它入门很简单、遵循 RESTful 规范、文档和生态比较完善、社区维护也很积极,而且它还支持实时推送,自称是:
A framework for real-time applications and REST APIs
feathersjs 是一款基于 express 的轻量级 node.js 框架,集成了以下几个重要特性:
- 严格遵循 RESTful API 设计风格
- 借助 websocket 提供实时通信功能
- 提供了多种前端技术栈的支持,例如 React, VueJS, Angular, React Native 等
- 通过 hook 横向扩展应用,是面向切片编程(AOP:Aspect Oriented Programming)思想的一种实现
快速体验
首先创建一个项目文件夹:
1 | mkdir feathersjs-demo |
然后安装脚手架工具并在当前文件夹初始化项目:
1 | npm install @feathersjs/cli -g |
接下来按照提示一步步做就行了,这里我的选择是:
- 语言用 typescript
包管理器用 yarn
API 集成 REST 和 Realtime via Socket.io
- 需要鉴权,采用用户名密码登录,实体是 user
- 测试库用 Jest
- 数据库 ORM 选择 mongoose
然后就会自动帮你创建下面的文件并下载依赖。然后运行:
1 | npm run dev |
API 接口被生成在 http://localhost:3031
,用浏览器访问会出现欢迎页。
创建服务
service 对应 RESTful 风格中的访问资源,例如 user、order 都是一种资源,而 GET /users/1
就是访问 id 为 1 的用户,POST /orders
就是创建订单。在 feathers 中可以用下面的命令创建服务:
1 | $ feathers g service |
一个基础的 service 就是 ES6 的一个类(class):
1 | class MyService { |
这些方法与 RESTful 风格之间的映射关系为:
feathers方法 | HTTP方法 | 路径 |
---|---|---|
find() | GET | /messages |
get() | GET | /messages/1 |
create() | POST | /messages |
update() | PUT | /messages/1 |
patch() | PATCH | /messages/1 |
remove() | DELETE | /messages/1 |
登录鉴权
feathers 帮助开发者封装好了一整套登录和鉴权逻辑,例如应用需要用户名密码登录和 jwt 验证,下面几行代码就搞定了,开发者不需要写额外代码:
1 | import { Application } from '@feathersjs/feathers' |
除此之外,feathers 还集成了很多第三方登录的 API,例如 Github、Google、Facebook,可以通过命令行交互的形式自己组合:
1 | $ feathers g authentication |
使用中间件
feathers 框架是建立在 express 基础之上的,从其注册 RESTful 服务的语法上看:app.use([path],service)
和 express 框架里面的 app.use([path], middleware)
非常类似:
1 | // 普通的中间件 |
还可以为 service 单独指定中间件:
1 | app.use('/todos', beforeMiddleware, todoService, afterMiddleware) |
例如想把 JSON 数据转成 CSV 让前端下载,可以这么写:
1 | const json2csv = require('json2csv') |
在 feathers 中可以使用脚手架创建中间件:
1 | feathers g middleware |
接入数据库
在 feathers 中操作数据库非常简单,因为框架都帮我们封装好了,以 mongoose 为例,只需要引入 feathers-mongoose 这个包,然后让上面的服务继承 MongooseService 即可。代码如下:
1 | import { Service, MongooseServiceOptions } from 'feathers-mongoose' |
你甚至都不要写增删改查方法了,默认全部生成好了,包括参数转换、分页逻辑等,开箱即用,封装数据层,快速建立映射关系(例如mysql、sqlserver、mongodb等),例如直接创建一条 book 记录:
1 | curl --location --request POST 'http://localhost:3030/books' \ |
然后查询 books 列表:
1 | curl --location --request GET 'http://localhost:3030/books' |
就这么简单,你如果想添加自己的逻辑,有两种方法,第一种是这样:
1 | create(data: any, params: Params) { |
即在当前类中重写 create 方法,把基类覆盖掉,如果需要用到基类的方法,再通过 super 调用。第二种方法就是接下来要讲的 hooks:
钩子函数
钩子函数是 feathers 里面一个非常重要的概念,是面向切面编程思想(AOP)的具体实现。那什么是 AOP 呢?举个例子,当你的 service 逻辑写好了,老板说要针对所有业务操作添加一个日志,然后再加一道权限控制,怎么办呢? 传统的做法是,改造每个业务方法,把日志逻辑和权限逻辑加进去,如果这样做的话,代码肯定一团糟,AOP 的思想是引导你从另一个切面来看待问题,把日志和权限控制逻辑单独抽离为函数,在需要的地方插入这些逻辑。所以 feathers 提供了前置、后置和错误钩子,用户可以把逻辑注入到里面:
1 | export default { |
下面这样图会更直观一些:
有了 hook,service 就可以更加聚焦在其独有的业务逻辑上面,但凡可复用的逻辑都能抽离到 hook 里面,例如:
- 参数格式验证(例如validate)
- 数据预处理(例如hashPwd)
- 发送通知(例如sendEmail)
- 等等…
从本质上来讲,hooks 就是在目标方法执行前后执行的函数而已。
实时事件
所有的 service 都会注册事件监听器,当资源发生改变的时候,即:create、update、patch、remove方法被调用的时候,即触发事件。由于每一个 service 继承了 EventEmitter,所以拥有下面三个方法:
1 | service.on(eventName, listener) // 监听事件 |
除此之外, service 还有一些通用的方法:
1 | service.hooks(hooks) // 注册钩子函数 |
其中 publish 方法定义了哪些事件会通过 websocket 实时推送到客户端:
1 | app.service('orders').publish(() => { |
这个时候,如果客户端连接之后,服务器每次新产生的订单,都会通知到客户端,实时推送。
1 | var socket = io('http://localhost:3030') |
feathers 提供了 channels.ts 文件让开发者可以自定义分组,每当客户端建立连接,服务端会收到一个 connection 对象,然后根据业务需要把这个 connection 加入到某个分组里面,当然这个分组也是开发者自己定义的 app.channel('xxx')
,一个典型的场景就是未登录用户全部加入 anonymous 分组,当登录之后将其从 anonymous 分组移除,然后加入 authenticated 分组,代码如下:
1 | app.on('connection', (connection: any): void => { |
实时事件是建立在 websocket 通道之上的,feathers 内部集成了 socket.io,可以建立浏览器和 web 服务器的双向通信。
客户端集成
feathers 可以集成到很多客户端框架中,包括 Vue、Angular、React、React Native,模块拆分很细,客户端可以自由搭配使用:
Feathers module | @feathersjs/client |
---|---|
@feathersjs/feathers | feathers (default) |
@feathersjs/authentication-client | feathers.authentication |
@feathersjs/rest-client | feathers.rest |
@feathersjs/socketio-client | feathers.socketio |
以 Angular 为例,如果选择使用 websocket 进行交互的话,可以创建一个全局 feathers.service.ts,代码如下:
1 | import { Injectable } from '@angular/core' |
以创建 book 为例,可以在组件中这么调用:
1 | import { Component } from '@angular/core' |
除了可以用 .create()
新增之外,还可以用:
.find()
查询列表.get()
获取详情.remove()
删除.patch()
更新
完全被框架封装好了,用户只需要选择走 RESTful 还是走 websocket,如果是前者的话,内部默认使用 axios 封装了 http 请求(也可以选择其他库),后者的话内部默认使用了 socket.io 通信。
上面初步介绍了 feathers 的核心概念,感兴趣的可以直接阅读官方文档,案例比较全,目前只有英文版。
本文示例代码地址:`git@github.com:keliq/feathersjs-demo.git`