nextjs-interceptor
Version:
A powerful and flexible interceptor middleware for Next.js applications, providing seamless request/response manipulation capabilities
130 lines (129 loc) • 4.69 kB
JavaScript
export class InterceptorRegistry {
constructor() {
this.interceptors = new Map();
}
use(config, handler) {
if (this.interceptors.has(config.id)) {
console.warn(`拦截器 ${config.id} 已存在,将被覆盖`);
}
this.interceptors.set(config.id, {
config: {
...config,
priority: config.priority || 0,
},
handler,
});
return this;
}
getPatterns() {
return Array.from(this.interceptors.values()).flatMap((i) => Array.isArray(i.config.pattern) ? i.config.pattern : [i.config.pattern]);
}
/**
* 获取所有拦截器的排除模式
* @returns 排除模式数组
*/
getExcludePatterns() {
return Array.from(this.interceptors.values())
.filter((i) => i.config.exclude)
.flatMap((i) => Array.isArray(i.config.exclude) ? i.config.exclude : [i.config.exclude]);
}
getMatchers() {
return this.getPatterns();
}
async handle(request, event) {
const sortedInterceptors = this.getSortedInterceptors();
// 依次执行每个拦截器
for (const interceptor of sortedInterceptors) {
// 如果拦截器不匹配当前请求,跳过
if (!this.matches(request, interceptor.config)) {
continue;
}
// 执行拦截器
const response = await interceptor.handler(request, event);
// 如果拦截器返回了 Response,直接返回结果
// 如果返回 null,继续执行下一个拦截器
if (response !== null) {
return response;
}
}
// 所有拦截器都执行完了,返回 null
return;
}
getSortedInterceptors() {
return Array.from(this.interceptors.values())
.sort((a, b) => (a.config.priority || 0) - (b.config.priority || 0));
}
matchesPattern(req, pattern) {
const patterns = Array.isArray(pattern) ? pattern : [pattern];
const pathname = req.nextUrl.pathname;
return patterns.some((p) => {
if (typeof p === "string") {
return new RegExp(p).test(pathname);
}
return p.test(pathname);
});
}
/**
* 检查请求路径是否匹配排除模式
* @param req - Next.js 请求对象
* @param exclude - 排除模式:字符串、正则表达式或它们的数组
* @returns 如果匹配任意一个排除模式则返回 true
*/
matchesExcludePattern(req, exclude) {
if (!exclude)
return false;
const excludePatterns = Array.isArray(exclude) ? exclude : [exclude];
const pathname = req.nextUrl.pathname;
return excludePatterns.some((p) => {
if (typeof p === "string") {
return new RegExp(p).test(pathname);
}
return p.test(pathname);
});
}
matchesConditions(req, conditions) {
if (!conditions)
return true;
const { headers, query, cookies } = conditions;
if (headers &&
!this.matchesRecord(Object.fromEntries(req.headers.entries()), headers)) {
return false;
}
if (query &&
!this.matchesRecord(Object.fromEntries(req.nextUrl.searchParams.entries()), query)) {
return false;
}
const _cookies = {};
req.cookies.getAll().forEach((cookie) => {
_cookies[cookie.name] = cookie.value;
});
if (cookies && !this.matchesRecord(_cookies, cookies)) {
return false;
}
return true;
}
matchesRecord(actual, expected) {
return Object.entries(expected).every(([key, value]) => {
const actualValue = actual[key];
if (!actualValue)
return false;
if (value instanceof RegExp) {
return value.test(actualValue);
}
return value === actualValue;
});
}
matches(req, config) {
return (this.matchesPattern(req, config.pattern) &&
!this.matchesExcludePattern(req, config.exclude) &&
this.matchesConditions(req, config.conditions));
}
}
const singleton = () => {
return new InterceptorRegistry();
};
const interceptorRegistry = globalThis.interceptorRegistry ?? singleton();
if (process.env.NODE_ENV !== "production")
globalThis.interceptorRegistry = interceptorRegistry;
export { interceptorRegistry };
export const interceptorMiddleware = interceptorRegistry.handle.bind(interceptorRegistry);