我在带有express的节点中使用Ramda
。我有一条标准路线:
app.get('/api/v1/tours', (req, res) => {
}
我想使用Ramda编写函数的地方,但是我在路由之外编写了这些函数(因此它们可以在其他路由中重用)。 例如:
function extractParams() {
return req.query.id;
}
function findXById(id) {
return xs.find(el => el.id == id);
}
function success(answer) {
return res.status(200).json(answer);
}
现在,我想在多个路由器中组成这些功能。其中之一将是:
app.get('/api/v1/tours', (req, res) => {
return R.pipe(extractParams, findXById, success)();
}
有什么办法可以准备一个通用包装器,将请求和响应对象包装在路由器上以供这些功能使用?我想我会 还必须更改其签名。
答案 0 :(得分:1)
我认为这里真正需要的是pipe
的版本,该版本接受一些初始参数并返回一个新函数,该函数将接受其余参数,所有函数都具有双重应用程序签名。我想出了以下doublePipe
实现方案:
const doublePipe = (...fns) => (...initialArgs) =>
pipe (...(map (pipe (apply, applyTo (initialArgs)), fns) ))
const foo = (x, y) => (z) => (x + y) * z
const bar = (x, y) => (z) => (x + y) * (z + 1)
const baz = doublePipe (foo, bar)
console .log (
baz (2, 4) (1) //=> (2 + 4) * (((2 + 4) * 1) + 1) => 42
// / \ '------+----'
// bar ( x --/ , `-- y , `-- z, which is foo (2, 4) (1) )
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {pipe, map, apply, applyTo} = R </script>
请注意,函数foo
和bar
都将接收相同的x
和y
参数,而foo (x, y)
将接收{{1} }参数从外部提供,其结果作为z
传递给z
。
这是一个有趣的功能,它是解决此类问题的相当有用的通用解决方案。 但是在您的Express环境中不起作用,因为处理程序需要具有签名bar (x, y)
而不是(req, res) => ...
。
因此,下面是一个替代方案,它模仿一个普通的类似Express的环境,并使用稍有不同的(req, res) => (...args) => ...
版本,该版本不需要额外的调用,只需调用不带参数的第一个函数,然后顺序传递结果按预期传递给其他人。这意味着doublePipe
的第一个函数必须具有签名doublePipe
,而其他函数则具有(req, res) => () => ...
。尽管我们可以对其进行修复,以使第一个只是(req, res) => (val) => ...
,但在我看来,这种不一致将无济于事。
(req, res) => ...
const doublePipe = (...fns) => (...initialArgs) =>
reduce (applyTo, void 0, map (apply (__, initialArgs), fns))
const xs = [{id: 1, val: 'abc'}, {id: 2, val: 'def'},{id: 3, val: 'ghi'}, {id: 4, val: 'jkl'}]
const extractParams = (req, res) => () => req .query .id
const findXById = (xs) => (req, res) => (id) => xs .find (el => el .id == id)
const success = (req, res) => (answer) => res .status (200) .json (answer)
app .get ('/api/v1/tours', doublePipe (extractParams, findXById (xs), success))
console .log (
app .invoke ('get', '/api/v1/tours?foo=bar&id=3')
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>
const {__, map, reduce, applyTo, apply, head, compose, split, objOf, fromPairs, last} = R
// Minimal version of Express, only enough for this demo
const base = compose (head, split ('?'))
const makeRequest = compose (objOf ('query'), fromPairs, map (split ('=')), split ('&'), last, split ('?'))
const makeResponse = () => {
const response = {
status: (val) => {response .status = val; return response},
json: (val) => {response.body = JSON .stringify (val); delete response.json; return response}
}
return response
}
const app = {
handlers: {get: {}, post: {}},
get: (route, handler) => app .handlers .get [route] = handler,
invoke: (method, route) =>
app .handlers [method] [base (route)] (makeRequest (route), makeResponse ())
}
</script>
没有必需的签名,但是findById
有签名,因此我们将这传递给findById(xs)
。
最后,请注意,Ramda和Express可能永远无法发挥特别出色的性能,因为发送给Express的处理程序旨在修改其参数,而Ramda则设计为永不突变输入数据。话虽如此,对于这些要求,这似乎工作得很好。
pipe
似乎有一条评论表明应该对doublePipe
进行更完整的描述。我将只讨论第二个版本,
doublePipe
有两个可能的电话:
const doublePipe = (...fns) => (...initialArgs) =>
reduce (applyTo, void 0, map (apply (__, initialArgs), fns))
如果您不熟悉Hindley-Milner签名(例如上面的// foo :: (a, b) -> f
const foo = doublePipe (
f1, // :: (a, b) -> Void -> (c)
f2, // :: (a, b) -> c -> d
f3, // :: (a, b) -> d -> e
f4, // :: (a, b) -> e -> f
)
// bar :: (a, b, c) -> f
const bar = doublePipe (
g1, // :: (a, b, c) -> Void -> d
g2, // :: (a, b, c) -> d -> e
g3, // :: (a, b, c) -> e -> f
)
),我会在long article上写一个Ramda wiki,以了解它们在Ramda中的用途。通过将(a, b) -> c -> d
-foo
传递到f1
来构建f4
函数。结果函数采用类型doublePipe
和a
(在您的示例中为b
和req
)的参数,并返回类型res
的值。同样,通过向f
提供bar
-g1
,并返回接受类型为g3
,doublePipe
和{ {1}},并返回类型为a
的值。
我们可以更强制性地重写b
来显示所采取的步骤:
c
并将其扩展一点,这看起来也可能
f
在第一行中,我们将初始参数部分地应用于每个提供的函数,从而为我们提供了一些简单函数的列表。对于doublePipe
,resultFns看起来像const doublePipe = (...fns) => (...initialArgs) => {
const resultFns = map (apply (__, initialArgs), fns)
return reduce (applyTo, void 0, resultFns)
}
,其签名为const doublePipe = (...fns) => (...initialArgs) => {
const resultFns = map (fn => fn(...initialArgs), fns)
return reduce ((value, fn) => fn (value), undefined, resultFns)
}
。现在,我们可以选择foo
这些函数并调用结果函数([f1(req, res), f2(req, res), f3(req, res), f4(req, res)]
),但是我没有看到创建管道函数的充分理由,该函数只能调用一次并扔掉,所以我[Void -> c, c -> d, d -> e, e -> f]
在该列表上,从pipe
开始,并将每个结果传递到下一个。
我倾向于就Ramda函数进行思考,但是如果没有它们,您可以很容易地编写此代码:
return pipe(...resultFns)()
我希望这可以使事情变得更清楚!
答案 1 :(得分:0)
您的三个函数在其声明的范围内没有所需的东西。您需要先修改其签名:
function extractParams(req) { //<-- added `req`
return req.query.id;
}
function findXById(id, xs) { //<-- added `xs`
return xs.find(el => el.id == id);
}
function success(res, answer) { //<-- added `res`
return res.status(200).json(answer);
}
请注意,参数的顺序不是“随机”的。您需要处理的数据应该是最后一个,因为它可以提供更好的功能组合体验。这是Ramda的宗旨之一:
Ramda函数的参数进行了排列,以方便进行计算。通常要最后处理要处理的数据。
但这还不够。您需要咖喱一些。为什么?尽管功能组成的“配方”看起来相同,但是每个单独的功能都在特定数据上运行。稍后将有道理,让我们先咖喱:
const extractParams = (req) => req.query.id;
const findXById = R.curry((id, xs) => xs.find(el => el.id == id));
const success = R.curry((res, answer) => res.status(200).json(answer));
现在,您可以构建函数组合,同时为组合中的函数提供一些特定参数:
app.get('/api/v1/tours', (req, res) =>
R.pipe(
extractParams,
findXById(42),
success(res))
(req));
重要的是要注意,尽管这没有什么“错”,但它也没有抓住重点:
R.pipe(extractParams, findXById, success)()
为什么? R.pipe
或R.compose
(或R.o
)返回一个函数组合,该函数组合本身就是您使用参数调用的函数(仅使用R.o
进行调用,但现在暂时忽略它)。因此,您需要考虑贯穿功能组合的数据。在您的情况下,可能是req
:
R.pipe(extractParams, findXById, success)(req)
函数组合中的每个函数都将前一个函数的结果作为参数接收。如果介于两者之间的某些东西不依赖于此,那么也许该功能不应该是组成部分。 (请以少量盐作为建议;可能会适用特殊条件 ;只需考虑一下;)