今天分享一个来自社区的 nextjs 非常好的中间件实践,
我们知道 nextjs 原生提供的中间件比较原始简陋,如果想实现比较多的功能代码实现起来会比较丑陋
这个版本主要是让 nextj 支持类似 express 中间件的模型,可以使用链式调用中间件
-
我们在
src或者app目录下新建middlewares文件夹,在middlewares文件夹下新建util文件夹 -
util文件夹下新建chain.ts
import { NextFetchEvent, NextRequest, NextResponse } from 'next/server'
import { mergeHeaders } from './merge-headers'
type GoNextMiddleware = () => 'continue'
export type MiddlewareFunction = (
request: NextRequest,
next: GoNextMiddleware,
event: NextFetchEvent
) => Promise<NextResponse<unknown> | ReturnType<GoNextMiddleware>>
export function composeMiddleware(handlers: MiddlewareFunction[] = []) {
const validMiddlewareHandlers = handlers.filter((handler) => typeof handler === 'function')
return async function (request: NextRequest, event: NextFetchEvent) {
const allResponses: NextResponse[] = []
// 1.
// run every middleware and collect responses (NextResponse)
// until a middleware want to break the chain (redirect or rewrite)
for (const fn of validMiddlewareHandlers) {
const result = await fn(request, () => 'continue', event)
// ensure that fn returned something or notify the dev
if (result !== 'continue' && !(result instanceof NextResponse)) {
console.error(
`The middleware chain has been broken because '${fn.name}' did not return a NextResponse or call next().`
)
return NextResponse.next()
}
// go next middleware
if (result === 'continue') continue
// we have a response
allResponses.push(result)
// the "middleware" function cannot return a native Response
// @see https://nextjs.org/docs/messages/middleware-upgrade-guide#no-response-body
// It can only :
// - return `NextResponse.redirect() or NextResponse.rewrite()`
// => this must break the chian
// - return a mutated request using `NextResponse.next({ request: { /* ... */ }})`
// => this must NOT break the chain
const isRedirect = () => result.headers.get('Location')
const isRewrite = () => result.headers.get('x-middleware-rewrite')
if (isRedirect() || isRewrite()) {
// break the chain
break
}
}
// 2.
// return final response
// middlewares have not returned any response, do nothing...
if (allResponses.length === 0) return NextResponse.next()
// only one middleware returned a response, return it
if (allResponses.length === 1) return allResponses[0]
// more than one middleware returned a response
// merge headers into a final response and return it
const finalResponse = allResponses[allResponses.length - 1]
const finalHeaders: Headers = mergeHeaders(...allResponses.map((r) => r.headers))
for (const [key] of Array.from(finalResponse.headers.entries())) {
finalResponse.headers.delete(key)
}
for (const [key, value] of Array.from(finalHeaders.entries())) {
finalResponse.headers.set(key, value)
}
return finalResponse
}
}
utils文件夹下新建merge-headers.ts
// @credits
// https://github.com/whitecrownclown/merge-headers/blob/master/index.ts
function isObject(value: any) {
return value !== null && typeof value === 'object'
}
export function mergeHeaders(...sources: HeadersInit[]) {
const result: Record<string, string> = {}
for (const source of sources) {
if (!isObject(source)) {
throw new TypeError('All arguments must be of type object')
}
const headers: Headers = new Headers(source)
for (const [key, value] of Array.from(headers.entries())) {
if (value === undefined || value === 'undefined') {
delete result[key]
} else {
result[key] = value
}
}
}
return new Headers(result)
}
- 下边我们举个
proxy中间件的例子说明如何使用这一套链式中间件,在middlewares文件夹下新建 proxy.middleware.ts
import { NextResponse, type NextRequest } from 'next/server'
import { MiddlewareFunction } from '@/middlewares/utils/chain'
export const proxyMiddleware = (req: NextRequest) => {
const destination = new URL(`${process.env.YOUR_BACKEND_SERVER}`)
const url = req.nextUrl.clone()
url.host = destination.host
url.protocol = destination.protocol
url.port = destination.port
return NextResponse.rewrite(url)
}
export const handleProxyMiddleware: MiddlewareFunction = async (req, next) => {
if (req.nextUrl.pathname.startsWith(`/api`)) {
return proxyMiddleware(req)
}
return next()
}
这个是一个生产可用版本的 proxy middleware,用来代理访问后端接口,将 process.env.YOUR_BACKEND_SERVER 换成你自己的后端环境变量就OK了,我们可以看到这个 handleProxyMiddleware 包括了两个参数,一个是 next 包装后的 request 对象,另一个是 next() 方法,用来在执行结束后调用, 调用 next() 方法就会去到下一个中间件
- 接下来,在
middlewares文件夹下新建index.ts文件夹,我们把这个proxyMiddleware放到这里来
import { handleProxyMiddleware } from './proxy.middleware'
export const middlewares = [handleProxyMiddleware]
OK,这时候我们主要的部分就结束了,你可以把新建的其余的middlewares加入到这个 middlewares 数组里面,需要注意的是,这里的数组顺序就是中间件执行的顺序,如果你需要有序的中间件,这里要注意放置的顺序
- 最后一步了,在
nextjs约定的src或者app或者pages文件夹下新建middleware.ts文件放入如下代码:
import { composeMiddleware } from '@/middlewares/utils/chain'
import { middlewares } from './middlewares'
export default composeMiddleware(middlewares)
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)', '/api/:path*'],
}
到此为止,一个生产可用的,优雅的 nextjs 中间件就实现了,可以让你的代码更好的维护,分离中间件的各个职责,祝大家用起来开心 ^_^