《Vite 设计与实现》完整目录
第15章 SSR 与模块运行器
开篇引言
服务端渲染(Server-Side Rendering, SSR)是现代 Web 框架的核心能力。它要求同一套源代码既能在浏览器中运行,又能在 Node.js(或其他服务端运行时)中执行。这给构建工具带来了独特的挑战:如何在服务端高效地加载和执行 ESM 模块?如何保持与开发时 HMR 的联动?如何为不同的运行环境提供差异化的模块解析策略?
Vite 的 SSR 方案经历了从 ssrLoadModule 到 Module Runner 的演进。早期的 ssrLoadModule 是一个相对简单的模块加载器,而 Vite 6+ 引入的 Module Runner 则是一个完整的模块执行运行时,支持 HMR、source map、循环依赖处理等高级特性。
本章将从 ssr/ 目录和 module-runner/ 目录的源码出发,深入分析 Vite SSR 架构的设计与实现。
:::tip 本章要点
- 理解 Vite SSR 的整体架构与模块加载流程
- 深入
ssrTransform的 ESM 到运行时代码的转换机制 - 掌握 Module Runner 的模块获取、求值与缓存策略
- 分析
fetchModule的外部化决策逻辑 - 理解 SSR Manifest 在预加载优化中的作用
- 对比传统 SSR 加载与 Module Runner 的架构差异 :::
15.1 SSR 架构概览
15.1.1 SSR 配置体系
Vite 的 SSR 配置定义在 ssr/index.ts 中。该文件虽然简短,但其中的每个选项都对应着 SSR 构建的一个关键决策点:
export interface SSROptions {
noExternal?: string | RegExp | (string | RegExp)[] | true
external?: string[] | true
target?: SSRTarget // 'node' | 'webworker'
optimizeDeps?: SsrDepOptimizationConfig
resolve?: {
conditions?: string[]
externalConditions?: string[]
mainFields?: string[]
}
}
noExternal 和 external 是 SSR 配置中最关键的两个选项。它们控制了依赖的"外部化"策略:
- 外部化 (external):依赖由 Node.js 原生加载器处理,不经过 Vite 转换。性能最优,但失去 Vite 插件的转换能力。
- 内部化 (noExternal):依赖像项目源码一样经过 Vite 完整管线处理。适用于需要编译的依赖(如仅提供 ESM 的包、含 CSS 导入的组件库)。
target 选项决定了 SSR 产物的运行目标环境。当设为 'node' 时,package.json 的 browser 字段会被忽略;当设为 'webworker' 时(适用于 Cloudflare Workers 等环境),browser 字段会被尊重。
默认配置通过 resolveSSROptions 函数合并:
const _ssrConfigDefaults = Object.freeze({
target: 'node',
optimizeDeps: {},
} satisfies SSROptions)
export function resolveSSROptions(
ssr: SSROptions | undefined,
preserveSymlinks: boolean,
): ResolvedSSROptions {
const defaults = mergeWithDefaults(_ssrConfigDefaults, {
optimizeDeps: { esbuildOptions: { preserveSymlinks } },
})
return mergeWithDefaults(defaults, ssr ?? {})
}
15.1.2 SSR 模块加载流水线
从一个 SSR 框架的角度看,加载一个模块需要经过以下完整流程:
flowchart TB
A["框架请求模块<br/>runner.import('/src/App.vue')"] --> B["DevEnvironment.fetchModule"]
B --> C{"模块类型判断"}
C -->|"内置模块 (node:fs)"| D["externalize<br/>type: 'builtin'"]
C -->|"外部 URL (https://...)"| E["externalize<br/>type: 'network'"]
C -->|"裸模块标识符 (lodash)"| F["tryNodeResolve 解析"]
C -->|"项目源码 (./App.vue)"| G["transformRequest"]
F --> H{"解析成功?"}
H -->|"是"| I["externalize: file URL<br/>type: 'module' | 'commonjs'"]
H -->|"否"| J["抛出 ERR_MODULE_NOT_FOUND"]
G --> K["插件管线转换"]
K --> L["ssrTransform"]
L --> M["返回 FetchResult<br/>(code + sourceMap)"]
D --> N["Module Runner 求值"]
E --> N
I --> N
M --> N
N --> O["返回模块 exports"]
style G fill:#e3f2fd
style L fill:#fff3e0
style N fill:#e8f5e9
15.1.3 DevEnvironment 中的 SSR 集成
DevEnvironment(定义在 server/environment.ts)是 SSR 模块加载的服务端入口。它通过 fetchModule 方法桥接 Module Runner 和 Vite 的转换管线:
export class DevEnvironment extends BaseEnvironment {
mode = 'dev' as const
moduleGraph: EnvironmentModuleGraph
fetchModule(
id: string,
importer?: string,
options?: FetchFunctionOptions,
): Promise<FetchResult> {
return fetchModule(this, id, importer, {
...this._remoteRunnerOptions,
...options,
})
}
transformRequest(url: string): Promise<TransformResult | null> {
return transformRequest(this, url)
}
}
DevEnvironment 通过 HotChannel 暴露 fetchModule 调用接口,使远程 Module Runner 能够通过传输层发起模块请求:
this.hot.setInvokeHandler({
fetchModule: (id, importer, options) => {
return this.fetchModule(id, importer, options)
},
getBuiltins: async () => {
return this.config.resolve.builtins.map((builtin) =>
typeof builtin === 'string'
? { type: 'string', value: builtin }
: { type: 'RegExp', source: builtin.source, flags: builtin.flags },
)
},
})
15.2 fetchModule:外部化决策
15.2.1 决策流程
fetchModule(ssr/fetchModule.ts)是 SSR 模块加载的核心函数。它为每个模块请求做出"内部化还是外部化"的决策:
export async function fetchModule(
environment: DevEnvironment,
url: string,
importer?: string,
options: FetchModuleOptions = {},
): Promise<FetchResult> {
// 决策 1:内置模块直接外部化
if (
url.startsWith('data:') ||
isBuiltin(environment.config.resolve.builtins, url)
) {
return { externalize: url, type: 'builtin' }
}
// 决策 2:外部 URL 直接外部化(file:// 除外)
const isFileUrl = url.startsWith('file://')
if (isExternalUrl(url) && !isFileUrl) {
return { externalize: url, type: 'network' }
}
// 决策 3:裸模块标识符 -- Node 解析后外部化
if (!isFileUrl && importer && url[0] !== '.' && url[0] !== '/') {
const resolved = tryNodeResolve(url, importer, {
mainFields: ['main'],
conditions: externalConditions,
extensions: ['.js', '.cjs', '.json'],
dedupe,
preserveSymlinks,
// ...
})
if (!resolved) {
const err: any = new Error(
`Cannot find module '${url}' imported from '${importer}'`,
)
err.code = 'ERR_MODULE_NOT_FOUND'
throw err
}
const file = pathToFileURL(resolved.id).toString()
const type = isFilePathESM(resolved.id, environment.config.packageCache)
? 'module'
: 'commonjs'
return { externalize: file, type }
}
// 决策 4:项目源码 -- Vite 管线转换
url = unwrapId(url)
const mod = await environment.moduleGraph.ensureEntryFromUrl(url)
const cached = !!mod.transformResult
if (options.cached && cached) {
return { cache: true } // 告知 Runner 使用本地缓存
}
let result = await environment.transformRequest(url)
if (!result) {
throw new Error(`[vite] transform failed for module '${url}'.`)
}
if (options.inlineSourceMap !== false) {
result = inlineSourceMap(mod, result, options.startOffset)
}
// 移除 shebang
if (result.code[0] === '#')
result.code = result.code.replace(/^#!.*/, (s) => ' '.repeat(s.length))
return {
code: result.code,
file: mod.file,
id: mod.id!,
url: mod.url,
invalidate: !cached,
}
}
graph TD
A["模块请求 URL"] --> B{"data: 或 内置模块?"}
B -->|"是"| C["externalize (builtin)"]
B -->|"否"| D{"外部 URL?<br/>(非 file://)"}
D -->|"是"| E["externalize (network)"]
D -->|"否"| F{"裸模块标识符?<br/>(不以 . / / 开头)"}
F -->|"是"| G["tryNodeResolve"]
F -->|"否"| H["transformRequest<br/>(Vite 管线)"]
G -->|"解析成功"| I["externalize<br/>(module/commonjs)"]
G -->|"解析失败"| J["ERR_MODULE_NOT_FOUND"]
style C fill:#e8f5e9
style E fill:#e8f5e9
style I fill:#e8f5e9
style H fill:#fff3e0
15.2.2 ESM vs CJS 类型判断
外部化时需要准确判断模块是 ESM 还是 CJS:
const type = isFilePathESM(resolved.id, environment.config.packageCache)
? 'module'
: 'commonjs'
isFilePathESM 通过以下规则判断:
.mjs文件 -> ESM.cjs文件 -> CJS.js文件 -> 查找最近的package.json的type字段
这个类型信息传递给 Module Runner 后,Runner 会根据类型选择不同的导入方式:ESM 使用 import(),CJS 使用 require()(或兼容逻辑)。
15.2.3 缓存协商
fetchModule 支持一种客户端-服务端缓存协商机制:
if (options.cached && cached) {
return { cache: true }
}
当 Module Runner 认为某个模块可能未变化时,它会设置 options.cached = true。服务端检查模块的 transformResult 是否仍然有效(未被 HMR 失效),如果有效则返回 { cache: true },告知 Runner 继续使用本地缓存。这避免了每次模块请求都传输完整的代码和 source map。
15.3 SSR Transform
15.3.1 转换的必要性
浏览器环境的 ESM 代码不能直接在 AsyncFunction 中执行,原因在于:
import/export语法在函数体内是语法错误import.meta在非模块上下文中不可用- 动态
import()的基准 URL 不正确
ssrTransform(ssr/ssrTransform.ts)的任务就是将 ESM 语法转换为可在 AsyncFunction 中执行的等价代码,同时保持 ESM 的语义特性(如 live binding、提升行为等)。
15.3.2 运行时协议
转换后的代码通过一组运行时函数与 Module Runner 交互:
| 运行时函数 | 作用 |
|---|---|
__vite_ssr_import__(source) | 替代 import 声明,返回模块 namespace |
__vite_ssr_dynamic_import__(source) | 替代 import() 表达式 |
__vite_ssr_exports__ | 替代模块的 exports 对象 |
__vite_ssr_exportAll__(obj) | 替代 export * from |
__vite_ssr_exportName__(name, getter) | 注册具名导出(带 getter 实现 live binding) |
__vite_ssr_import_meta__ | 替代 import.meta |
15.3.3 转换规则详解
ssrTransformScript 分为三个主要阶段:
阶段一:导入处理
graph LR
subgraph "输入 (ESM)"
A1["import { foo } from './mod'"]
A2["import * as ns from './lib'"]
A3["import def from './default'"]
A4["export { bar } from './re'"]
A5["export * from './all'"]
end
subgraph "输出 (Runtime)"
B1["const __ssr_import_0__ = await __vite_ssr_import__('./mod')"]
B2["const __ssr_import_1__ = await __vite_ssr_import__('./lib')"]
B3["const __ssr_import_2__ = await __vite_ssr_import__('./default')"]
B4["const __ssr_import_3__ = await __vite_ssr_import__('./re')"]
B5["const __ssr_import_4__ = await __vite_ssr_import__('./all')"]
end
A1 --> B1
A2 --> B2
A3 --> B3
A4 --> B4
A5 --> B5
每个 import 声明被转换为对 __vite_ssr_import__ 的 await 调用。defineImport 函数负责生成转换后的代码:
function defineImport(index, importNode, metadata) {
const source = importNode.source.value
deps.add(source)
// 精简 metadata -- 默认值不传递以减小体积
const metadataArg =
(metadata?.importedNames?.length ?? 0) > 0
? `, ${JSON.stringify(metadata)}`
: ''
const importId = `__vite_ssr_import_${uid++}__`
const transformedImport = `const ${importId} = await ${ssrImportKey}(${
JSON.stringify(source)
}${metadataArg});\n`
s.update(importNode.start, importNode.end, transformedImport)
if (importNode.start === index) {
hoistIndex = importNode.end // 保持顺序
} else {
s.move(importNode.start, importNode.end, index) // 提升到顶部
}
return importId
}
关键细节:metadata.importedNames 携带了导入的具体名称(如 ['foo']),允许 Module Runner 进行更精确的加载优化。当 metadata 全为默认值时,省略参数以减小传输体积。
导入语句的提升(hoist)行为模拟了 ESM 规范中导入声明的提升语义 -- 无论 import 写在代码的哪个位置,它都会在模块执行前被处理。
阶段二:导出处理
function defineExport(name, local = name) {
// 使用 getter 实现 live binding
s.appendLeft(
fileStartIndex,
`${ssrExportNameKey}(${JSON.stringify(name)}, () => {
try { return ${local} } catch {}
});\n`,
)
}
导出使用 Object.defineProperty 的 getter 形式注册,实现了 ESM 的 live binding 语义:当导出变量在源模块中被修改时,导入该变量的其他模块能够感知到变化。try/catch 包裹是为了处理循环依赖场景 -- 当被引用的变量尚未初始化时(处于 TDZ),优雅地返回 undefined 而非抛出 ReferenceError。
不同类型的导出有不同的处理:
// export function foo() {} --> 保留函数声明,注册导出名
defineExport(node.declaration.id!.name)
// export const a = 1, b = 2 --> 提取所有声明名,逐一注册
for (const decl of declaration.declarations) {
const names = extractNames(decl.id)
for (const name of names) {
defineExport(name)
}
}
// export default expression --> 创建中间变量
const name = `__vite_ssr_export_default__`
s.update(node.start, node.start + 14, `const ${name} =`)
defineExport('default', name)
// export * from './foo' --> 整体导出
s.appendLeft(node.end, `${ssrExportAllKey}(${importId});\n`)
阶段三:引用重写
这是最复杂的阶段。转换需要遍历整个 AST,将所有引用导入绑定的标识符替换为对应的属性访问:
walk(ast, {
onIdentifier(id, parent, parentStack) {
const binding = idToImportMap.get(id.name)
if (!binding) return
if (isStaticProperty(parent) && parent.shorthand) {
// 对象简写属性: { foo } -> { foo: __import_x__.foo }
if (!isNodeInPattern(parent) ||
isInDestructuringAssignment(parent, parentStack)) {
s.appendLeft(id.end, `: ${binding}`)
}
} else if (parent.type === 'CallExpression') {
// 方法调用: foo() -> (0, __import_x__.foo)()
s.update(id.start, id.end, binding)
s.prependRight(id.start, `(0,`)
s.appendLeft(id.end, `)`)
} else if (parent.type === 'PropertyDefinition' &&
parentStack[1]?.type === 'ClassBody') {
// 类字段初始化器中的引用需要提升为局部变量
if (!declaredConst.has(id.name)) {
declaredConst.add(id.name)
const topNode = parentStack[parentStack.length - 2]
s.prependRight(topNode.start, `const ${id.name} = ${binding};\n`)
}
} else {
s.update(id.start, id.end, binding)
}
},
onImportMeta(node) {
s.update(node.start, node.end, ssrImportMetaKey)
},
onDynamicImport(node) {
s.update(node.start, node.start + 6, ssrDynamicImportKey)
},
})
15.3.4 作用域分析
walk 函数实现了完整的 JavaScript 作用域分析,确保只重写真正引用了导入绑定的标识符。它需要正确处理以下场景:
flowchart TB
A["遍历 AST 节点"] --> B{"节点类型?"}
B -->|"FunctionDeclaration"| C["将函数名注册到父级作用域"]
B -->|"FunctionExpression (有名)"| D["将函数名注册到自身作用域"]
B -->|"VariableDeclarator (var)"| E["注册到最近的函数级作用域"]
B -->|"VariableDeclarator (let/const)"| F["注册到最近的块级作用域"]
B -->|"ClassDeclaration"| G["将类名注册到父级作用域"]
B -->|"CatchClause"| H["将参数注册到 catch 作用域"]
B -->|"函数参数"| I["注册到函数自身作用域"]
B -->|"Identifier"| J{"是否在当前作用域中?"}
J -->|"是"| K["跳过 (被本地声明遮蔽)"]
J -->|"否"| L{"是否引用导入绑定?"}
L -->|"是"| M["重写为属性访问"]
L -->|"否"| N["跳过"]
作用域信息存储在 WeakMap<Node, Set<string>> 中,var 声明和 let/const 声明使用不同的作用域查找策略:
function findParentScope(
parentStack: ESTree.Node[],
isVar = false,
): ESTree.Node | undefined {
return parentStack.find(isVar ? isFunction : isBlock)
}
var 声明提升到函数级作用域,而 let/const 限定在块级作用域。这种区分确保了转换后代码的语义与原始 ESM 代码一致。
15.3.5 方法调用的 this 解绑
当导入的函数被作为方法调用时,需要特殊处理 this 绑定:
// 原始代码
import { foo } from './mod'
foo()
// 直接转换(错误)
__vite_ssr_import_0__.foo()
// 此时 foo 内的 this 指向 __vite_ssr_import_0__ 对象
// 正确转换
;(0, __vite_ssr_import_0__.foo)()
// 逗号表达式使 foo 成为独立值,this 变为 undefined (strict) 或 globalThis
(0, expr) 是一个经典的 JavaScript 技巧,广泛用于 Babel、TypeScript 等编译器中。逗号表达式的结果是最后一个操作数的值,但作为方法调用时,this 不再绑定到属性所在的对象。
15.3.6 JSON 模块的特殊处理
对于 JSON 请求,ssrTransform 有一条快速路径:
async function ssrTransformJSON(code, inMap) {
return {
code: code.replace('export default', `${ssrModuleExportsKey}.default =`),
map: inMap,
deps: [],
dynamicDeps: [],
ssr: true,
}
}
JSON 模块只有一个默认导出,不需要完整的 AST 分析,简单的字符串替换即可完成转换。
15.4 Module Runner
15.4.1 架构概览
Module Runner(module-runner/)是 Vite 的模块执行运行时。它运行在服务端或任何非浏览器环境中,负责加载和执行经过 ssrTransform 转换的代码。
graph TB
subgraph "Module Runner 进程"
A["ModuleRunner"] --> B["EvaluatedModules<br/>(模块缓存图)"]
A --> C["NormalizedTransport<br/>(通信层)"]
A --> D["ESModulesEvaluator<br/>(代码求值器)"]
A --> E["HMRClient<br/>(热更新客户端)"]
end
subgraph "Vite Dev Server 进程"
F["DevEnvironment"] --> G["PluginContainer"]
F --> H["EnvironmentModuleGraph"]
F --> I["fetchModule"]
end
C <-->|"invoke('fetchModule', [url, importer])"| I
C <-->|"HMR 消息 (update/full-reload)"| F
style A fill:#e8f5e9
style F fill:#e3f2fd
15.4.2 ModuleRunner 初始化
export class ModuleRunner {
public evaluatedModules: EvaluatedModules
public hmrClient?: HMRClient
private readonly transport: NormalizedModuleRunnerTransport
constructor(
public options: ModuleRunnerOptions,
public evaluator: ModuleEvaluator = new ESModulesEvaluator(),
) {
this.evaluatedModules = options.evaluatedModules ?? new EvaluatedModules()
this.transport = normalizeModuleRunnerTransport(options.transport)
// 初始化 HMR 客户端
if (options.hmr !== false) {
this.hmrClient = new HMRClient(
resolvedHmrLogger,
this.transport,
({ acceptedPath }) => this.import(acceptedPath),
)
if (!this.transport.connect) {
throw new Error(
'HMR is not supported by this runner transport',
)
}
this.transport.connect(createHMRHandlerForRunner(this))
}
// Source Map 支持
if (options.sourcemapInterceptor !== false) {
this.resetSourceMapSupport = enableSourceMapSupport(this)
}
}
}
初始化过程的三个关键组件:
- Transport:抽象的通信层,可以是 WebSocket、HTTP、或进程间通信
- HMRClient:与浏览器端的 HMR 客户端共享同一套
HMRClient实现 - Source Map:通过拦截
Error.prepareStackTrace(Node.js)实现源码级的堆栈追踪
15.4.3 模块加载流程
当调用 runner.import(url) 时,触发一个多阶段的加载流程:
sequenceDiagram
participant App as 应用代码
participant Runner as ModuleRunner
participant Cache as EvaluatedModules
participant Transport as Transport
participant Server as DevServer
App->>Runner: import(url)
Runner->>Runner: cachedModule(url)
Runner->>Cache: getModuleByUrl(url)
alt 缓存命中且有效
Cache-->>Runner: 返回已缓存的 EvaluatedModuleNode
else 需要从服务端获取
Runner->>Transport: invoke('fetchModule', [url, importer, options])
Transport->>Server: fetchModule(environment, url, importer)
Server-->>Transport: FetchResult
Transport-->>Runner: 模块信息
Runner->>Cache: ensureModule(id, url)
end
Runner->>Runner: cachedRequest(url, mod, callstack)
alt 已求值且无循环
Runner-->>App: 返回 mod.exports
else 需要求值
Runner->>Runner: directRequest(url, mod, callstack)
Note over Runner: 构建运行时上下文
Runner->>Runner: evaluator.runInlinedModule(context, code)
Runner-->>App: 返回 exports
end
15.4.4 循环依赖处理
循环依赖是模块系统中最棘手的问题之一。Module Runner 实现了三级检测策略:
private async cachedRequest(url, mod, callstack = [], metadata) {
const moduleId = mod.meta!.id
const { importers } = mod
const importee = callstack[callstack.length - 1]
if (importee) importers.add(importee)
// 快速路径:已完全求值的模块不会死锁
if (mod.evaluated && mod.promise) {
return this.processImport(await mod.promise, mod.meta!, metadata)
}
// 三级循环检测
if (
callstack.includes(moduleId) || // 1. 调用栈直接检测
this.isCircularModule(mod) || // 2. 直接循环检测
this.isCircularImport(importers, moduleId) // 3. 传递性循环检测
) {
if (mod.exports)
return this.processImport(mod.exports, mod.meta!, metadata)
}
// 正常求值
try {
if (mod.promise) return this.processImport(await mod.promise, mod.meta!, metadata)
const promise = this.directRequest(url, mod, callstack)
mod.promise = promise
mod.evaluated = false
return this.processImport(await promise, mod.meta!, metadata)
} finally {
mod.evaluated = true
}
}
三级检测的具体实现:
// 第 1 级:直接循环 -- A -> B -> A
// callstack.includes(moduleId) 即可检测
// 第 2 级:模块级循环 -- 模块的导入者同时也是它的依赖
private isCircularModule(mod: EvaluatedModuleNode) {
for (const importedFile of mod.imports) {
if (mod.importers.has(importedFile)) return true
}
return false
}
// 第 3 级:传递性循环 -- A -> B -> C -> A
private isCircularImport(importers, moduleUrl, visited = new Set()) {
for (const importer of importers) {
if (visited.has(importer)) continue
visited.add(importer)
if (importer === moduleUrl) return true
const mod = this.evaluatedModules.getModuleById(importer)
if (mod?.importers.size &&
this.isCircularImport(mod.importers, moduleUrl, visited)) {
return true
}
}
return false
}
当检测到循环时,返回目标模块当前已有的 exports(可能尚未完全初始化)。这与 Node.js 的 CJS 模块加载器行为一致:循环依赖中,后加载的模块拿到的是部分初始化的 exports 对象。
15.4.5 ESModulesEvaluator
ESModulesEvaluator 是默认的模块求值器,使用 AsyncFunction 构造器执行代码:
export class ESModulesEvaluator implements ModuleEvaluator {
public readonly startOffset: number =
getAsyncFunctionDeclarationPaddingLineCount()
async runInlinedModule(context, code): Promise<any> {
const initModule = new AsyncFunction(
ssrModuleExportsKey, // __vite_ssr_exports__
ssrImportMetaKey, // __vite_ssr_import_meta__
ssrImportKey, // __vite_ssr_import__
ssrDynamicImportKey, // __vite_ssr_dynamic_import__
ssrExportAllKey, // __vite_ssr_exportAll__
ssrExportNameKey, // __vite_ssr_exportName__
'"use strict";' + code,
)
await initModule(
context[ssrModuleExportsKey],
context[ssrImportMetaKey],
context[ssrImportKey],
context[ssrDynamicImportKey],
context[ssrExportAllKey],
context[ssrExportNameKey],
)
Object.seal(context[ssrModuleExportsKey])
}
runExternalModule(filepath: string): Promise<any> {
return import(filepath)
}
}
设计要点:
AsyncFunction而非vm模块:vm.Module是 Node.js 特有的 API,而AsyncFunction在 Deno、Bun、Cloudflare Workers 等运行时中都可用"use strict"前缀:ESM 规范要求模块始终在严格模式下执行Object.seal密封 exports:模拟 ESM 的不可变 namespace 特性startOffset:AsyncFunction构造器生成的函数声明占据额外的行,startOffset记录了这个偏移量以便修正 source map
15.4.6 directRequest 与运行时上下文
directRequest 方法为每个模块构建完整的执行上下文:
protected async directRequest(url, mod, _callstack) {
const fetchResult = mod.meta!
const moduleId = fetchResult.id
const callstack = [..._callstack, moduleId]
// 构建 __vite_ssr_import__ 函数
const request = async (dep, metadata) => {
const importer = ('file' in fetchResult && fetchResult.file) || moduleId
const depMod = await this.cachedModule(dep, importer)
depMod.importers.add(moduleId)
mod.imports.add(depMod.id)
return this.cachedRequest(dep, depMod, callstack, metadata)
}
// 构建 __vite_ssr_dynamic_import__ 函数
const dynamicRequest = async (dep) => {
dep = String(dep)
if (dep[0] === '.') {
dep = posixResolve(posixDirname(url), dep)
}
return request(dep, { isDynamicImport: true })
}
// 外部化模块使用原生 import
if ('externalize' in fetchResult) {
const exports = await this.evaluator.runExternalModule(externalize)
mod.exports = exports
return exports
}
// 构建 import.meta 并注入 hot 属性
const meta = await createImportMeta(modulePath)
if (this.hmrClient) {
Object.defineProperty(meta, 'hot', {
enumerable: true,
get: () => {
hotContext ||= new HMRContext(this.hmrClient, mod.url)
return hotContext
},
})
}
// 组装完整上下文
const exports = Object.create(null)
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module', enumerable: false, configurable: false,
})
mod.exports = exports
const context = {
[ssrImportKey]: request,
[ssrDynamicImportKey]: dynamicRequest,
[ssrModuleExportsKey]: exports,
[ssrExportAllKey]: (obj) => exportAll(exports, obj),
[ssrExportNameKey]: (name, getter) =>
Object.defineProperty(exports, name, {
enumerable: true, configurable: true, get: getter,
}),
[ssrImportMetaKey]: meta,
}
await this.evaluator.runInlinedModule(context, code, mod)
return exports
}
注意 exports 对象在模块执行前就被赋给 mod.exports,这使得循环依赖中的部分导出能被其他模块访问到。
15.5 SSR Manifest
15.5.1 预加载映射
SSR Manifest 插件(ssr/ssrManifestPlugin.ts)在构建时生成从模块 ID 到预加载资源的映射。框架利用这个映射在服务端渲染时注入正确的 <link> 预加载标签:
generateBundle(_options, bundle) {
const ssrManifest = getSsrManifest(this)
for (const file in bundle) {
const chunk = bundle[file]
if (chunk.type === 'chunk') {
for (const id in chunk.modules) {
const normalizedId = normalizePath(relative(config.root, id))
const mappedChunks =
ssrManifest[normalizedId] ?? (ssrManifest[normalizedId] = [])
if (!chunk.isEntry) {
mappedChunks.push(joinUrlSegments(base, chunk.fileName))
chunk.viteMetadata!.importedCss.forEach((file) => {
mappedChunks.push(joinUrlSegments(base, file))
})
}
chunk.viteMetadata!.importedAssets.forEach((file) => {
mappedChunks.push(joinUrlSegments(base, file))
})
}
}
}
this.emitFile({
fileName: '.vite/ssr-manifest.json',
type: 'asset',
source: JSON.stringify(sortObjectKeys(ssrManifest), undefined, 2),
})
}
15.5.2 动态导入的 CSS 追踪
SSR Manifest 还需要处理动态导入 chunk 中 __vitePreload 引用的 CSS 依赖。插件通过解析 chunk 代码中的导入语句,递归追踪所有关联的 CSS 文件:
flowchart TB
A["检测 chunk 中的 __vitePreload"] --> B["es-module-lexer 解析动态导入"]
B --> C["获取每个导入的 URL"]
C --> D["解析为 bundle 中的文件名"]
D --> E["递归追踪 importedCss"]
E --> F{"还有子导入?"}
F -->|"是"| E
F -->|"否"| G["收集所有 CSS deps"]
G --> H["写入 ssrManifest"]
const addDeps = (filename: string) => {
if (filename === ownerFilename) return // 避免自引用
if (analyzed.has(filename)) return // 避免重复
analyzed.add(filename)
const chunk = bundle[filename] as OutputChunk | undefined
if (chunk) {
chunk.viteMetadata!.importedCss.forEach((file) => {
deps.push(joinUrlSegments(base, file))
})
chunk.imports.forEach(addDeps) // 递归追踪
}
}
这种递归追踪确保了即使 CSS 依赖嵌套在多层 chunk 导入链中,也能被正确地包含在 SSR Manifest 中。
15.6 Source Map 处理
fetchModule 中的 inlineSourceMap 函数将 source map 内联到模块代码中:
function inlineSourceMap(mod, result, startOffset) {
const map = result.map
let code = result.code
if (!map || !('version' in map) ||
code.includes(MODULE_RUNNER_SOURCEMAPPING_SOURCE))
return result
// 移除其他 source map(只保留 Vite 的)
if (OTHER_SOURCE_MAP_REGEXP.test(code))
code = code.replace(OTHER_SOURCE_MAP_REGEXP, '')
// 补偿 AsyncFunction 引入的行偏移
const sourceMap = startOffset
? Object.assign({}, map, {
mappings: ';'.repeat(startOffset) + map.mappings,
})
: map
result.code = `${code.trimEnd()}\n` +
`//# sourceURL=${mod.id}\n` +
`${MODULE_RUNNER_SOURCEMAPPING_SOURCE}\n` +
`//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(sourceMap)}\n`
return result
}
startOffset 的作用至关重要。ESModulesEvaluator 使用 new AsyncFunction(params, code) 创建函数,这个函数声明本身会占据一些行(如 async function anonymous(param1, param2, ...) {)。Source map 的行映射必须加上这个偏移量才能正确指向源代码。
15.7 设计决策分析
15.7.1 为什么不直接用 Node.js ESM Loader
Node.js 提供了自定义 ESM Loader(--loader / --import)的能力,但 Vite 选择实现独立的 Module Runner,原因包括:
- HMR 需求:Node.js ESM 规范不允许模块被重新求值。Module Runner 通过
invalidateModule+ 重新执行实现热更新 - 插件集成:Vue SFC、TypeScript 等需要 Vite 插件管线处理,Node.js Loader 难以完整集成
- 跨运行时:
AsyncFunction方案在 Node.js、Deno、Bun、Cloudflare Workers 中均可工作 - 精确控制:Module Runner 拥有完整的模块图,能实现精确的缓存失效和循环依赖处理
- Source Map:通过内联和行偏移补偿提供准确的错误定位
15.7.2 exports 密封设计
Object.seal(context[ssrModuleExportsKey])
Object.seal 在模块求值后禁止添加新属性,模拟了 ESM namespace 的静态特性。但已有属性的 getter 仍可返回变化后的值,保持了 live binding 语义。这是一个巧妙的平衡:防止意外的属性添加,同时允许导出值的合法变化。
15.7.3 传输层抽象
Module Runner 通过 NormalizedModuleRunnerTransport 抽象通信层,使得它不依赖于特定的 IPC 机制。同一个 Module Runner 可以通过 WebSocket、HTTP、或内存通道与 DevServer 通信。这种抽象使得 Module Runner 可以运行在与 DevServer 相同或不同的进程中。
graph LR
A["ModuleRunner"] --> B["NormalizedTransport"]
B --> C["WebSocket 通道<br/>(远程 Runner)"]
B --> D["内存通道<br/>(同进程 Runner)"]
B --> E["HTTP 通道<br/>(自定义部署)"]
C --> F["DevServer"]
D --> F
E --> F
15.8 小结
本章深入分析了 Vite SSR 架构的核心组件:
- fetchModule 实现了精确的外部化决策逻辑,通过四级判断(内置模块、外部 URL、裸模块、项目源码)确定每个模块的最优加载路径。缓存协商机制避免了不必要的代码传输。
- SSR Transform 将 ESM 语法转换为可在
AsyncFunction中执行的运行时代码,实现了完整的作用域分析、导入提升、导出 live binding、this解绑等语义保持。三阶段处理(导入、导出、引用重写)的设计清晰而高效。 - Module Runner 是一个完整的模块执行运行时,通过 Transport 抽象层与 DevServer 通信。它实现了三级循环依赖检测、
EvaluatedModules缓存图、HMR 热更新支持以及精确的 source map 行偏移补偿。 - SSR Manifest 在构建时生成模块到资源的映射,包括递归追踪动态导入链中的 CSS 依赖,使 SSR 框架能够注入完整的预加载标签。
Vite SSR 架构的核心设计哲学是"选择性介入":项目源码经过完整管线处理以获得 HMR 和转换支持,node_modules 依赖尽可能外部化以获得最优性能。Module Runner 的跨运行时设计(基于 AsyncFunction 而非 vm)和传输层抽象,使得 Vite 的 SSR 方案能够适应从传统 Node.js 到 Edge Runtime 的多种部署场景。