Farrow 介绍:类型友好的函数式风格 Node.js Web 框架

4,727 阅读7分钟

今天给大家介绍一个我最近开发的新项目——Farrow。一款类型友好的函数式风格 Node.js Web 服务框架。

项目动机

在当下的 Node.js 开源生态里,已经有 Express.js, Koa, EggJS, Hapi, Restify, Fastify, NestJS等或许数不胜数的 Web 服务框架,我们还需要另一个吗?

答案可能因人而异,在此分享一下我的个人看法。

Node.js 里大部分流行的 Web 服务框架,是在 TypeScript 真正流行之前,用 JavaScript 的视角下所开发。

它们充分发挥了 JavaScript 动态类型的表达能力,这毋庸置疑。

如果我们把——充分发挥 Static Type-System 的能力,在 Compile-Time 去尽可能捕获潜在的问题——也列入 Web 框架的考量范畴,那么用 TypeScript 重新开发一个 Web 服务框架,或许是一个值得尝试的事情。

Farrow 就是我在这个方向上的一个产出。

TypeScript 视角下的中间件设计

Rollup 和 Svelte 的作者 Rich Harris 最近分享了他对 Next-gen Node HTTP APIs 的看法*[0]*, 我深受启发。

事情源于 Wes 的一条推特投票:

img

接近 70% 的开发者,选择了 expressjs 风格的中间件函数设计。一个压倒性的选择。

而 Rich Harris 所选的,只有 14.5% 的支持率。

在那条 Gist 里,Rich Harris 解释了他不喜欢第一个选项的原因。大致是:

1、总是需要丑陋的传递 res 参数

2、组合中间件的时候,常常要对 res 做 monkey-patching

他给出了他觉得更好的另一种设计。

img

简单地说,就是取消 res 参数,只保留 req 参数,通过 return response 表达响应结果,通过 return void/undefined 表达调用下一个中间件 next()。

另一位开发者 Oliver Ash 在推特上,也指出了 expressjs 的中间件设计的一个不足之处——没有充分利用 Compile-Time 的排查能力*[1]*。

imgtwitter.com/OliverJAsh/…

简单地说,当 response 是 middleware 的返回值时,TypeScript 可以从类型上约束,每个请求都必须有个返回值,而不必担心遗漏。

fp-ts 的作者 Giulio Canti 对此也有他的尝试——hyper-ts[3]。hyper-ts 受到 purescript 的 hyper 项目的启发,利用 TypeScript 的 Type-System,可以规避一些常见的错误,比如:

img

这些线索都指向一个结论:或许可以用函数式风格(immutable way)去设计 HTTP 中间件 API。

Farrow-Pipeline:类型友好的中间件函数设计

Farrow 的中间件函数受到了 Koa 中间件的启发,但又有所不同。

img

从上图中,我们可以了解到以下信息:

1)Response 不在中间件函数的参数中,而是来自 farrow-http 模块导出的朴素函数。

2)response 是中间件函数的返回值,可以在 Compile-Time 得到检查。

如果没有返回值,会像下面这样:

img

如果返回错误的值,会像下面这样:

img

必须通过 Response.{method}() 的方式,对客户端做出响应:

img

Response 的 API 设计,支持 Method Chaining,可以像下面那样调用:

img

如上,设置 response status,设置 response headers,设置 response cookie,设置 response content,都可以优雅地编写到一起。

那么,在 Farrow 中,多个中间件之间又是如何协作的呢?

比如,在上游中间件中,给下游中间件传递新的 request,像下面这样:

img

Farrow 中间件函数的第二个参数是 next 函数,跟 expressjs/koajs 的中间件函数不同,Farrow 中间件函数,既有参数,又有返回值。

其参数为可选的 request,其返回值为 response。

当 next() 调用没有传递参数时,下游中间件拿到的 request 跟上游中间件的一样。

当调用 next 时传递新的 request,下游中间件将拿到新的 request 对象。

通过这个很自然的参数传递机制,我们不必修改当前的 request。甚至,Farrow 将 request 的类型,设置为 readonly。

Farrow 鼓励保持 request/response 的 immutable 状态。

同理,我们也可以在上游中间件中,过滤或者操作下游中间件返回的 response,像下面这样做:

img

Response 对象提供了 merge 方法,合以方便地合并多个 response 里的 status, headers, cookies, content 等组成部分。

Farrow 还提供了支持分形的 Router 设计,可以帮助我们细粒度地分割业务逻辑到不同的模块,并能有机地组合它们。

img

Farrow-Schema:类型安全的 Routing 设计

Farrow 实现了一个强大而又灵活的 Schema-Based Validation,可以类型安全的方式,匹配特定的 request 对象。

基本用法如下所示:

img

http.match 方法接受参数为 { pathname, method, query, params, headers, cookies } 对象,构成一个 Request Schema。

其中 schema.pathname 采用了 expressjs-like 的 path-to-regexp 写法。

Farrow 会根据 Request Schema,通过 type infer 提取出匹配的 request 对象的精确类型,并且在 runtime 里做 validate,保证 request 对象的类型安全。

此外,Farrow 还基于 TypeScript V4.1 的 Template Literal Types 特性,实现了类型安全的 route matching。

img

在 path 中通过 <key: type> 的格式,我们可以将 { pathname, params, query } 合并起来,只编写一个 path,通过 type infer 从 path 中提取出对应的 type。

更复杂一点的案例如下所示:

img

key:type 出现在 ? 之前时,它被当作 params 的一部分,此时顺序是敏感的。

key:type 出现在 ? 之后时,它被当作 query 的一部分,此时顺序则不敏感。

想要更了解 Farrow 的 Router-Url-Schema,可以查看它的文档[3]

Farrow-Hooks 机制

在 Farrow 里,另一个值得一提的特性是,我们参考了 React-Hooks 的设计思路,提供了 Farrow-Hooks 机制,用以整合服务器的其它部分,如 logger, database-connection 等。

相比 koajs 通过 mutable ctx 参数去挂载拓展的方法,在 Farrow 里,context 不是参数,而是 Hooks。

就像 React-Hooks 的 useState,可以看作是把 class-component 里共享的 this.state 做细粒度的切割。

Farrow 里的 Context.use 将共享的 ctx,也做如此切割。如下所示:

img

我们定义了一个 User 类型,以类似 React.createContext 的方式,创建了一个 Farrow Context,并提供默认值(此处是 null)。

通过 UserContext.use() 这个内置的 hooks,可以访问到对应的 user context。在每一次 request -> response 的过程中,所有 Context 都是新的、独立的。

我们不再具有一个大的 ctx,而是多个小的 Context 单元。

我们可以基于 Context.use() 封装自定义的 hooks,如上图的 useUser。

动态更新 Context 的 value,则像下面这样做。

img

实现一个 Provider Middleware,可以动态的、异步的更新 context value,让下游中间件消费。其中的关系类似于 React Context 里的 Provider 和 Consumer。 上游中间件是 Context Provider,下游中间件是 Context Consumer。

然后像使用 react-hooks 一样使用 farrow-hooks,如下:

img

通过 Context Hooks 机制,我们的中间件函数类型始终是简单和稳定的,它只关注 request -> response 的处理,其它附加事物,可以按需通过 Hooks 去提供。

Farrow-React:内置的组件化 SSR

Farrow 提供了一个官方 SSR 库——farrow-react。当然,你也可以根据 Response.html 或 Response.stream 等方法,去构建自己的 SSR 库。

img

如上所示,farrow-react 提供了一个 Farrow-Hooks,通过 useReactView 我们获取到了一个 ReactView 对象,通过它的 render 方法,可以将 JSX 渲染成 html 并通过 farrow-http 发送给浏览器。

farrow-react 提供了 Link 组件,可以帮助我们处理 prefix 相关的自动补全。想要了解更多,可以查看 farrow 官方文档。

总结

至此,我们大致介绍了 Farrow 的几个核心特征。

Farrow 的目标并不止步于此,未来我们将构建更多的 farrow 生态。比如:

1)farrow-restapi 和 farrow-restapi-client,支持在 client project 中复用 server project 的 schema/type,实现 server/client 两个端数据传递上的 type safe 功能。

2)farrow-graphql 和 farrow-graphql-client,类似于 farrow-restapi 但支持通过 graphql 去实现。

3)farrow-server-component,支持 React Server Component。

要做的工作还很多,如果你也感兴趣,欢迎一起来为 Farrow 项目做贡献*[4]*。


[0] Next-gen Node HTTP APIs

twitter.com/Rich_Harris…

[1] Oliver Ash

twitter.com/OliverJAsh/…

[2] hyper-ts: Type safe middleware architecture for HTTP servers

github.com/gcanti/hype…

[3] Farrow Router-Url-Schema

github.com/Lucifier129…

[4] Farrow: A type friendly web framework for node.js written by TypeScript

github.com/Lucifier129…