适用于 NodeJS 的可组合 HTTP 客户端
API,API,到处都是。
作品
实用性
圆满回归
大干一场
结论
所以我为NodeJS编写了这个HTTP客户端:
var compose = require('request-compose')
顺便说一句,如果你想直接跳过前面的环节,立刻沉浸在FP的精彩内容中——那就别犹豫了。
它是如何使用的?
var {res, body} = await compose.client({
url: 'https://api.github.com/users/simov',
headers: {
'user-agent': 'request-compose'
}
})
哇!真的吗?!
又一个NodeJS的HTTP客户端!
太棒了!!!
API,API,到处都是。
作为最终用户,如果我想对其他人的模块进行修复、更改或添加某些内容,我该怎么办?我有哪些选择?
- 在 GitHub 上创建一个 issue 并提出请求
- 我自己实现并提交一个拉取请求
- 寻找满足我需求的替代模块
- 重复
原因在于模块作者会提供一份API文档,明确规定你能做什么和不能做什么。你实际上被限制在文档的范围内。此外,作者还会严格保护项目的范围,防止无关内容渗入。
但是,如果我们拥有更强大的原语,允许我们深入底层,优雅地构建我们自己的功能,那会怎样呢?完全为我们自己服务,彻底绕过其他方案中存在的 API 和作用域瓶颈。
作品
幸运的是,有一种叫做函数式组合的原始技术:
在计算机科学中,函数组合(不要与对象组合混淆)是一种将简单函数组合起来构建更复杂函数的行为或机制。与数学中常见的函数组合类似,每个函数的结果都作为下一个函数的参数传递,而最后一个函数的结果就是整个函数的最终结果。
事实上,request-compose所公开的正是这一点:
var compose = (...fns) => (args) =>
fns.reduce((p, f) => p.then(f), Promise.resolve(args))
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)
或者更确切地说——编写你自己的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',
})
你能找到 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()
需要注意的是,这些中间件只是一个可能的实现示例,是我自己实现的。但你并非必须使用它,因为它并没有被 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'})
把这段代码做成模块上传到 NPM,然后就大功告成了。
圆满回归
拥有可以随意安排和扩展的独立中间件固然很好,但我们的代码能否更具表达力且更简洁呢?
嗯,这就是compose.client接口存在的唯一目的:
var {res, body} = await compose.client({
url: 'https://api.github.com/users/simov',
headers: {
'user-agent': 'request-compose'
}
})
正如你可能已经猜到的那样,传递给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]'}})
现在您拥有了一个HTTP客户端,它可以同时在GitHub、GitLab和BitBucket中搜索代码仓库。它还会将搜索结果整齐地打包成数组返回,方便您的前端应用程序使用。
把它封装成 Lambda 函数,然后部署到云端。
这就是你的无服务器架构!
结论
如果我们拥有不会限制我们使用的模块呢?如果没有API,或者API完全可选且可扩展呢?如果我们拥有能够让我们自己成为作者,构建最适合自己的产品的工具呢?
request-compose 的设计理念正是如此,此外,它还是一个功能齐全、函数式(明白了吗?)的 NodeJS HTTP 客户端。或者更确切地说:它内置了一个带有明确设计理念的 HTTP 客户端。它涵盖了你可能遇到的绝大多数使用场景,而且它绝非一个玩具项目,也不是我的第一个 HTTP 客户端。
我不是说它是最好的,只是让你知道一下而已 :)
祝您编程愉快!
文章来源:https://dev.to/simov/composable-http-client-for-nodejs-83f