发布日期

第 03 讲|源码主入口与执行流(dev / build / start 三条线)

梳理 dev / build / start 三条执行主线的源码入口与流程走向

阶段一:框架认知与源码导航 · 第 3 / 40 讲 难度:⭐⭐⭐ · 预计耗时:2 小时

学习目标

  • 能默写 next dev / next build / next start 三条命令对应的源码调用栈。
  • 理解 Next.js 的"父子进程"模型:CLI 进程、router-server、render-server 各自的职责。
  • 能在 app-render.tsx 入口处打断点,看到一次完整请求是怎么走过来的。
  • 区分清楚 Server(抽象) / NextNodeServer(生产 Node) / DevServer(开发) 的继承关系。

1. CLI 总入口:bin/next

所有命令都从一个二进制脚本开始:

  • 源码:packages/next/src/bin/next.ts(编译产物:packages/next/dist/bin/next.js
  • 包:packages/nextpackage.json 里通过 "bin": { "next": "dist/bin/next.js" } 暴露给 npm(详见上一讲)。

它做的事很简单:解析 process.argv[2] 取子命令名(dev / build / start / export / ...),require 对应的 cli/next-*.ts 文件并把剩余 argv 传过去。packages/next/src/cli/ 下你能看到一一对应:

src/cli/
├── next-dev.ts          ← next dev
├── next-build.ts        ← next build
├── next-start.ts        ← next start
├── next-export.ts       ← next export
├── next-info.ts         ← next info
├── next-telemetry.ts    ← next telemetry
├── next-test.ts         ← next test (实验)
├── next-typegen.ts      ← next typegen
└── next-upgrade.ts      ← next upgrade

下面我们重点拆 dev / build / start 这三条主线。

2. next dev:开发服务器

2.1 一行调用图

bin/next               (CLI 路由)
  └─ cli/next-dev.ts                 (父进程 - watcher / restart 逻辑)
       └─ fork()  →  子进程                ← 真正跑服务的进程
              └─ server/lib/start-server.ts
                   └─ server/lib/router-server.ts  (initialize)
                        ├─ setupFsCheck          (静态文件 / public 索引)
                        ├─ setupDevBundler       (webpack/turbopack/rspack 选择)
                        ├─ render-server.ts      (initialize - 起渲染 worker)
                        └─ requestListener       (HTTP server 真正绑定)

2.2 关键源码

第 1 跳:CLI 解析与子进程 fork

next-dev.ts 的关键能力是「自重启」:当你修改 next.config.js.env 或 server 内存阈值触发时,它需要重启子进程;为此它在父进程里只做监控,真正跑服务的是 child_process.fork() 出来的子进程。

43:packages/next/src/cli/next-dev.ts
import { initialEnv } from '@next/env'
import { fork } from 'child_process'
import type { ChildProcess } from 'child_process'
import {
  getReservedPortExplanation,
  isPortIsReserved,
} from '../lib/helpers/get-reserved-port'
import { getCacheDirectory } from '../lib/helpers/get-cache-directory'
import { getGitBranch } from '../lib/helpers/git'
import os from 'os'
import fs from 'node:fs'
import { once } from 'node:events'
import { clearTimeout } from 'timers'
import { trace, initializeTraceState, exportTraceState } from '../trace'
import { traceId } from '../trace/shared'
import { Bundler, parseBundlerArgs } from '../lib/bundler'

父子进程是 dev 模式独有的设计——next start 不 fork,因为它不需要自重启。

第 2 跳:start-server 起 HTTP 监听

子进程 require start-server.ts,由它在 Node 中创建真正的 http.createServer()

213:packages/next/src/server/lib/start-server.ts
export async function startServer(
  serverOptions: StartServerOptions
): Promise<StartServerResult> {
  const {
    dir,
    isDev,
    hostname,
    minimalMode,
    allowRetry,
    keepAliveTimeout,
    selfSignedCertificate,
    serverFastRefresh,
  } = serverOptions
  let { port } = serverOptions

  process.title = `next-server (v${process.env.__NEXT_VERSION})`
  let handlersReady = () => {}
  let handlersError = () => {}

  let handlersPromise: Promise<void> | undefined = new Promise<void>(
    (resolve, reject) => {
      handlersReady = resolve
      handlersError = reject
    }
  )
  let requestHandler: WorkerRequestHandler = async (
    req: IncomingMessage,
    res: ServerResponse
  ): Promise<void> => {
    // ... defer until handlers are ready
  }

注意 requestHandler 一开始是个 placeholder(异步等待 handlersPromise)——这是为了让 socket 尽早 listen,避免端口被其他进程抢占;真正的 handler 在 router-server.initialize() 完成后被赋值。

第 3 跳:router-server 初始化

router-server.ts 是 dev/start 共用的"接请求中心"。initialize() 是其入口:

114:packages/next/src/server/lib/router-server.ts
export async function initialize(opts: {
  // ... options
}) {
  // ...
  let experimentalFeatures: ConfiguredExperimentalFeature[] = []
  const config = await loadConfig(
    opts.dev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_SERVER,
    opts.dir,
    {
      silent: false,
      reportExperimentalFeatures(features) {
        experimentalFeatures = features.toSorted(({ key: a }, { key: b }) =>
          a.localeCompare(b)
        )
      },
    }
  )

它依次做:

  1. loadConfig() 加载 next.config.js
  2. setupFsCheck() 扫描静态资源、public/
  3. (dev 才有)setupDevBundler() 起 webpack/turbopack/rspack;
  4. render-server.initialize() 起渲染服务;
  5. 返回一个 requestHandler,挂回 start-server 的 placeholder。

第 4 跳:render-server → BaseServer

render-server.ts 内会实例化 NextNodeServer(生产)或 DevServer(开发):

179:packages/next/src/server/next-server.ts
export default class NextNodeServer extends BaseServer<
  Options,
  NodeNextRequest,
  NodeNextResponse
> {
321:packages/next/src/server/base-server.ts
export default abstract class Server<
  ServerOptions extends Options = Options,
  ServerRequest extends BaseNextRequest = BaseNextRequest,
  ServerResponse extends BaseNextResponse = BaseNextResponse,
> {

注意:base-server.ts 里类名其实是 Server,但在 next-server.ts 里被 import as BaseServer。这是 Next.js 一个很容易混淆的命名陷阱

  • 文件 base-server.tsexport default abstract class Server ← 抽象基类
  • 文件 next-server.tsexport default class NextNodeServer extends BaseServer
  • 文件 dev/next-dev-server.tsexport default class DevServer extends Server

"Server" / "NextServer" / "NextNodeServer" / "DevServer" / "BaseServer" 这些词在源码里各有所指,看到时先确认是从哪个文件 import 的,否则会迷路。

2.3 dev 特有:HMR、on-demand entries

dev 模式比 start 多两块逻辑:

  • on-demand entriessrc/server/dev/on-demand-entry-handler.ts):路由没被访问过时不编译,访问时再触发 webpack 增量编译。
  • HMRsrc/server/dev/hot-reloader-*.ts):监听文件变更,通过 WebSocket 向 _next/webpack-hmr 推送增量。

第 34 讲会专门讲 dev 调试体系。

3. next build:构建管线

3.1 一行调用图

bin/next
  └─ cli/next-build.ts
       └─ build/index.ts             ← 主入口(4000+ 行的大块头)
            ├─ loadConfig + nextBuildSpan.trace(...)    (trace 全程)
            ├─ collectPages                              (扫 app/ + pages/)
            ├─ runWebpackCompiler / runTurbopackBuild    (3 套 compiler 并行/串行)
            ├─ writeManifests                            (12+ 个 manifest 落盘)
            ├─ generateStaticPages (worker)             (调用 worker.ts,并行预渲染)
            ├─ collectBuildTraces                        (生成 .nft.json)
            └─ printTreeView                             (终端表格)

3.2 关键源码

入口next-build.ts 主要做 flag 校验,最后调用:

8:packages/next/src/cli/next-build.ts
#!/usr/bin/env node

import { saveCpuProfile } from '../server/lib/cpu-profile'
import { existsSync } from 'fs'
import { italic } from '../lib/picocolors'
import build from '../build'
import { warn } from '../build/output/log'
import { printAndExit } from '../server/lib/utils'

import build from '../build' 这一行进入了 build/index.ts —— 这是整个构建管线的"心脏",光这一个文件就有 4000+ 行。

worker 化静态生成:build 阶段会 fork 多个 worker 进程同时跑 getStaticPaths / generateStaticParams / generateMetadata,源码 src/build/worker.ts + src/server/dev/static-paths-worker.ts

这一讲不展开 build,阶段四(第 23–28 讲)会专门拆 build 流程

4. next start:生产服务器

4.1 一行调用图

bin/next
  └─ cli/next-start.ts
       └─ server/lib/start-server.ts  (与 dev 共用!isDev=false)
            └─ router-server.ts initialize
                 └─ render-server.ts initialize
                      └─ NextNodeServer  (不是 DevServer)
                           └─ BaseServer  (公共逻辑)

next-start.tsnext-dev.ts 短得多——94 行,只解析 port/hostname、设置 inspector,最后直接 await startServer({ isDev: false })

91:packages/next/src/cli/next-start.ts
const nextStart = async (options: NextStartOptions, directory?: string) => {
  const dir = getProjectDir(directory)
  const hostname = options.hostname
  const inspect = options.inspect
  const port = options.port
  const keepAliveTimeout = options.keepAliveTimeout

  if (isPortIsReserved(port)) {
    printAndExit(getReservedPortExplanation(port), 1)
  }
  // ... inspector setup ...

  await startServer({
    dir,
    isDev: false,
    hostname,
    port,
    keepAliveTimeout,
  })
}

4.2 dev vs start 的"分叉点"

二者共用 start-server.tsrouter-server.ts 的入口,但分叉发生在两处:

  1. router-server.ts 内部opts.dev 决定是否调用 setupDevBundler、是否注入 HMR 路由。
  2. render-server.ts 内部:根据 dev flag 实例化 DevServer 还是 NextNodeServer

所以 dev / start 的源码差异其实非常少。这也是为什么 pnpm test-dev-turbopnpm test-start-turbo 测试同一份代码就能验证两种模式

5. 一次请求的"全栈"调用路径(App Router)

把以上整合起来,画一次 GET /products/123 的完整路径:

HTTP socket
http.createServer(requestListener)            ← start-server.ts:240
requestHandler  (placeholder → 真实 handler)   ← start-server.ts:209
router-server.ts  resolveRoutes               ← 检查 rewrite / redirect / public / API
NextNodeServer.handleRequest                  ← next-server.ts (继承 base-server.ts)
  ↓  匹配到 app/products/[id]/page.tsx
RouteModule (app-page module)                 ← route-modules/app-page/module.ts
app-render.tsx  renderToHTMLOrFlight          ← 真正的渲染入口
React renderToReadableStream + RSC payload    ← compiled/react-server-dom-webpack/
pipeReadable → http.ServerResponse           ← pipe-readable.ts
浏览器收到 HTML + 内嵌 RSC payload

这个图你要刻在脑子里——后面 35 讲都是在某一个箭头上深挖。

6. 实操:亲手打断点跑一遍

步骤 1:确保 packages/next 已构建

pnpm --filter=next build

步骤 2:起一个 fixture(用上一讲生成的也行)

cd test/e2e/lecture-01

步骤 3:用 inspect 模式启动

node --inspect-brk=9229 ../../../packages/next/dist/bin/next.js dev --port 3001

打开 Chrome chrome://inspect,attach 到 9229 端口。在以下文件里下断点:

  • dist/server/lib/start-server.js(对应 start-server.ts 第 240 行的 requestListener
  • dist/server/lib/router-server.js(initialize 入口)
  • dist/server/next-server.js(NextNodeServer 类)
  • dist/server/app-render/app-render.js(最终的渲染入口)

按 F8 继续执行直到 server ready,然后 curl http://localhost:3001/,观察依次命中哪些断点。

Tip:dist 目录是 build 出来的 JS(不是 TS),但行号和源码大致对应;如果开启了 source map(pnpm --filter=next build 默认开),DevTools 会直接显示原始 TS。

步骤 4:加一个观察日志

不想用 debugger 也可以直接打 log。在 packages/next/src/server/app-render/app-render.tsx 文件最上面加:

console.log("[app-render] start at", new Date().toISOString());

然后 pnpm --filter=next build(如果开了 watch mode 就不用),跑请求就能看到日志。这是最朴素也最有效的源码学习手段。

7. 重难点小结

7.1 "Server" 这个词的歧义

源码里至少有 4 种用法:

名字(import as)实际类文件
Server / BaseServerabstract class Serverserver/base-server.ts
NextNodeServerconcrete classserver/next-server.ts
DevServerconcrete classserver/dev/next-dev-server.ts
NextServer(外部 API)封装类server/next.ts
WebServer(Edge)concrete classserver/web-server.ts

每次看到 class XxxServer 都问自己一句:在哪个文件、继承自谁?

7.2 父进程 / 子进程 / worker 三层

父进程  next-dev (CLI)
  ├─ 子进程  next-server ()
  │    ├─ worker  static-paths-worker (build 时)
  │    ├─ worker  render-server worker(部分配置下)
  │    └─ worker  use-cache-probe-worker(实验)
  └─ 父进程发现子进程 crash → fork 重启

子进程是默认形态;某些配置(自定义 server、minimalMode)下渲染会进一步分到 worker 进程。

7.3 phase 这个隐藏参数

loadConfig(phase, ...) 的第一个参数:

  • PHASE_DEVELOPMENT_SERVER
  • PHASE_PRODUCTION_SERVER
  • PHASE_PRODUCTION_BUILD
  • PHASE_EXPORT
  • PHASE_TEST

next.config.js 可以导出函数 (phase) => config,根据 phase 返回不同配置。这是排查"我 dev 没事 build 失败"问题的重要线索。

8. 检验问题

  1. next devnext start 共用哪几个源码文件?分叉发生在何处?
  2. 为什么 next dev 要 fork 子进程?next start 为什么不 fork?
  3. BaseServer / NextNodeServer / DevServer 三者继承关系?分别在哪个文件?
  4. requestHandlerstart-server.ts 里为什么先是一个 placeholder 函数,等什么时机被替换?
  5. loadConfig(phase, ...) 的 phase 有几种?这与 next.config.js 导出函数有什么关系?
  6. App Router 一次请求从 socket 到 app-render.tsx 至少经过几层?哪一层负责路由匹配?
  7. 怎么用最少的代码改动在请求最早进入 Next.js 时打一个 console.log

9. 延伸阅读

  • packages/next/src/server/next.ts(对外暴露的 NextServer 封装类)
  • packages/next/src/server/lib/render-server.ts(render-server initialize 逻辑)
  • 仓库 AGENTS.md 的「Development Tips」与「NODE_ENV vs __NEXT_DEV_SERVER

下一讲预告

第 04 讲|本地开发环境与测试体系:把 pnpm --filter=next dev 的 watch 模式、4 种测试矩阵、new-test 模板生成、NEXT_SKIP_ISOLATE 等开发工具吃透,为后续阶段提供可重现的实验环境。