【问题标题】:Express.js server with functional programming (pure routes)具有函数式编程的 Express.js 服务器(纯路由)
【发布时间】:2017-02-17 17:39:14
【问题描述】:

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

为了访问数据库和我知道的东西,我可以使用美妙的 Future monad 来保持纯净,但是路由渲染本身呢?

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

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

使用Future monad,我可以处理错误和成功案例,但在此之后成功案例就没有太多粒度了。

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

【问题讨论】:

  • 您应该提供一些您尝试使用的路线的代码示例——也许也可以展示您尝试过的东西?
  • 一些简单的东西,例如显示购物车中所有商品的路线。鉴于调用res.renderres.redirect 甚至为res.local 赋值都是不纯的事情,我想看看如何以纯粹的方式实现它。我如何创建一个纯函数来处理这些情况?
  • @MarceloLazaroni, here 概述了我为使我的路线更具功能性所做的努力。

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


【解决方案1】:

简短回答:否 - 不可能。

说明: 在函数式编程的上下文中,我们有一个数据流 - 程序获取一些输入数据,对其进行转换并返回输出数据

如果是服务器,我们有两个数据流。首先是当您启动服务器时。在此流程中,您可能希望从外部世界读取配置文件或命令行参数,以获取端口、主机、数据库字符串等内容。这是一个副作用,因此我们通常会将其放在 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
};

在上面的sn-p中,一切都是纯净的。只有不纯的东西是 .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 更高级别的抽象。所有的副作用都被限制在 observable 中,然后被分派到一个框架并执行,使所有(我的字面意思是 100%)你的组件都是纯的。

我尝试按照您使用 Hapi.js+Folktale.js+Ramda 的方式创建服务器。我的想法最初起源于 Cycle.js。然而,我取得了轻微的成功。有些部分代码真的很尴尬,而且限制太多,无法编写。之后,我放弃了纯路线。但是,我的其余代码是纯粹的并且可读性很强。

最后,函数式编程是关于部分编码而不是整体编码。您或我正在尝试做的是从整体上进行函数式编程。 至少在 JavaScript 中这会让人觉得有点尴尬。

【讨论】:

    【解决方案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 库中获得它。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-07-14
      • 1970-01-01
      • 1970-01-01
      • 2018-01-26
      • 2020-09-10
      • 2017-02-22
      • 2011-08-14
      • 1970-01-01
      相关资源
      最近更新 更多