发布于 2026-01-06 0 阅读
0

适用于 NodeJS 的可组合 HTTP 客户端……API,API……无处不在的组合实用性,圆满完成,成就卓越。结论

适用于 NodeJS 的可组合 HTTP 客户端

API,API,到处都是。

作品

实用性

圆满回归

大干一场

结论

所以我为NodeJS编写了这个HTTP客户端:

var compose = require('request-compose')
Enter fullscreen mode Exit fullscreen mode

顺便说一句,如果你想直接跳过前面的环节,立刻沉浸在FP的精彩内容中——那就别犹豫了。

它是如何使用的?

var {res, body} = await compose.client({
  url: 'https://api.github.com/users/simov',
  headers: {
    'user-agent': 'request-compose'
  }
})
Enter fullscreen mode Exit fullscreen mode

哇!真的吗?!
又一个NodeJS的HTTP客户端!
太棒了!!!

API,API,到处都是。

作为最终用户,如果我想对其他人的模块进行修复、更改或添加某些内容,我该怎么办?我有哪些选择?

  • 在 GitHub 上创建一个 issue 并提出请求
  • 我自己实现并提交一个拉取请求
  • 寻找满足我需求的替代模块
  • 重复

原因在于模块作者会提供一份API文档,明确规定你能做什么和不能做什么。你实际上被限制在文档的范围内。此外,作者还会严格保护项目的范围,防止无关内容渗入。

但是,如果我们拥有更强大的原语,允许我们深入底层,优雅地构建我们自己的功能,那会怎样呢?完全为我们自己服务,彻底绕过其他方案中存在的 API 和作用域瓶颈。

作品

幸运的是,有一种叫做函数式组合的原始技术:

在计算机科学中,函数组合(不要与对象组合混淆)是一种将简单函数组合起来构建更复杂函数的行为或机制。与数学中常见的函数组合类似,每个函数的结果都作为下一个函数的参数传递,而最后一个函数的结果就是整个函数的最终结果。

事实上,request-compose所公开的正是这一点

var compose = (...fns) => (args) =>
  fns.reduce((p, f) => p.then(f), Promise.resolve(args))
Enter fullscreen mode Exit fullscreen mode

request-compose的核心甚至不是一个客户端,而是一种函数式编程模式,一种理念,一个简单的单行代码,可以帮助你组合自己的东西。

有了它,你可以组合任何函数,无论异步还是异步:

var sum = compose(
  (x) => x + 1,
  (x) => new Promise((resolve) => setTimeout(() => resolve(x + 2), 1000)),
  (x) => x + 3,
  async (x) => (await x) + 4
)
await sum(5) // 15 (after one second)
Enter fullscreen mode Exit fullscreen mode

或者更确切地说——编写你自己的HTTP客户端:

var compose = require('request-compose')
var https = require('https')

var request = compose(
  (options) => {
    options.headers = options.headers || {}
    options.headers['user-agent'] = 'request-compose'
    return options
  },
  (options) => new Promise((resolve, reject) => {
    https.request(options)
      .on('response', resolve)
      .on('error', reject)
      .end()
  }),
  (res) => new Promise((resolve, reject) => {
    var body = ''
    res
      .on('data', (chunk) => body += chunk)
      .on('end', () => resolve({res, body}))
      .on('error', reject)
  }),
  ({res, body}) => ({res, body: JSON.parse(body)}),
)

var {res, body} = await request({
  protocol: 'https:',
  hostname: 'api.github.com',
  path: '/users/simov',
})
Enter fullscreen mode Exit fullscreen mode

你能找到 API 吗?
没有。
这完全是你的,你自己的基于 Promise 的 HTTP 客户端。
恭喜!

实用性

这想法固然不错,但不太实用。毕竟,我们通常会将代码拆分成模块,而不是把所有代码都写在一个地方。

既然所有工作都必须自己完成,那为什么还要费心使用request-compose 呢?

答案很简单:

你可以选择使用哪些功能,可以根据需要进行扩展,或者完全不使用任何功能——从头开始创建你自己的东西。

不过,还有许多功能(巧妙命名的中间件),它们封装了部分 HTTP 客户端逻辑,您可能会发现它们很有用:

var compose = require('request-compose')
var Request = compose.Request
var Response = compose.Response

var request = compose(
  Request.defaults({headers: {'user-agent': 'request-compose'}}),
  Request.url('https://api.github.com/users/simov'),
  Request.send(),
  Response.buffer(),
  Response.string(),
  Response.parse(),
)

var {res, body} = await request()
Enter fullscreen mode Exit fullscreen mode

需要注意的是,这些中间件只是一个可能的实现示例,是我自己实现的。但你并非必须使用它,因为它并没有被 API 层层包围。

您可以自由创作:

var compose = require('request-compose')
var Request = compose.Request
var Response = compose.Response

var request = (options) => compose(
  Request.defaults(),
  // my own stuff here - yay!
  ({options}) => {
    options.headers['user-agent'] = 'request-compose'
    options.headers['accept'] = 'application/vnd.github.v3+json'
    return {options}
  },
  // base URL? - no problem!
  Request.url(`https://api.github.com/${options.url}`),
  Request.send(),
  Response.buffer(),
  Response.string(),
  Response.parse(),
)(options)

var {res, body} = await request({url: 'users/simov'})
Enter fullscreen mode Exit fullscreen mode

把这段代码做成模块上传到 NPM,然后就大功告成了。

圆满回归

拥有可以随意安排和扩展的独立中间件固然很好,但我们的代码能否更具表达力且更简洁呢?

嗯,这就是compose.client接口存在的唯一目的:

var {res, body} = await compose.client({
  url: 'https://api.github.com/users/simov',
  headers: {
    'user-agent': 'request-compose'
  }
})
Enter fullscreen mode Exit fullscreen mode

正如你可能已经猜到的那样,传递给compose.client 的选项只是在底层使用完全相同的内置中间件来构建 HTTP 客户端。

大干一场

让我们换个角度来看——与其只关注HTTP的内部机制,不如问问自己:

我们如何利用函数式组合来构建更大的东西?

如何编写一个高阶HTTP客户端:

var compose = require('request-compose')

var search = ((
  github = compose(
    ({query}) => compose.client({
      url: 'https://api.github.com/search/repositories',
      qs: {q: query},
      headers: {'user-agent': 'request-compose'},
    }),
    ({body}) => body.items.slice(0, 3)
      .map(({full_name, html_url}) => ({name: full_name, url: html_url})),
  ),
  gitlab = compose(
    ({query, token}) => compose.client({
      url: 'https://gitlab.com/api/v4/search',
      qs: {scope: 'projects', search: query},
      headers: {'authorization': `Bearer ${token}`},
    }),
    ({body}) => body.slice(0, 3)
      .map(({path_with_namespace, web_url}) =>
        ({name: path_with_namespace, url: web_url})),
  ),
  bitbucket = compose(
    ({query}) => compose.client({
      url: 'https://bitbucket.org/repo/all',
      qs: {name: query},
    }),
    ({body}) => body.match(/repo-link" href="[^"]+"/gi).slice(0, 3)
      .map((match) => match.replace(/repo-link" href="\/([^"]+)"/i, '$1'))
      .map((path) => ({name: path, url: `https://bitbucket.org/${path}`})),
  ),
  search = compose(
    ({query, cred}) => Promise.all([
      github({query}),
      gitlab({query, token: cred.gitlab}),
      bitbucket({query}),
    ]),
    (results) => results.reduce((all, results) => all.concat(results)),
  )) =>
    Object.assign(search, {github, gitlab, bitbucket})
)()

var results = await search({query: 'request', {gitlab: '[TOKEN]'}})
Enter fullscreen mode Exit fullscreen mode

现在您拥有了一个HTTP客户端,它可以同时在GitHub、GitLab和BitBucket中搜索代码仓库。它还会将搜索结果整齐地打包成数组返回,方便您的前端应用程序使用。

把它封装成 Lambda 函数,然后部署到云端。
这就是你的无服务器架构!

结论

如果我们拥有不会限制我们使用的模块呢?如果没有API,或者API完全可选且可扩展呢?如果我们拥有能够让我们自己成为作者,构建最适合自己的产品的工具呢?

request-compose 的设计理念正是如此,此外,它还是一个功能齐全、函数式(明白了吗?)的 NodeJS HTTP 客户端。或者更确切地说:它内置了一个带有明确设计理念的 HTTP 客户端。它涵盖了你可能遇到的绝大多数使用场景,而且它绝非一个玩具项目,也不是我的第一个 HTTP 客户端。

我不是说它是最好的,只是让你知道一下而已 :)

祝您编程愉快!

文章来源:https://dev.to/simov/composable-http-client-for-nodejs-83f