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

Mar*_*oni 4 javascript functional-programming functional-testing node.js express

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

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

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

  • 重定向
  • 模板渲染
  • 错误返回
  • 杰森回归
  • 文件返回

使用Futuremonad,我可以处理错误和成功案例,但之后的成功案例没有更多的粒度.

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

Har*_*til 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);
Run Code Online (Sandbox Code Playgroud)

这是典型的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 Code Online (Sandbox Code Playgroud)

在上面的片段中,一切都是纯粹的.只有不纯的东西是.run()方法(我正在使用Hapi.js和Folktale.js任务).

与Angular或React等前端框架相同的想法也是如此.这些框架中的组件应包含所有影响/杂质.像路由处理程序一样,这些组件是应该发生副作用的端点.您的模型/服务应该没有杂质.

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

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

对于前端框架,如前所述,您的UI组件将具有副作用.但是有一些新的框架超越了这种抽象.Cycle.js是我所知道的一个框架.它应用比Angular或React更高级别的抽象.所有的副作用都被观察到了,然后被分派到一个框架并执行所有(我的字面意思是100%)你的组件纯粹.

我试图使用Hapi.js + Folktale.js + Ramda创建服务器.我的想法最初追溯到Cycle.js.但是,我取得了轻微的成功.有些部分代码非常难以编写,而且限制性太强.之后,我放弃了纯粹的路线.但是,我的其余代码是纯粹的,并且非常易读.

最后,功能编程是关于部分编码而不是整体编码.你或我想要做的是整个函数式编程.至少在JavaScript中,这会让人觉得有点尴尬.


Yur*_*lho 6

我也不认为你可以用 Express.js 做到这一点,但我可以建议一个替代方案。

Web 服务器可以描述为一个接受Request并提供 的函数Response。但是,如果您检查 Express.js 中发生的情况,您实际上会看到签名实际上是:

(IncomingMessage, ServerResponse) -> ()
Run Code Online (Sandbox Code Playgroud)

例如:

const express = require('express')

const handler = (req, res, next) =>
  doSomethingAsync(req.body).then(res.json).catch(next)

express()
  .get('/', handler)
  .listen(3000, console.error)
Run Code Online (Sandbox Code Playgroud)

虽然我们更喜欢这样的东西:

Request -> Promise Response
Run Code Online (Sandbox Code Playgroud)

Paperplane是一个轻量级 NodeJS Web 服务器框架,适用于上述类型的签名。这个想法是使用纯函数来转换您想要发送的响应。当谈到路由时,它有两个允许干净声明的函数:

routes :: { k: (Request -> Promise Response) } -> (Request -> Response)
Run Code Online (Sandbox Code Playgroud)

methods :: { k: (Request -> Promise Response) } -> (Request -> Promise Response)
Run Code Online (Sandbox Code Playgroud)

从Paperplane 的 API 文档中查看这个简单的示例:

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)
Run Code Online (Sandbox Code Playgroud)

它还提供其他功能来解决您遇到的困难。

另外,它还支持代数数据类型 (ADT),您可以从可爱的Crocks库中获取它。