next.js 源码解析 - API 路由篇

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 首先我们得确认下源码的位置, next.js 的 packages 中包括了很多包,我们先要确认和 API 路由相关的源码位置。

网络异常,图片无法展示
|

文章中包含大量源码排查路径相关内容,不感兴趣可直接跳到最后。

首先我们得确认下源码的位置, next.js 的 packages 中包括了很多包,我们先要确认和 API 路由相关的源码位置。

排查 CLI 源码

本章是排查 API 路由相关的源码的步骤,不感兴趣的可以跳过。

next 会启动 API 路由的命令包括:startdev,我们首先从这两个命令开始排查:

首先 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,只有在访问该属性时才会去挂载。
相关文章
|
25天前
|
JSON 缓存 JavaScript
深入浅出:使用Node.js构建RESTful API
在这个数字时代,API已成为软件开发的基石之一。本文旨在引导初学者通过Node.js和Express框架快速搭建一个功能完备的RESTful API。我们将从零开始,逐步深入,不仅涉及代码编写,还包括设计原则、最佳实践及调试技巧。无论你是初探后端开发,还是希望扩展你的技术栈,这篇文章都将是你的理想指南。
|
18天前
|
JSON JavaScript 前端开发
深入浅出Node.js:从零开始构建RESTful API
在数字化时代的浪潮中,后端开发作为连接用户与数据的桥梁,扮演着至关重要的角色。本文将引导您步入Node.js的奇妙世界,通过实践操作,掌握如何使用这一强大的JavaScript运行时环境构建高效、可扩展的RESTful API。我们将一同探索Express框架的使用,学习如何设计API端点,处理数据请求,并实现身份验证机制,最终部署我们的成果到云服务器上。无论您是初学者还是有一定基础的开发者,这篇文章都将为您打开一扇通往后端开发深层知识的大门。
34 12
|
16天前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
20天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
50 12
|
17天前
|
监控 数据管理 测试技术
API接口自动化测试深度解析与最佳实践指南
本文详细介绍了API接口自动化测试的重要性、核心概念及实施步骤,强调了从明确测试目标、选择合适工具、编写高质量测试用例到构建稳定测试环境、执行自动化测试、分析测试结果、回归测试及集成CI/CD流程的全过程,旨在为开发者提供一套全面的技术指南,确保API的高质量与稳定性。
|
24天前
|
JavaScript NoSQL API
深入浅出Node.js:从零开始构建RESTful API
在数字化时代的浪潮中,后端开发如同一座灯塔,指引着数据的海洋。本文将带你航行在Node.js的海域,探索如何从一张白纸到完成一个功能完备的RESTful API。我们将一起学习如何搭建开发环境、设计API结构、处理数据请求与响应,以及实现数据库交互。准备好了吗?启航吧!
|
25天前
|
JavaScript 前端开发 API
Vue.js 3:探索组合式API带来的新变革
Vue.js 3:探索组合式API带来的新变革
|
14天前
|
监控 搜索推荐 测试技术
电商API的测试与用途:深度解析与实践
在电子商务蓬勃发展的今天,电商API成为连接电商平台、商家、消费者和第三方开发者的重要桥梁。本文深入探讨了电商API的核心功能,包括订单管理、商品管理、用户管理、支付管理和物流管理,并介绍了有效的测试技巧,如理解API文档、设计测试用例、搭建测试环境、自动化测试、压力测试、安全性测试等。文章还详细阐述了电商API的多样化用途,如商品信息获取、订单管理自动化、用户数据管理、库存同步、物流跟踪、支付处理、促销活动管理、评价管理、数据报告和分析、扩展平台功能及跨境电商等,旨在为开发者和电商平台提供有益的参考。
23 0
|
25天前
|
JavaScript 前端开发 API
Vue.js 3:深入探索组合式API的实践与应用
Vue.js 3:深入探索组合式API的实践与应用

推荐镜像

更多
下一篇
DataWorks