最近在做个 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
});
}
};
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
});
}
};
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 直接推送流就会需要做一些额外的处理。当然目前的封装还不算结束,后期预计还会封装一些请求参数判定、统一日志记录等。