我的目标是能够为Express.js服务器编写纯路由。这甚至可能吗?
为了访问数据库和我知道的东西,我可以使用神话般的Future
monad保持纯净,但路线渲染本身怎么样?
我发现的最大困难之一是路线可能以不同的方式结束,例如:
使用Future
monad,我可以处理错误和成功案例,但在此之后,成功案例的粒度要小得多。
有没有办法为Express.js编写纯粹且完全可测试的路由?
答案 0 :(得分:6)
简短回答:不 - 这是不可能的。
说明: 在函数式编程的上下文中,我们有一个数据流 - 程序获取一些输入数据,转换并返回输出数据。
如果是服务器,我们有两个数据流。首先是当你启动服务器时。在这个流程中,您可能希望从外部世界读取配置文件或命令行参数,例如端口,主机,数据库字符串等。这是副作用,因此我们通常将它放在Future中。例如,
readJson(process.argv[2]) // Read configuration file
.chain(app) // Get app instance (routes, middlewares, etc.)
.chain(start) // Start server
.run().promise()
.then((server) => info(`Server running at: ${server.info.uri}`))
.catch(error);
这是典型的index.js文件,包含所有副作用(读取配置)。现在让我们转向第二个数据流。
这个数据流有点难以想象。第一个数据流的输出/副作用是服务器侦听某个端口以进行外部连接。 现在想象一下,每个请求作为一个独立的数据流来到这个服务器。
就像 index.js 是用于处理所有副作用的文件一样,你的路由文件或路由处理函数是为了处理副作用,即应该回复结果请求这个路线处理者。通常,接近纯粹的功能路线如下:
function handler(request, reply) {
compose(serveFile(reply), fileToServe)(request)
.orElse((err) => err.code === 'ENOENT' ? reply404(reply) : reply500(reply))
.run(); // .run() is the side effect
}
return {
method: 'GET',
path: '/employer/{files*}',
handler
};
在上面的片段中,一切都是纯粹的。只有不纯的东西是 .run()方法(我正在使用Hapi.js和Folktale.js任务)。
与Angular或React等前端框架相同的想法。这些框架中的组件应包含所有影响/杂质。像路由处理程序一样,这些组件是应该发生副作用的端点。您的模型/服务应该没有杂质。
话虽如此,如果你仍然希望让你的路线完全纯净,那么就有了希望。你最本想做的是 - 更高层次的抽象:
如前所述,对于前端框架,您的UI组件会产生副作用。但是有一些新的框架超越了这种抽象。 Cycle.js是我所知道的一个框架。它应用比Angular或React更高级别的抽象。所有副作用都被观察到,然后被分派到一个框架并执行,使所有(我的字面意思是100%)你的组件纯净。
我尝试使用Hapi.js + Folktale.js + Ramda创建服务器。我的想法最初追溯到Cycle.js的根源。但是,我取得了轻微的成功。有些部分代码非常难以编写,而且限制性太强。之后,我放弃了纯粹的路线。但是,我的其余代码都是纯粹的,而且非常易读。
最后,功能编程是关于部分编码而不是整体编码。你或我想要做的是整个函数式编程。 至少在JavaScript中会觉得有点尴尬。
答案 1 :(得分:2)
我也不认为您可以使用Express.js做到这一点,但我可以建议一种替代方法。
Web服务器可以描述为一种功能,该功能采用Request
并提供Response
。但是,如果检查Express.js中发生了什么,您实际上会看到签名实际上是:
(IncomingMessage, ServerResponse) -> ()
例如:
const express = require('express')
const handler = (req, res, next) =>
doSomethingAsync(req.body).then(res.json).catch(next)
express()
.get('/', handler)
.listen(3000, console.error)
尽管我们更喜欢以下内容:
Request -> Promise Response
Paperplane是一种轻量级的NodeJS Web服务器框架,可与上述签名类型一起使用。这个想法是使用纯函数来转换您想要发送的响应。关于路由,它具有两个允许进行清晰声明的功能:
routes :: { k: (Request -> Promise Response) } -> (Request -> Response)
和
methods :: { k: (Request -> Promise Response) } -> (Request -> Promise Response)
在Paperplane's API docs中查看以下简单示例:
const http = require('http')
const { mount, routes } = require('paperplane')
const { fetchUser, fetchUsers, updateUser } = require('./lib/users')
const app = routes({
'/users': methods({
GET: fetchUsers
}),
'/users/:id': methods({
GET: fetchUser,
PUT: updateUser
})
})
http.createServer(mount({ app })).listen(3000)
它还提供其他功能来解决您遇到的困难。
此外,它还支持代数数据类型(ADT),您可以从漂亮的Crocks库中获取。