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

1,638 阅读6分钟

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)思想的一种实现

快速体验

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

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: [],
  }
}

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

feathers-hook

有了 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/feathersfeathers (default)
@feathersjs/authentication-clientfeathers.authentication
@feathersjs/rest-clientfeathers.rest
@feathersjs/socketio-clientfeathers.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