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)思想的一种实现
快速体验
首先创建一个项目文件夹:
mkdir feathersjs-demo
cd feathersjs-demo
然后安装脚手架工具并在当前文件夹初始化项目:
npm install @feathersjs/cli -g
feathers g app
接下来按照提示一步步做就行了,这里我的选择是:
-
语言用 typescript
-
包管理器用 yarn
-
API 集成 REST 和 Realtime via Socket.io
-
需要鉴权,采用用户名密码登录,实体是 user
-
测试库用 Jest
-
数据库 ORM 选择 mongoose
然后就会自动帮你创建下面的文件并下载依赖。然后运行:
npm run dev
API 接口被生成在 http://localhost:3031
,用浏览器访问会出现欢迎页。
创建服务
service 对应 RESTful 风格中的访问资源,例如 user、order 都是一种资源,而 GET /users/1
就是访问 id 为 1 的用户,POST /orders
就是创建订单。在 feathers 中可以用下面的命令创建服务:
$ 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):
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 验证,下面几行代码就搞定了,开发者不需要写额外代码:
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,可以通过命令行交互的形式自己组合:
$ 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)
非常类似:
// 普通的中间件
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 单独指定中间件:
app.use('/todos', beforeMiddleware, todoService, afterMiddleware)
例如想把 JSON 数据转成 CSV 让前端下载,可以这么写:
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 中可以使用脚手架创建中间件:
feathers g middleware
接入数据库
在 feathers 中操作数据库非常简单,因为框架都帮我们封装好了,以 mongoose 为例,只需要引入 feathers-mongoose 这个包,然后让上面的服务继承 MongooseService 即可。代码如下:
import { Service, MongooseServiceOptions } from 'feathers-mongoose'
import { Application } from '../../declarations'
export class Book extends Service {
constructor(options: Partial<MongooseServiceOptions>, app: Application) {
super(options)
}
}
你甚至都不要写增删改查方法了,默认全部生成好了,包括参数转换、分页逻辑等,开箱即用,例如直接创建一条 book 记录:
curl --location --request POST 'http://localhost:3030/books' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "feathersjs从入门到精通",
"author": "乔柯力",
"price": 36
}'
然后查询 books 列表:
curl --location --request GET 'http://localhost:3030/books'
如果你用的是 MySQL、SQL Server 等关系型数据库的话,可以用 feathers-sequelize 包,内部封装了数据层,可快速建立映射关系。
钩子函数
钩子函数是 feathers 里面一个非常重要的概念,是面向切面编程思想(AOP)的具体实现。那什么是 AOP 呢?举个例子,当你的 service 逻辑写好了,老板说要针对所有业务操作添加一个日志,然后再加一道权限控制,怎么办呢? 传统的做法是,改造每个业务方法,把日志逻辑和权限逻辑加进去,如果这样做的话,代码肯定一团糟,AOP 的思想是引导你从另一个切面来看待问题,把日志和权限控制逻辑单独抽离为函数,在需要的地方插入这些逻辑。所以 feathers 提供了前置、后置和错误钩子,用户可以把逻辑注入到里面:
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: [],
}
}
下面这样图会更直观一些:
有了 hook,service 就可以更加聚焦在其独有的业务逻辑上面,但凡可复用的逻辑都能抽离到 hook 里面,例如:
- 参数格式验证(例如validate)
- 数据预处理(例如hashPwd)
- 发送通知(例如sendEmail)
- 等等...
从本质上来讲,hooks 就是在目标方法执行前后执行的函数而已。
实时事件
所有的 service 都会注册事件监听器,当资源发生改变的时候,即:create、update、patch、remove方法被调用的时候,即触发事件。由于每一个 service 继承了 EventEmitter,所以拥有下面三个方法:
service.on(eventName, listener) // 监听事件
service.emit(eventName, data) // 触发事件
service.removeListener(eventName) // 移除事件监听
除此之外, service 还有一些通用的方法:
service.hooks(hooks) // 注册钩子函数
service.publish(event, publisher) // 发布通知
service.mixin(mixin) // 扩展 service
其中 publish 方法定义了哪些事件会通过 websocket 实时推送到客户端:
app.service('orders').publish(() => {
return [
app.channel(`group/admin`),
app.channel(`group/order-receiver`),
]
})
这个时候,如果客户端连接之后,服务器每次新产生的订单,都会通知到客户端,实时推送。
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 分组,代码如下:
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,代码如下:
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 为例,可以在组件中这么调用:
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