您将无法完全避免副作用,但您可以努力尽可能地最大限度地消除它们。
例如,Express 框架本质上是势在必行的。你运行类似的函数res.send()
完全是因为它们的副作用(大多数时候你甚至不关心它的返回值)。
你可以做什么(除了使用const
对于您的所有声明,使用不可变.js https://facebook.github.io/immutable-js/数据结构,Ramda http://ramdajs.com/,将所有函数写为const fun = arg => expression;
代替const fun = (arg) => { statement; statement; };
等)将是对 Express 通常如何工作进行一些抽象。
例如,您可以创建函数req
作为参数并返回一个对象,其中包含响应状态、标头和要作为正文传输的流。从某种意义上说,这些函数可以是纯函数,它们的返回值仅取决于它们的参数(请求对象),但您仍然需要一些包装器来使用 Express 固有的命令式 API 实际发送响应。这可能不是微不足道的,但它是可以做到的。
作为示例,请考虑以下函数,该函数将 body 作为对象并以 json 形式发送:
const wrap = f => (req, res) => {
const { status = 200, headers = {}, body = {} } = f(req);
res.status(status).set(headers).json(body);
};
它可以用来创建这样的路由处理程序:
app.get('/sum/:x/:y', wrap(req => ({
headers: { 'Foo': 'Bar' },
body: { result: +req.params.x + +req.params.y },
})));
使用返回单个表达式且没有副作用的函数。
完整示例:
const app = require('express')();
const wrap = f => (req, res) => {
const { status = 200, headers = {}, body = {} } = f(req);
res.status(status).set(headers).json(body);
};
app.get('/sum/:x/:y', wrap(req => ({
headers: { 'Foo': 'Bar' },
body: { result: +req.params.x + +req.params.y },
})));
app.listen(4444);
测试响应:
$ curl localhost:4444/sum/2/4 -v
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 4444 (#0)
> GET /sum/2/4 HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:4444
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Foo: Bar
< Content-Type: application/json; charset=utf-8
< Content-Length: 12
< ETag: W/"c-Up02vIPchuYz06aaEYNjufz5tpQ"
< Date: Wed, 19 Jul 2017 15:14:37 GMT
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
{"result":6}
当然这只是一个基本想法。你可以使wrap()
函数接受承诺异步操作函数的返回值,但可以说它不会那么无副作用:
const wrap = f => async (req, res) => {
const { status = 200, headers = {}, body = {} } = await f(req);
res.status(status).set(headers).json(body);
};
和一个处理程序:
const delay = (t, v) => new Promise(resolve => setTimeout(() => resolve(v), t));
app.get('/sum/:x/:y', wrap(req =>
delay(1000, +req.params.x + +req.params.y).then(result => ({
headers: { 'Foo': 'Bar' },
body: { result },
}))));
I used .then()
代替async
/await
在处理程序本身中,使其看起来更具功能性,但它可以写为:
app.get('/sum/:x/:y', wrap(async req => ({
headers: { 'Foo': 'Bar' },
body: { result: await delay(1000, +req.params.x + +req.params.y) },
})));
如果作为参数的函数可以变得更加通用wrap
将是一个生成器,它不会仅生成承诺解决(就像基于生成器的协程通常所做的那样),它会生成承诺解决或卡盘流,并通过一些包装来区分两者。这只是一个基本想法,但它可以进一步扩展。