next.js 源码解析 - API 路由篇
文章中包含大量源码排查路径相关内容,不感兴趣可直接跳到最后。
首先我们得确认下源码的位置, next.js
的 packages
中包括了很多包,我们先要确认和 API
路由相关的源码位置。
排查 CLI 源码
本章是排查
API
路由相关的源码的步骤,不感兴趣的可以跳过。
next
会启动 API
路由的命令包括:start
和 dev
,我们首先从这两个命令开始排查:
首先 next start
这些命令调用的是项目 node_modules/bin
下的 next
文件,我们找到文件后看一下:
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/xxxx"
else
export NODE_PATH="$NODE_PATH:xxxx"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../next/dist/bin/next" "$@"
else
exec node "$basedir/../next/dist/bin/next" "$@"
fi
NODE_PATH
里的代码被我省略,可以看出,该文件指向的是 next/dist/bin/next
,所以可以看出包名就是 next
,而 cli
入口文件为 next
下的 dist/bin/next
。
我们到 next
项目下确认下:
{
"bin": {
"next": "./dist/bin/next"
}
}
确认 next
命令确实是在 next
包下,且文件为 dist/bin/next
,从文件结构不难猜到,这个文件的源码就是 bin/next
文件。进入这个文件我们可以看到,调用命令的源码为:
commands[command]()
.then(exec => exec(forwardedArgs))
.then(() => {
if (command === 'build') {
// ensure process exits after build completes so open handles/connections
// don't cause process to hang
process.exit(0);
}
});
点击 commands
进去跳到 lib/commands.ts
文件:
export const commands: { [command: string]: () => Promise<cliCommand> } = {
build: () => Promise.resolve(require('../cli/next-build').nextBuild),
start: () => Promise.resolve(require('../cli/next-start').nextStart),
export: () => Promise.resolve(require('../cli/next-export').nextExport),
dev: () => Promise.resolve(require('../cli/next-dev').nextDev),
lint: () => Promise.resolve(require('../cli/next-lint').nextLint),
telemetry: () => Promise.resolve(require('../cli/next-telemetry').nextTelemetry),
info: () => Promise.resolve(require('../cli/next-info').nextInfo)
};
可以看出 start
和 dev
,最终指向的是 cli
下的 next-dev
和 next-start
文件。我们直接看 next-start
文件:
startServer({
dir,
hostname: host,
port,
keepAliveTimeout
})
.then(async app => {
const appUrl = `http://${app.hostname}:${app.port}`;
Log.ready(`started server on ${host}:${app.port}, url: ${appUrl}`);
await app.prepare();
})
.catch(err => {
console.error(err);
process.exit(1);
});
前面是一些参数的 parse
、校验,这里直接跳过,看下面主体部分,调用的是 lib/start-server
中的 startServer
,其实 next-dev
最终也会调用 startServer
,只是参数的不同。
我们再看下 startServer
,抽离其中关键代码瞧瞧:
let requestHandler: RequestHandler;
const server = http.createServer((req, res) => {
return requestHandler(req, res);
});
return new Promise<NextServer>((resolve, reject) => {
server.on('listening', () => {
const addr = server.address();
const hostname = !opts.hostname || opts.hostname === '0.0.0.0' ? 'localhost' : opts.hostname;
const app = next({
...opts,
hostname,
customServer: false,
httpServer: server,
port: addr && typeof addr === 'object' ? addr.port : port
});
requestHandler = app.getRequestHandler();
upgradeHandler = app.getUpgradeHandler();
resolve(app);
});
server.listen(port, opts.hostname);
});
可以看到此处就是调用 node
中的 http
模块创建 server
,然而 handler
是从 next
函数创建的 app
中通过 getRequestHandler
获取的。在其中经过无数次跳转最终来到 server/next.ts
中的 createServer
方法:
export class NextServer {
private async createServer(options: DevServerOptions): Promise<Server> {
if (options.dev) {
const DevServer = require('./dev/next-dev-server').default;
return new DevServer(options);
}
const ServerImplementation = await getServerImpl();
return new ServerImplementation(options);
}
}
此处会根据 dev
参数出现分支,也就是 next dev
会调用 server/dev/next-dev-server
中的 DevServer
,而 next start
则会调用 getServerImpl
其中调的是 server/next-server
中的 NextNodeServer
,而 DevServer
继承自 NextNodeServer
,上面用到的 NextNodeServer
的 getRequestHandler
又继承自 server/base-server
中的 Server
。
差不多算是找到正主了,好了,此文终结。🤦♂️ 还好代码还算清晰,虽然路径长了些,找起来还算方便。
getRequestHandler
代码篇幅过长,因为本文目的是找到 API
路由相关的代码,其余包含的很多 i18n
、cookie
、url parse
之类的代码直接跳过。进过无数次调用,getRequestHandler
最终会调用 this.run
:
export default abstract class Server<ServerOptions extends Options = Options> {
protected async run(req: BaseNextRequest, res: BaseNextResponse, parsedUrl: UrlWithParsedQuery): Promise<void> {
this.handleCompression(req, res);
try {
const matched = await this.router.execute(req, res, parsedUrl);
if (matched) {
return;
}
} catch (err) {
if (err instanceof DecodeError || err instanceof NormalizeError) {
res.statusCode = 400;
return this.renderError(null, req, res, '/_error', {});
}
throw err;
}
await this.render404(req, res, parsedUrl);
}
}
可以看出此处会调用 this.router.execute
来进行路由匹配,不匹配会进入 render404
,报错会 renderError
。this.router
是在 constructor
中通过 new Router(this.generateRoutes())
来生成,而 generateRoutes
的实现则是在 NextServer
中,其中大部分都是处理 pages
相关的请求,此处不做讨论,找到 API
相关的代码:
if (pathname === '/api' || pathname.startsWith('/api/')) {
delete query._nextBubbleNoFallback;
const handled = await this.handleApiRequest(req, res, pathname, query);
if (handled) {
return { finished: true };
}
}
此处调用 this.handleApiRequest
来处理发送到 /api/
下的 API
请求,DevServer
也复写了 generateRoutes
方法,不过其中只是变动了 fsServer
:
export default class DevServer extends Server {
generateRoutes() {
const { fsRoutes, ...otherRoutes } = super.generateRoutes();
}
}
所以最终 API
的处理都集中在 handleApiRequest
中:
export default class NextNodeServer extends BaseServer {
protected async handleApiRequest(
req: BaseNextRequest,
res: BaseNextResponse,
pathname: string,
query: ParsedUrlQuery
): Promise<boolean> {
let page = pathname;
let params: Params | undefined = undefined;
let pageFound = !isDynamicRoute(page) && (await this.hasPage(page));
if (!pageFound && this.dynamicRoutes) {
for (const dynamicRoute of this.dynamicRoutes) {
params = dynamicRoute.match(pathname) || undefined;
if (dynamicRoute.page.startsWith('/api') && params) {
page = dynamicRoute.page;
pageFound = true;
break;
}
}
}
if (!pageFound) {
return false;
}
// Make sure the page is built before getting the path
// or else it won't be in the manifest yet
await this.ensureApiPage(page);
let builtPagePath;
try {
builtPagePath = this.getPagePath(page);
} catch (err) {
if (isError(err) && err.code === 'ENOENT') {
return false;
}
throw err;
}
return this.runApi(req, res, query, params, page, builtPagePath);
}
}
其中上方做路由匹配,校验路由是否存在,然后调用 runApi
。
export default class NextNodeServer extends BaseServer {
protected async runApi(
req: BaseNextRequest | NodeNextRequest,
res: BaseNextResponse | NodeNextResponse,
query: ParsedUrlQuery,
params: Params | undefined,
page: string,
builtPagePath: string
): Promise<boolean> {
const edgeFunctions = this.getEdgeFunctions();
for (const item of edgeFunctions) {
if (item.page === page) {
const handledAsEdgeFunction = await this.runEdgeFunction({
req,
res,
query,
params,
page,
appPaths: null
});
if (handledAsEdgeFunction) {
return true;
}
}
}
const pageModule = await require(builtPagePath);
query = { ...query, ...params };
delete query.__nextLocale;
delete query.__nextDefaultLocale;
if (!this.renderOpts.dev && this._isLikeServerless) {
if (typeof pageModule.default === 'function') {
prepareServerlessUrl(req, query);
await pageModule.default(req, res);
return true;
}
}
await apiResolver(
(req as NodeNextRequest).originalRequest,
(res as NodeNextResponse).originalResponse,
query,
pageModule,
{
...this.renderOpts.previewProps,
revalidate: (newReq: IncomingMessage, newRes: ServerResponse) =>
this.getRequestHandler()(new NodeNextRequest(newReq), new NodeNextResponse(newRes)),
// internal config so is not typed
trustHostHeader: (this.nextConfig.experimental as any).trustHostHeader
},
this.minimalMode,
this.renderOpts.dev,
page
);
return true;
}
}
上方 edgeFunctions
可以看出是用来做边缘计算的,由于目前暂时对边缘计算相关内容了解不多,这里不做深入,继续向下看。下面会直接将对应的 API
路由文件通过 require
引用,中间一段是处理 serverless
,会直接调用路由模块的默认方法来处理 req
、res
。serverless
支持通过配置中的 target
进行配置。
export async function apiResolver(
req: IncomingMessage,
res: ServerResponse,
query: any,
resolverModule: any,
apiContext: ApiContext,
propagateError: boolean,
dev?: boolean,
page?: string
): Promise<void> {
const apiReq = req as NextApiRequest;
const apiRes = res as NextApiResponse;
if (!resolverModule) {
res.statusCode = 404;
res.end('Not Found');
return;
}
const config: PageConfig = resolverModule.config || {};
const bodyParser = config.api?.bodyParser !== false;
const responseLimit = config.api?.responseLimit ?? true;
const externalResolver = config.api?.externalResolver || false;
}
在 apiResolver
中首先检查代码模块检查,然后获取模块中的 config
,及其中具体的 bodyParser
参数等。
// Parsing of cookies
setLazyProp({ req: apiReq }, 'cookies', getCookieParser(req.headers));
// Parsing query string
apiReq.query = query;
// Parsing preview data
setLazyProp({ req: apiReq }, 'previewData', () => tryGetPreviewData(req, res, apiContext));
// Checking if preview mode is enabled
setLazyProp({ req: apiReq }, 'preview', () => (apiReq.previewData !== false ? true : undefined));
// Parsing of body
if (bodyParser && !apiReq.body) {
apiReq.body = await parseBody(
apiReq,
config.api && config.api.bodyParser && config.api.bodyParser.sizeLimit ? config.api.bodyParser.sizeLimit : '1mb'
);
}
然后是进行 cookie
、query
、previewData
、preview
、bodyParser
、body
等获取和处理。此处还设置了 previewData
和 preview
,这两个属性在页面中有使用,但是在 API
路由中的用途在官网没有介绍,我暂时也没遇到使用场景,也不多说了。代码中还用到一段 setLazyProp
:
export function setLazyProp<T>({ req }: LazyProps, prop: string, getter: () => T): void {
const opts = { configurable: true, enumerable: true };
const optsReset = { ...opts, writable: true };
Object.defineProperty(req, prop, {
...opts,
get: () => {
const value = getter();
// we set the property on the object to avoid recalculating it
Object.defineProperty(req, prop, { ...optsReset, value });
return value;
},
set: value => {
Object.defineProperty(req, prop, { ...optsReset, value });
}
});
}
用途是将传入的 req
,上面挂载 getter
、setter
,这样在第一次获取上面的属性时,会走到 get
定义,此时才会执行传入的 getter
函数,并覆盖 getter setter
定义,这样就可以做到只有当我们去使用 req
上的对应属性时,才回去调用 setter
,并且覆盖后不会再重复调用 setter
,算是一个常见的懒加载性能优化。
然后下面的大部分代码都是扩展 res
和 req
上的属性,如 status
、send
、json
等,后面还用到了 interopDefault
来获取路由模块中的处理函数:
export function interopDefault(mod: any) {
return mod.default || mod;
}
所以 API
路由模块文件中的默认处理函数可以使用 esm
的 export default
也可以使用 cjs
中的 module.exports
。
总结
从源码解析我们可以看到文档中没有描绘出来的一些点:
next.js
中的/api
下的请求和ssr
、ssg
是同一个server
处理,只是ssr
、ssg
的处理由next.js
内部处理,而api
请求会转交给API
路由文件中的默认函数来处理。API
路由文件可以使用esm
模式导出处理函数,也可以使用cjs
导出。API
路由下也可以访问到previewData
和preview
,虽然暂不清楚用途是什么。cookies
、previewData
和preview
结尾lazyProp
,只有在访问该属性时才会去挂载。