语言服务与 tsserver:驱动 IDE 体验的核心引擎
前置知识
- ›第 1-5 篇:完整的编译器流水线理解
- ›熟悉语言服务器协议(LSP)的基本概念
- ›了解编辑器与语言服务器之间的通信方式
语言服务与 tsserver:驱动 IDE 体验的核心引擎
每次你看到红色波浪线、触发自动补全、跳转到定义,或在编辑器中应用快速修复,背后运行的正是本文要探讨的代码。TypeScript 语言服务与 tsserver 共同构成了编译器核心与编辑器体验之间的桥梁——它们将解析、绑定和类型检查流水线封装为面向 IDE 的 API,并通过 JSON 协议对外暴露。
这一层在编译器核心之上额外引入了约 40,000 行代码,分为两个独立层次:LanguageService(编程式 API)和 tsserver 进程(wire 协议服务器)。理解这套架构,有助于我们搞清楚 TypeScript 是如何在运行完整类型检查的同时,仍能保持近乎即时的编辑器响应速度的。
LanguageService 接口与 createLanguageService
LanguageService 接口定义了编辑器功能的完整 API 契约:
export interface LanguageService {
getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[];
getSemanticDiagnostics(fileName: string): Diagnostic[];
getSuggestionDiagnostics(fileName: string): DiagnosticWithLocation[];
getCompletionsAtPosition(fileName, position, options?): CompletionInfo;
getCompletionEntryDetails(fileName, position, name, ...): CompletionEntryDetails;
getDefinitionAtPosition(fileName, position): DefinitionInfo[];
getTypeDefinitionAtPosition(fileName, position): DefinitionInfo[];
findReferences(fileName, position): ReferencedSymbol[];
getRenameInfo(fileName, position, preferences?): RenameInfo;
getSignatureHelpItems(fileName, position, options?): SignatureHelpItems;
getQuickInfoAtPosition(fileName, position): QuickInfo;
getApplicableRefactors(fileName, positionOrRange, ...): ApplicableRefactorInfo[];
getCodeFixesAtPosition(fileName, start, end, errorCodes, ...): CodeFixAction[];
// ... 50+ 个方法
}
createLanguageService() 接收一个 LanguageServiceHost——这是对文件系统和项目配置的抽象封装——并创建一个与 host 文件版本保持同步的 Program。每次调用语言服务方法时,它都会先执行 synchronizeHostData(),确保内部的 Program 反映最新的文件变更。
classDiagram
class LanguageServiceHost {
+getScriptFileNames(): string[]
+getScriptVersion(fileName): string
+getScriptSnapshot(fileName): IScriptSnapshot
+getCompilationSettings(): CompilerOptions
+getCurrentDirectory(): string
}
class LanguageService {
+getCompletionsAtPosition()
+getSemanticDiagnostics()
+getDefinitionAtPosition()
+findReferences()
+getCodeFixesAtPosition()
}
class Program {
+getSourceFiles()
+getTypeChecker()
+emit()
}
LanguageServiceHost <-- LanguageService : reads from
LanguageService --> Program : creates/manages
Program --> TypeChecker : creates on demand
LanguageServiceHost 的设计是 TypeScript 支持多种嵌入方式的关键所在。VS Code 的 TypeScript 扩展、tsserver 进程、webpack 的 ts-loader,以及各类编程式消费者,都以不同方式实现了 LanguageServiceHost,却能共享同一套语言服务能力。
提示: 如果你正在构建需要 TypeScript 类型信息的工具(如 linter、代码生成器、文档工具),可以实现
LanguageServiceHost并调用createLanguageService()。这样就能获得类型检查器的全部能力,而无需处理 tsserver 的通信协议。
补全、诊断与导航
补全引擎
src/services/completions.ts 中的自动补全引擎共 6,173 行,是服务层中体量最大的单一功能实现。给定一个光标位置,它需要判断以下几件事:
- 当前属于哪种补全上下文? ——成员访问(
foo.)、字符串字面量(import 路径)、JSX 属性、类型位置、语句位置等 - 当前作用域内有哪些符号? ——标识符补全使用 checker 的
getSymbolsInScope(),成员访问使用getPropertiesOfType() - 如何对结果排序和过滤 ——模糊匹配、基于类型的相关性、最近使用频率、标签详情
sequenceDiagram
participant Editor
participant LS as LanguageService
participant Comp as completions.ts
participant TC as TypeChecker
Editor->>LS: getCompletionsAtPosition(file, pos)
LS->>Comp: getCompletions(sourceFile, position, ...)
Comp->>Comp: Determine completion context
alt Member access (foo.|)
Comp->>TC: getTypeAtLocation(foo)
Comp->>TC: getPropertiesOfType(type)
else Identifier position
Comp->>TC: getSymbolsInScope(location)
else Import path
Comp->>Comp: Search file system / package.json
end
Comp->>Comp: Filter, rank, format entries
Comp-->>Editor: CompletionInfo { entries: [...] }
通过声明合并扩展节点
代码库中最具创意的模式之一,是服务层为 AST 节点添加便捷方法的方式。在 src/services/types.ts 中可以看到:
declare module "../compiler/types.js" {
export interface Node {
getSourceFile(): SourceFile;
getChildCount(sourceFile?: SourceFile): number;
getChildAt(index: number, sourceFile?: SourceFile): Node;
getChildren(sourceFile?: SourceFile): readonly Node[];
getStart(sourceFile?: SourceFile, includeJsDocComment?: boolean): number;
getFullStart(): number;
getEnd(): number;
getWidth(sourceFile?: SourceFileLike): number;
getFullWidth(): number;
getText(sourceFile?: SourceFile): string;
getFirstToken(sourceFile?: SourceFile): Node | undefined;
getLastToken(sourceFile?: SourceFile): Node | undefined;
forEachChild<T>(cbNode: (node: Node) => T | undefined, ...): T | undefined;
}
}
这里利用了 TypeScript 自身的声明合并特性,为编译器的 Node 接口追加了方法。这些方法的实现在运行时由服务层挂载到 Node.prototype 上。编译器核心本身并不依赖这些方法——它们的存在,纯粹是为了方便服务层代码以及公共 API 的外部使用者。
诊断的分层处理
诊断信息在服务层分三个层次流转:
- 语法诊断 ——速度快,来自 parser,无需类型检查
- 语义诊断 ——来自 checker,需要完整的类型检查
- 建议诊断 ——非错误性的改进提示
服务层对诊断信息按文件缓存,并在文件版本变更时使缓存失效。在编辑器场景下,语法诊断会优先返回(即时反馈),语义诊断随后到达(若 checker 需要执行额外工作,则可能有延迟)。
代码修复与重构
服务层内置了两个可扩展的自动代码操作注册表。
代码修复(70+ 个实现)
src/services/codefixes/ 目录下包含 70 余个独立的代码修复提供者,每个针对特定的错误码:
| 代码修复 | 触发条件 |
|---|---|
importFixes.ts |
"Cannot find name 'X'" |
fixSpelling.ts |
"Property 'X' does not exist. Did you mean 'Y'?" |
fixClassIncorrectlyImplementsInterface.ts |
"Class incorrectly implements interface" |
fixMissingAsync.ts |
在非 async 函数中使用了 await |
addMissingAwait.ts |
在期望值的位置使用了 Promise |
fixStrictClassInitialization.ts |
属性未在构造函数中初始化 |
convertToTypeOnlyImport.ts |
import 仅用作类型 |
每个提供者通过 registerCodeFix() 调用注册自身,并声明它所处理的错误码。当编辑器为某条诊断请求代码修复时,注册表将请求分发给匹配的提供者,提供者分析错误上下文并生成描述修复内容的 TextChange 对象。
重构(16 个提供者)
src/services/refactors/ 目录包含以下重构提供者:
extractSymbol.ts——提取函数 / 提取常量moveToFile.ts/moveToNewFile.ts——在文件间移动声明convertParamsToDestructuredObject.ts——将位置参数转换为具名对象convertImport.ts——在具名导入与命名空间导入之间切换inlineVariable.ts——在所有引用处内联变量convertToOptionalChainExpression.ts——将&&链式调用转换为?.
flowchart TD
Editor["Editor: 'Refactor...'"] --> LS["getApplicableRefactors(file, range)"]
LS --> Registry["Refactoring Registry"]
Registry --> R1["extractSymbol"]
Registry --> R2["moveToFile"]
Registry --> R3["convertImport"]
Registry --> RN["...16 total"]
R1 --> Check["Is refactoring applicable<br/>at this position?"]
Check -->|Yes| Available["Return refactoring info"]
Check -->|No| Skip["Skip"]
Available --> Editor2["User selects refactoring"]
Editor2 --> Apply["getEditsForRefactor()"]
Apply --> Edits["TextChange[] across files"]
tsserver:协议、Session 与项目管理
编辑器实际通信的对象是 tsserver 进程。它是一个 Node.js 进程,从 stdin 读取 JSON 请求,将 JSON 响应写入 stdout。
Wire 协议
src/server/protocol.ts 定义了 CommandTypes 枚举——服务器能够处理的所有请求类型:
export const enum CommandTypes {
CompletionInfo = "completionInfo",
CompletionDetails = "completionEntryDetails",
Definition = "definition",
DefinitionAndBoundSpan = "definitionAndBoundSpan",
Implementation = "implementation",
References = "references",
Rename = "rename",
Geterr = "geterr",
Format = "format",
Configure = "configure",
Open = "open",
Close = "close",
Change = "change",
NavBar = "navbar",
// ... 50+ 个命令
}
这套协议并非 LSP(语言服务器协议)——它是 TypeScript 自己的协议,诞生时间早于 LSP。VS Code 的 TypeScript 扩展负责在 LSP 与这套协议之间进行转换。部分编辑器(如旧版 Visual Studio)则直接使用此协议。
Session 类
src/server/session.ts 中的 Session 类负责分发传入请求。它维护着一张从命令名到处理函数的映射表,每个处理函数负责:
- 从请求中提取参数
- 将行/列位置转换为内部偏移量
- 调用相应的
LanguageService方法 - 将内部结果转换回协议响应格式
ProjectService
src/server/editorServices.ts 中的 ProjectService 是整个工作区的管理者。单个 tsserver 进程可以同时管理多个项目:
flowchart TD
PS["ProjectService"] --> CP["ConfiguredProject<br/>(tsconfig.json based)"]
PS --> IP["InferredProject<br/>(loose files)"]
PS --> EP["ExternalProject<br/>(build tool managed)"]
CP --> LS1["LanguageService"]
IP --> LS2["LanguageService"]
EP --> LS3["LanguageService"]
LS1 --> P1["Program"]
LS2 --> P2["Program"]
LS3 --> P3["Program"]
- ConfiguredProject:基于
tsconfig.json文件创建。当你打开一个.ts文件时,服务器会向上查找tsconfig.json并为其创建对应的项目。 - InferredProject:为不属于任何
tsconfig.json的文件创建。服务器以默认配置创建项目,并将当前打开的所有散落文件纳入其中。 - ExternalProject:由外部构建工具创建,构建工具会明确告知服务器需要使用哪些文件和选项。
每个项目拥有独立的 LanguageService 和 Program。ProjectService 负责整个生命周期的管理——在文件打开时创建项目、在文件变更时更新项目、在项目不再需要时将其销毁。
服务器入口点
src/tsserver/server.ts 入口点负责初始化 Node.js 系统、创建 logger、配置取消机制,然后调用 start()。有一个值得关注的细节:它覆盖了 console.log、console.warn 和 console.error,将所有输出重定向到 logger。这样做的目的是防止语言服务插件(它们可能使用 console.log)意外污染 stdout 上的 JSON 协议流。
console.log = (...args) =>
logger.msg(args.length === 1 ? args[0] : args.join(", "), ts.server.Msg.Info);
console.warn = (...args) =>
logger.msg(args.length === 1 ? args[0] : args.join(", "), ts.server.Msg.Err);
console.error = (...args) =>
logger.msg(args.length === 1 ? args[0] : args.join(", "), ts.server.Msg.Err);
提示: 调试 tsserver 时,可以将环境变量
TSS_LOG设置为某个文件路径。服务器会将每条请求、响应和内部事件的详细信息写入该文件——在排查编辑器集成问题时非常有价值。
全局视图
通过六篇文章对 TypeScript 编译器的完整梳理,让我们看看所有组件是如何协同运作的:
flowchart TD
subgraph "User Interface"
Editor["VS Code / Editor"]
end
subgraph "Server Layer (src/server)"
tsserver["tsserver process"]
Session["Session"]
ProjectService["ProjectService"]
end
subgraph "Service Layer (src/services)"
LS["LanguageService"]
Completions["Completions"]
Diagnostics["Diagnostics"]
CodeFixes["CodeFixes (70+)"]
Refactors["Refactors (16)"]
end
subgraph "Compiler Core (src/compiler)"
Program["Program"]
Scanner["Scanner"]
Parser["Parser"]
Binder["Binder"]
Checker["Checker (54K lines)"]
Emitter["Emitter + Transformers"]
end
Editor <-->|"JSON protocol"| tsserver
tsserver --> Session
Session --> ProjectService
ProjectService --> LS
LS --> Completions
LS --> Diagnostics
LS --> CodeFixes
LS --> Refactors
LS --> Program
Program --> Scanner
Program --> Parser
Program --> Binder
Program --> Checker
Program --> Emitter
这套架构——精简的入口点、五阶段编译流水线、三种核心数据结构(Node、Symbol、Type),以及从编译器核心到语言服务再到服务器的清晰分层——已经支撑 TypeScript 运行了十余年。当团队用 Go 重写编译器以实现 10 倍性能提升时,基本架构思路仍将延续。深入理解这份 JavaScript 实现,能为你阅读新版实现打下坚实的基础。
TypeScript 编译器是 JavaScript 生态系统中最令人叹为观止的开源项目之一。无论你是想为 TypeScript 本身做贡献、构建消费其 API 的工具,还是单纯好奇这门语言底层的工作原理,希望本系列文章能为你绘制一张清晰的地图,让你在这片代码疆域中游刃有余。