具有函数式编程的Express.js服务器(纯路由)

时间:2017-02-17 17:39:14

标签: javascript node.js express functional-programming functional-testing

我的目标是能够为Express.js服务器编写纯路由。这甚至可能吗?

为了访问数据库和我知道的东西,我可以使用神话般的Future monad保持纯净,但路线渲染本身怎么样?

我发现的最大困难之一是路线可能以不同的方式结束,例如:

  • 重定向
  • 模板渲染
  • 错误返回
  • json return
  • 文件返回

使用Future monad,我可以处理错误和成功案例,但在此之后,成功案例的粒度要小得多。

有没有办法为Express.js编写纯粹且完全可测试的路由?

2 个答案:

答案 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等前端框架相同的想法。这些框架中的组件应包含所有影响/杂质。像路由处理程序一样,这些组件是应该发生副作用的端点。您的模型/服务应该没有杂质。

话虽如此,如果你仍然希望让你的路线完全纯净,那么就有了希望。你最本想做的是 - 更高层次的抽象:

  1. 如果 Express.js 是一个框架,那么您应该在它上面构建自己的抽象。
  2. 您通常会为所有路由编写自己的通用路由处理程序,并且您将公开自己的API来注册路由。
  3. 您的路线处理程序将返回future / task / observable或包含等待释放的所有副作用的任何其他monad。
  4. 您的抽象路线处理程序将简单地调用 .fork() .run()
  5. 这意味着当您对路线处理程序进行单元测试时,不会发生任何副作用。它们只是包含在一些Async Monad中。
  6. 如前所述,对于前端框架,您的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库中获取。