Read OSS

Next.js 代码库架构:全局地图

中级

前置知识

  • 具备作为应用开发者使用 Next.js 的基本经验
  • 对 monorepo 结构和包管理器有基本了解
  • 了解服务端渲染与客户端渲染的区别

Next.js 代码库架构:全局地图

Next.js 是 JavaScript 生态中体量最大的开源 TypeScript 项目之一。仅核心包就超过 7,000 个源文件,加上驱动打包和编译流水线的 Rust 层,整个代码库对初学者来说可谓壁垒重重。本文是你的入门向导——在深入任何单一子系统之前,先从全局视角了解这张"地图"。我们将介绍 monorepo 的组织方式、各模块的职责划分,以及贯穿后续所有深度解析的核心架构概念。

Monorepo 布局与工具链

Next.js 代码库是一个由 Turborepo 管理的 pnpm workspace。pnpm-workspace.yaml 中的 workspace 配置定义了各包的边界:

graph TD
    Root["nextjs-project (root)"] --> Packages["packages/* (19 npm packages)"]
    Root --> Apps["apps/* (docs, analyzer)"]
    Root --> Crates["crates/* (Rust workspace)"]
    Root --> Turbopack["turbopack/ (vendored)"]
    Root --> Bench["bench/* (benchmarks)"]

    Packages --> Core["next (core framework)"]
    Packages --> SWC["next-swc (native bindings)"]
    Packages --> Rspack["next-rspack"]
    Packages --> CreateApp["create-next-app"]
    Packages --> Font["@next/font"]
    Packages --> ESLint["eslint-plugin-next"]
    Packages --> Others["+ 13 more"]

    Crates --> NapiBind["next-napi-bindings"]
    Crates --> NextCore["next-core"]
    Crates --> CustomTransforms["next-custom-transforms"]
    Crates --> NextBuild["next-build"]

packages/ 下的 19 个包各司其职:

包名 职责
next 核心框架——CLI、服务端、客户端、构建系统
next-swc Rust 的 N-API 原生绑定(SWC 编译转换 + Turbopack)
next-rspack Rspack 打包器集成
create-next-app 项目脚手架 CLI
eslint-plugin-next Next.js 规范的 ESLint 规则
@next/font 字体优化
react-refresh-utils React Fast Refresh 的 HMR 支持
next-codemod 自动化迁移 codemod 工具

Rust 部分分布在两处:crates/ 包含 Next.js 专属的 Rust crate(SWC 编译转换、Turbopack 集成、NAPI 绑定),turbopack/ 则是 Turbopack 打包引擎的 vendored 副本。

根目录的 Cargo.toml 定义了 Rust workspace,成员包括 next-napi-bindingsnext-corenext-custom-transforms 以及整个 turbopack/crates/* 子树。构建编排由 turbo.json 负责,配置十分简洁——主 build 任务依赖 ^build(上游包),输出目录为 dist/

提示: 探索代码库时,优先聚焦 packages/next/src/。其他部分——200 多个示例、测试套件、性能基准——对于理解 Next.js 的工作原理来说都是外围内容。

核心包:packages/next/src/

绝大多数框架逻辑都位于 packages/next 包中。它的 package.json 定义了 next 二进制文件、主入口点(./dist/server/next.js),以及对外暴露 next/navigationnext/headersnext/imagenext/cache 等公共模块的 exports 映射。

src/ 目录按执行上下文组织:

graph TD
    src["packages/next/src/"] --> cli["cli/ — CLI 命令(dev, build, start)"]
    src --> server["server/ — 服务端运行时(Node.js + Edge)"]
    src --> client["client/ — 浏览器运行时(router, components)"]
    src --> build["build/ — 构建系统(webpack config, plugins, loaders)"]
    src --> shared["shared/ — 跨上下文共享代码"]
    src --> lib["lib/ — 内部工具库"]

    server --> baseServer["base-server.ts (abstract Server class)"]
    server --> nextServer["next-server.ts (Node.js server)"]
    server --> appRender["app-render/ (App Router rendering)"]
    server --> routeModules["route-modules/ (route handlers)"]

    client --> appRouter["components/app-router.tsx"]
    client --> routerReducer["components/router-reducer/"]
    client --> segmentCache["components/segment-cache/"]

    build --> webpackConfig["webpack-config.ts"]
    build --> plugins["webpack/plugins/"]
    build --> templates["templates/"]

这种组织方式并非随意为之——它直接对应 Next.js 代码运行的三种环境。server/ 包含在 Node.js 或 Edge 运行时执行的代码;client/ 包含打包给浏览器的代码;build/ 在构建时运行,生成供前两者消费的产物;shared/ 存放类型定义、常量和工具函数,可在任何上下文中安全引用。

三种执行上下文:Build、Server、Client

理解 Next.js 的关键,在于认识到每一段代码都会被编译为三个目标之一。这一点在 COMPILER_NAMES 常量中有明确体现:

export const COMPILER_NAMES = {
  client: 'client',
  server: 'server',
  edgeServer: 'edge-server',
} as const

每个编译器生成独立的 bundle,拥有各自的模块解析规则、externals 配置和 polyfill。client 编译器生成包含 React Client Components 的浏览器 JavaScript;server 编译器生成可访问 fscrypto 及完整 Node.js API 的 Node.js bundle;edge-server 编译器生成适用于受限 Edge 运行时的 bundle——不支持 Node.js API,仅限 V8 环境。

flowchart LR
    Source["Your Source Code"] --> ClientCompiler["Client Compiler"]
    Source --> ServerCompiler["Server Compiler"]
    Source --> EdgeCompiler["Edge Server Compiler"]

    ClientCompiler --> Browser["Browser Bundle\n(React, client components)"]
    ServerCompiler --> NodeJS["Node.js Bundle\n(RSC, API routes, SSR)"]
    EdgeCompiler --> Edge["Edge Bundle\n(Middleware, edge routes)"]

同一个源文件可能出现在多次编译中——标注了 'use client' 的组件,会同时被纳入服务端 bundle(作为 client reference)和客户端 bundle(作为实际可执行代码)。这种三路拆分正是代码库复杂性的根本原因,也是你在 server、build 和 client 层随处可见并行代码路径的缘故。

同一文件还定义了构建阶段,用于决定各阶段应用哪套配置:

export const PHASE_PRODUCTION_BUILD = 'phase-production-build'
export const PHASE_PRODUCTION_SERVER = 'phase-production-server'
export const PHASE_DEVELOPMENT_SERVER = 'phase-development-server'
export const PHASE_TEST = 'phase-test'

这些阶段标识在 next.config.js 为函数时传入其中,允许用户按上下文调整配置。代码库中大量引用这些阶段常量,以区分构建时和运行时的行为。

双路由范式与路由类型

Next.js 维护着两套完整的路由系统:旧版 Pages Router(pages/)和新版 App Router(app/),两者可在同一应用中共存。框架通过 RouteKind 枚举来区分路由所属的范式:

export const enum RouteKind {
  PAGES = 'PAGES',
  PAGES_API = 'PAGES_API',
  APP_PAGE = 'APP_PAGE',
  APP_ROUTE = 'APP_ROUTE',
  IMAGE = 'IMAGE',
}
flowchart TD
    Request["Incoming Request"] --> RouteResolution["Route Resolution"]
    RouteResolution --> PAGES["PAGES\n(pages/*.tsx)"]
    RouteResolution --> PAGES_API["PAGES_API\n(pages/api/*.ts)"]
    RouteResolution --> APP_PAGE["APP_PAGE\n(app/**/page.tsx)"]
    RouteResolution --> APP_ROUTE["APP_ROUTE\n(app/**/route.ts)"]
    RouteResolution --> IMAGE["IMAGE\n(/_next/image)"]

这个枚举无处不在——在构建系统中决定用哪个模板包裹用户代码,在服务端将请求分发到对应处理器,在缓存系统中应用正确的失效策略。凡是看到代码中对 routeKind 的判断,几乎都是在 Pages Router 和 App Router 行为之间做分支。

constants.ts 中的 AdapterOutputType 枚举与 RouteKind 对应,并额外包含 PRERENDERSTATIC_FILEMIDDLEWARE 等类型——它们代表 Vercel 等托管适配器所使用的部署输出类型。

打包器抽象:Webpack、Turbopack、Rspack

Next.js 支持三种打包器,可通过 CLI 参数或配置文件选择。Bundler 枚举定义了可选项:

export enum Bundler {
  Turbopack,
  Webpack,
  Rspack,
}

parseBundlerArgs 函数负责处理选择逻辑。在本次提交中,Turbopack 是默认选项——未指定任何参数时,第 74-76 行会返回 Bundler.Turbopack,并将 TURBOPACK 设为 'auto'。使用 Webpack 需要传入 --webpack 参数,Rspack 则通过 NEXT_RSPACK 环境变量或 next.config.js 启用。

flowchart TD
    CLI["CLI Arguments"] --> Parse["parseBundlerArgs()"]
    Env["Environment Variables\n(TURBOPACK, NEXT_RSPACK)"] --> Parse
    Config["next.config.js\n(experimental.rspack)"] --> Finalize["finalizeBundlerFromConfig()"]
    Parse --> Finalize
    Finalize --> Turbopack["Turbopack\n(Rust-based, default)"]
    Finalize --> Webpack["Webpack\n(legacy, --webpack)"]
    Finalize --> Rspack["Rspack\n(NEXT_RSPACK)"]

有一个重要的设计细节:Rspack 配置通过 next.config.js 设置,而该文件只在子进程中加载,不在主 CLI 进程中加载。这就是 finalizeBundlerFromConfig() 作为独立步骤存在的原因——它在配置加载完成后才被调用。源码中的注释对此坦率地写道:"Rspack is configured via next config which is chaotic."(Rspack 通过 next config 配置,这很混乱。)

三种打包器共享同一套构建流水线抽象——入口点收集、基于模板的代码生成,以及 manifest 输出。build/webpack-config.ts 是 Webpack 专属的配置工厂(约 2,760 行),Turbopack 使用 turbopack/crates/next-core 中的 Rust crate,Rspack 则位于 packages/next-rspack

配置系统概览

Next.js 配置通过 server/config.ts 中的 loadConfig() 加载。这个函数背后是一条出乎意料地复杂的处理流水线:

flowchart TD
    A["File Detection\n(next.config.js/mjs/ts)"] --> B["TypeScript Transpilation\n(if .ts)"]
    B --> C["Phase-Aware Execution\n(config can be a function)"]
    C --> D["Merge with Defaults\n(defaultConfig)"]
    D --> E["Zod Validation\n(config-schema.ts)"]
    E --> F["Normalization\n(images, i18n, rewrites)"]
    F --> G["NextConfigComplete\n(fully resolved)"]

配置文件通过 find-up 自动查找。如果是 .ts 文件,会先用 SWC 转译再执行。配置可以是对象,也可以是接收当前阶段参数(PHASE_DEVELOPMENT_SERVERPHASE_PRODUCTION_BUILD 等)的函数,从而实现按上下文条件配置。

config-shared.ts 中的类型定义区分了面向用户的 NextConfig(部分字段、可选)和内部使用的 NextConfigComplete(已应用所有默认值的完整配置)。ExperimentalConfig 类型尤为庞大,涵盖了从 PPR 到 Turbopack 文件系统缓存的所有实验性功能开关。

提示: 阅读访问 nextConfig 的代码时,注意它的类型是 NextConfig 还是 NextConfigComplete。前者某些字段可能为 undefined,后者则保证所有字段完整。大多数服务端代码使用的是 NextConfigComplete

constants 文件还定义了 manifest 文件名,它们是构建系统与运行时之间的契约:

export const BUILD_MANIFEST = 'build-manifest.json'
export const PAGES_MANIFEST = 'pages-manifest.json'
export const APP_PATHS_MANIFEST = 'app-paths-manifest.json'
export const PRERENDER_MANIFEST = 'prerender-manifest.json'
export const ROUTES_MANIFEST = 'routes-manifest.json'

这十余个 manifest 文件是构建系统与服务端运行时之间的主要接口。构建阶段生成它们,服务端通过读取这些文件来了解存在哪些路由、哪些页面已预渲染、需要提供哪些客户端 bundle,以及如何解析跨服务端/客户端边界的模块引用。

代码库导航指南

以下是后续文章将深入探讨的各关键路径:

路径 行数 职责
server/base-server.ts ~3,050 抽象 Server 类——请求处理流水线
server/app-render/app-render.tsx ~7,350 App Router 渲染引擎——RSC + 流式传输
build/index.ts ~4,330 构建编排器——编译 + 静态生成
build/webpack-config.ts ~2,930 面向 3 个编译器的 Webpack 配置工厂
client/components/app-router.tsx 客户端 router 根组件
client/components/router-reducer/ 类 Redux 的导航状态机
server/lib/router-server.ts 顶层请求路由器
server/lib/cache-handlers/ use cache 缓存处理器接口

代码库遵循一种固定模式:复杂子系统集中在单个大文件中(有时超过 3,000 行),而非拆散到多个小文件里。这是一种有意为之的权衡——将相关逻辑聚合在一起,便于在单个文件内理解完整的处理流程,代价是单个文件的可读性有所下降。

下一步

有了这张全局地图,我们就可以开始追踪真实的执行路径了。下一篇文章将从 CLI 调用入手,跟随 next dev 命令经历进程 fork、HTTP 服务器创建,以及层层嵌套的服务端架构——最终完整呈现一个 HTTP 请求穿越整条处理流水线的全过程。