写一个 react hooks + koa 风格的 web 框架

1,555 阅读4分钟

目录

前言

最近一直打算写一个 web 框架自用,但是一直头疼于插件之间的互相依赖问题,所以最近暂时停止了开发。

但是最近从 react 的学习中,突然冒出了一些好玩的想法,那就是能不能以 react hooks 风格写 nodejs 代码呢?

一、设计 api 风格

主应用代码设想

import { Server } from 'http'
import { isNullOrUndefined } from 'util'

import { use, route, redirect, urlFor, listen } from '@zhengxs/koa-hooks'

import { useSession } from './session'

route('product.detail', '/api/product/:id', (ctx) => {
  ctx.status = 200
  ctx.type = 'application/json'
  ctx.body = ctx.params
})

route('/api/user/info', (ctx) => {
  const sess = useSession()
  const userId = sess.userId

  // 用户未登陆就重定向到登陆页
  if (isNullOrUndefined(userId)) {
    return redirect(urlFor('login'))
  }

  ctx.status = 200
  ctx.type = 'application/json'
  ctx.body = {
    userId: userId,
    nickname: '张三',
  }
})

route('POST', '/login', (ctx) => {
  const sess = useSession()

  // 写入用户 id
  sess.userId = 1

  ctx.status = 200
  ctx.type = 'application/json'
  ctx.body = { code: 200, message: 'ok' }
})

route('login', '/login', (ctx) => {
  ctx.status = 200
  ctx.type = 'text/plan'
  ctx.body = 'This is login page'
})

route('/view', (ctx) => {
  const sess = useSession()

  sess.view = sess.view || 0

  ctx.status = 200
  ctx.type = 'application/json'
  ctx.body = { view: sess.view++ }
})

use((ctx) => {
  ctx.body = `hello,world`
})

listen(8080, function onReady(this: Server) {
  console.log('Liston http://127.0.0.1:8080')
})

主应用代码看起来并无任何特别之初,仅仅取消了 app 创建这一步,但是别急,我们看看自定义 hooks 的设计又有什么特别之处。

自定义 hooks 设计

举个服务端消息处理的🌰,服务端接收到用户私信,并转发给接收方的流程,大概是这样的

代码逻辑大概是这样的

async saveMsgToDB(msgData, next){
    // 获取当前请求上下文
    const db = useContext('db')
    // 数据库操作
    
    // 将处理好的 message 流转给下一个任务
    return next(msg)
}

// 点对点消息发送
const sendToUser = series(
  // 保存数据
  saveMsgToDB,
  // 消息通知
  parallel(
    // 发送消息到客户端
    sendMsgWithSocketIo,
    // 将消息转成通知格式数据,并且推送给微信
    series(msgToNotice, pushNoticeToWeChat)
    // 将消息转成通知格式数据,并且推送给钉钉
    series(msgToNotice, pushNoticeToDingTalk)
  )
)

从上面的的函数设计中我们可以看出,自定义 hooks 的特别之处在于:

  1. 每一个都是纯函数,不再受到框架的任何约束,如果需要使用上下文,那就显示的使用 useContext
  2. 因为是纯函数,所以我们可以在外部再包一层,避免冗余代码的出现,比如 通知 可以是独立的函数,也可以被包一层,变成一个 task 函数。

比如下面这些函数,就可以做成通用函数,只要约定好数据格式就行,因为需要上下文什么的,可以内部直接使用 useContext 获取。

// 推送给钉钉
pushNoticeToWeChat

// 推送给钉钉
pushNoticeToDingTalk

// 发送消息到客户端
sendMsgWithSocketIo

二、实现功能

难点:如何解决请求处理过程中的上下文一致的问题

在这场说干就干的行动中,一开始就出现碰壁的情况

那就是 web 服务是无状态的,而且会接收到多个客户端请求,中间还夹杂着许多的异步操作,光是如何保证每个被执行函数的上下文一致性就非常麻烦。

在多次使用 hack 手段无果的情况下,我遇到了 Async Hooks 这个 Node 8 时增加的特性。

这个模块的意义在于我们可以在函数中拿到 node 为这次函数运行所生成的唯一性 id,也就是每调用一次 id++,而且可以保证中间过程中,后续执行的函数,拿到的入口函数的 id 值不变

我们可以看到后续函数拿到的 triggerAsyncId 都是 6,入口每次被重新调用 triggerAsyncId 都会累加,但是当前调用的堆栈中,后续函数拿到的 triggerAsyncId 是保持不变的。

那么我们就可以基于这个做上下文的切换,核心代码:

每次调用 requestListener 方法都会生成一个新的 triggerAsyncId,但是中间执行到的函数,获取到的 triggerAsyncId 是一致的,这个也就解决了当出现多个上下文的时候,上下文切换的问题

最后

其实在 python 中,像 flask 这种框架上下文就是可以全局获取的,这样的好处就是不会特别受制于框架的约束。

坏处也是在于太灵活了,就像我现在还是写着 vue 的代码, 因为 react 太灵活了,能写东西,每次看代码,总感觉哪里还可以再改改。

目前第一版的代码已经发布到 github 和 npm 上了,第一次接触 Async Hooks 还无法掌控其中遇到的问题,不过作为一个学习的思路还是比较有意思的。