最近在做个 next.js
的内部项目,由于 next.js
可以通过文件 API
路由的方式快速创建一个 API
,因此选择了使用 restful
风格,这样可以利用好 next.js
文件路由的优势。
问题暴露
不过这样做了一段时间后便发现了一些问题:
- 每个
handler
中都需要去按照request method
来判断操作逻辑,导致每个API
路由文件中充斥着各种=== 'GET' === 'POST'
判断,并且会导致代码层级变深导致代码更丑陋。 - 为了有更好的开发体验使用了
ts
开发,然而每次API handler
中都要手动声明一次request
类型和response
类型,着实麻烦。 handler
中的报错必须要随时捕获不然就会被next.js
处理返回 500 页面。handler
中要返回的数据必须要手动调用res.json
。
基于上面这些麻烦事儿,作为一个爱偷懒的程序员,我还是决定给他封装一层。
wrapper 封装
上面列出的这些问题,其实只需要做一层简单的函数封装即可,使用时我们只需要将 handler
包在封装函数中。
/pages/api/handler.ts
import handlerWrapper from '../../handler-wrapper';
export default handlerWrapper(async (req, res) => {
// ....
});
而在 handlerWrapper
中,我们则可以对 handler
做些简单的封装
/handler-wrapper.ts
import { NextApiHandler } from 'next';
export const handlerWrapper = (handler: NextApiHandler) => (req, res) => {
try {
await handler(req, res);
} catch (e) {
logger.error(e);
res.status(500).json({
message: 'error: ' e
});
}
};
这样我们就可以通过 handlerWrapper
来掌控 handler
的行为,通过这样一层简单封装,我们可以解决掉上面所提到到的需要手动声明 handler
中参数类型的问题,还解决了代码报错时会返回 500 页面的问题。这样当代码中存在参数错误等情况时,我们就可以直接 throw
,这样外层就可以统一封装成标准化的输出进行返回了。
import handlerWrapper from '../../handler-wrapper';
export default handlerWrapper(async (req, res) => {
if (!req.query.id) throw new Error('Missing required id!');
//
});
如此开发时遍可以少写大把的 res.status(500).json()
,对一些未捕获的异常也无需心虚,自然会被 handlerWrapper
所捕获。当然也不能太过掉以轻心,比如一些异步回调函数中的报错或者是一些 error
事件等还是需要自己去处理的。
封装返回值
做完这些,我们可以再继续解决返回值封装的问题,只需要在现在的基础上做些小小的改动,res.status(200).json()
也可以不用写了:
import { NextApiHandler } from 'next';
export const handlerWrapper = (handler: NextApiHandler) => (req, res) => {
try {
const result = await handler(req, res);
res.status(200).json(result);
} catch (e) {
logger.error(e);
res.status(500).json({
message: 'error: ' e
});
}
};
代码语言:javascript复制import handlerWrapper from '../../handler-wrapper';
export default handlerWrapper(async (req, res) => {
if (!req.query.id) throw new Error('Missing required id!');
return {
dataList: await redisClient.json.get('xxx', { path: req.query.id })
};
});
我们在 handler
中只负责逻辑处理、返回和 throw
,别的都交给 handlerWrapper
来处理即可。
封装 method 处理
然而第一个问题:处理 method
逻辑分支的问题我们还没有解决,不过这种问题我们可以通过策略模式来快速搞定:
import { NextApiHandler } from 'next';
type Method = 'POST' | 'GET' | 'PUT' | 'DELETE';
export const handlerWrapper = (handlerMap: Partial<Record<Method, NextApiHandler>>) => (req, res) => {
const handler = handlerMap[req.method as Method];
if (!handler) {
res.status(500).json({
message: 'Unsupported method'
});
return;
}
try {
const result = await handler(req, res);
res.status(200).json(result);
} catch (e) {
logger.error(e);
res.status(500).json({
message: 'error: ' e
});
}
};
代码语言:javascript复制export default handlerWrapper({
GET: async function handler(req, res) {
if (!req.query.id) throw new Error('Missing required id!');
return {
dataList: await redisClient.json.get('xxx', { path: req.query.id })
};
},
POST: async function handler(req, res) {
if (!req.query.id) throw new Error('Missing required id!');
await redisClient.json.set('xxx', path, req.body);
return {
message: 'success'
};
}
});
我们只需要将 handlerWrapper
中的 handler
修改给 handlerMap
即可,这样我们就可以从策略映射表中取出 method
相应的处理函数,而未支持的 method
也可以直接交给 handlerWrapper
来进行统一判定。
结语
通过上面的封装,不需要多少时间就可以将 next.js
的 API
处理简化数倍,且让程序健壮性更高,后续的可维护性也大大提升。当然封装后也有一定的局限性,比如如果此时要用到 res.pipe
直接推送流就会需要做一些额外的处理。当然目前的封装还不算结束,后期预计还会封装一些请求参数判定、统一日志记录等。