Node.js企业级Web框架(Feathers)

node.js 企业级 web 框架有很多,例如阿里的 eggjs、IBM 的 loopback、风格类似 Spring 的 nestjs 等,今天介绍另一种选择,那就是 feathersjs,虽然在国内比较小众,但它入门很简单、遵循 RESTful 规范、文档和生态比较完善、社区维护也很积极,而且它还支持实时推送,自称是:

A framework for real-time applications and REST APIs

feathersjs

feathersjs 是一款基于 express 的轻量级 node.js 框架,集成了以下几个重要特性:

  • 严格遵循 RESTful API 设计风格
  • 借助 websocket 提供实时通信功能
  • 提供了多种前端技术栈的支持,例如 React, VueJS, Angular, React Native 等
  • 通过 hook 横向扩展应用,是面向切片编程(AOP:Aspect Oriented Programming)思想的一种实现

快速体验

首先创建一个项目文件夹:

1
2
mkdir feathersjs-demo
cd feathersjs-demo

然后安装脚手架工具并在当前文件夹初始化项目:

1
2
npm install @feathersjs/cli -g
feathers g app

接下来按照提示一步步做就行了,这里我的选择是:

  • 语言用 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
2
3
4
5
$ feathers g service
? What kind of service is it? Mongoose
? What is the name of the service? order
? Which path should the service be registered on? orders
? Does the service require authentication? Yes

一个基础的 service 就是 ES6 的一个类(class):

1
2
3
4
5
6
7
8
9
10
11
class MyService {
find(params [, callback]) {}
get(id, params [, callback]) {}
create(data, params [, callback]) {}
update(id, data, params [, callback]) {}
patch(id, data, params [, callback]) {}
remove(id, params [, callback]) {}
setup(app, path) {}
}

app.use('/my-service', new 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
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Application } from '@feathersjs/feathers'
import { AuthenticationService, JWTStrategy } from '@feathersjs/authentication'
import { LocalStrategy } from '@feathersjs/authentication-local'
import { expressOauth } from '@feathersjs/authentication-oauth'

export default (app: Application) => {
const authService = new AuthenticationService(app);

authService.register('jwt', new JWTStrategy())
authService.register('local', new LocalStrategy())

app.use('/authentication', authService)
app.configure(expressOauth())
}

除此之外,feathers 还集成了很多第三方登录的 API,例如 Github、Google、Facebook,可以通过命令行交互的形式自己组合:

1
2
3
4
5
6
7
8
$ feathers g authentication
? What authentication strategies do you want to use? (See API docs for all 180+ supported oAuth providers)
❯◯ Username + Password (Local)
◯ Auth0
◯ Google
◯ Facebook
◯ Twitter
◉ GitHub

使用中间件

feathers 框架是建立在 express 基础之上的,从其注册 RESTful 服务的语法上看:app.use([path],service)和 express 框架里面的 app.use([path], middleware) 非常类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 普通的中间件
app.use(function(req,res,next){
if(req.isAuthenticated()){
next();
} else {
next(new Error(401));
}
})

// 注册 RESTful service 对象,被 feathers 框架封装
app.use('/todos', {
async get(id) {
return { id, description: `You have to do ${name}!` }
}
})

还可以为 service 单独指定中间件:

1
app.use('/todos', beforeMiddleware, todoService, afterMiddleware)

例如想把 JSON 数据转成 CSV 让前端下载,可以这么写:

1
2
3
4
5
6
7
8
9
10
const json2csv = require('json2csv')
const fields = [ 'done', 'description' ]

app.use('/todos', todoService, function(req, res) {
const result = res.data
const data = result.data
const csv = json2csv({ data, fields })
res.type('csv')
res.end(csv)
})

在 feathers 中可以使用脚手架创建中间件:

1
feathers g middleware

接入数据库

在 feathers 中操作数据库非常简单,因为框架都帮我们封装好了,以 mongoose 为例,只需要引入 feathers-mongoose 这个包,然后让上面的服务继承 MongooseService 即可。代码如下:

1
2
3
4
5
6
7
8
import { Service, MongooseServiceOptions } from 'feathers-mongoose'
import { Application } from '../../declarations'

export class Book extends Service {
constructor(options: Partial<MongooseServiceOptions>, app: Application) {
super(options)
}
}

你甚至都不要写增删改查方法了,默认全部生成好了,包括参数转换、分页逻辑等,开箱即用,封装数据层,快速建立映射关系(例如mysql、sqlserver、mongodb等),例如直接创建一条 book 记录:

1
2
3
4
5
6
7
curl --location --request POST 'http://localhost:3030/books' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "feathersjs从入门到精通",
"author": "乔柯力",
"price": 36
}'

然后查询 books 列表:

1
curl --location --request GET 'http://localhost:3030/books'

就这么简单,你如果想添加自己的逻辑,有两种方法,第一种是这样:

1
2
3
4
create(data: any, params: Params) {
// 自己的逻辑
return super.create(data, params)
}

即在当前类中重写 create 方法,把基类覆盖掉,如果需要用到基类的方法,再通过 super 调用。第二种方法就是接下来要讲的 hooks:

钩子函数

钩子函数是 feathers 里面一个非常重要的概念,是面向切面编程思想(AOP)的具体实现。那什么是 AOP 呢?举个例子,当你的 service 逻辑写好了,老板说要针对所有业务操作添加一个日志,然后再加一道权限控制,怎么办呢? 传统的做法是,改造每个业务方法,把日志逻辑和权限逻辑加进去,如果这样做的话,代码肯定一团糟,AOP 的思想是引导你从另一个切面来看待问题,把日志和权限控制逻辑单独抽离为函数,在需要的地方插入这些逻辑。所以 feathers 提供了前置、后置和错误钩子,用户可以把逻辑注入到里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default {
before: {
all: [isLoggedIn],
find: [isAdmin],
get: [isCurrent],
create: [validate, hashPwd],
update: [],
patch: [],
remove: [],
},
after: {
all: [removePasswords],
find: [],
get: [],
create: [sendEmail],
update: [],
patch: [],
remove: [],
}
}

下面这样图会更直观一些:

feathers-hook

有了 hook,service 就可以更加聚焦在其独有的业务逻辑上面,但凡可复用的逻辑都能抽离到 hook 里面,例如:

  • 参数格式验证(例如validate)
  • 数据预处理(例如hashPwd)
  • 发送通知(例如sendEmail)
  • 等等…

从本质上来讲,hooks 就是在目标方法执行前后执行的函数而已。

实时事件

所有的 service 都会注册事件监听器,当资源发生改变的时候,即:create、update、patch、remove方法被调用的时候,即触发事件。由于每一个 service 继承了 EventEmitter,所以拥有下面三个方法:

1
2
3
service.on(eventName, listener) // 监听事件
service.emit(eventName, data) // 触发事件
service.removeListener(eventName) // 移除事件监听

除此之外, service 还有一些通用的方法:

1
2
3
service.hooks(hooks) // 注册钩子函数
service.publish(event, publisher) // 发布通知
service.mixin(mixin) // 扩展 service

其中 publish 方法定义了哪些事件会通过 websocket 实时推送到客户端:

1
2
3
4
5
6
app.service('orders').publish(() => {
return [
app.channel(`group/admin`),
app.channel(`group/order-receiver`),
]
})

这个时候,如果客户端连接之后,服务器每次新产生的订单,都会通知到客户端,实时推送。

1
2
3
4
5
6
7
var socket = io('http://localhost:3030')
socket.on('connect', () => {
console.log('连接成功')
})
socket.on('orders created', function (order) {
console.log('Got a new order!', order)
})

feathers 提供了 channels.ts 文件让开发者可以自定义分组,每当客户端建立连接,服务端会收到一个 connection 对象,然后根据业务需要把这个 connection 加入到某个分组里面,当然这个分组也是开发者自己定义的 app.channel('xxx'),一个典型的场景就是未登录用户全部加入 anonymous 分组,当登录之后将其从 anonymous 分组移除,然后加入 authenticated 分组,代码如下:

1
2
3
4
5
6
7
8
app.on('connection', (connection: any): void => {
app.channel('anonymous').join(connection)
})
app.on('login', (authResult: any, { connection }: any): void => {
if (!connection) return
app.channel('anonymous').leave(connection)
app.channel('authenticated').join(connection)
})

实时事件是建立在 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Injectable } from '@angular/core'
import * as io from 'socket.io-client'
import socketio from '@feathersjs/socketio-client'
import feathers from '@feathersjs/feathers'

@Injectable()
export class Feathers {
private _feathers
private _socket: SocketIOClient.Socket

constructor() {}

init() {
if (this._feathers) return
this._socket = io('http://localhost:3050')
this._feathers = feathers().configure(socketio(this._socket))
}

public service(name: string) {
return this._feathers.service(name)
}
}

以创建 book 为例,可以在组件中这么调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component } from '@angular/core'
import { Feathers } from './services/feathers.service'

export class AppComponent {
constructor(private feathers: Feathers) {}

createBook() {
this.feathers
.service('books')
.create({
name: 'feathesjs从入门到精通',
author: '乔柯力'
})
.then((res) => {
console.log('书籍创建结果', res)
})
}
}

除了可以用 .create() 新增之外,还可以用:

  • .find() 查询列表
  • .get() 获取详情
  • .remove() 删除
  • .patch() 更新

完全被框架封装好了,用户只需要选择走 RESTful 还是走 websocket,如果是前者的话,内部默认使用 axios 封装了 http 请求(也可以选择其他库),后者的话内部默认使用了 socket.io 通信。

上面初步介绍了 feathers 的核心概念,感兴趣的可以直接阅读官方文档,案例比较全,目前只有英文版。

本文示例代码地址:`git@github.com:keliq/feathersjs-demo.git`