编程 TypeScript 7.0 深度解析:微软用 Go 重写编译器,14 年来最大变革如何让类型检查提速 10 倍——从逐行移植到共享内存并行,一个「不可能完成」的工程壮举的完整技术剖析

2026-07-07 04:42:02 +0800 CST views 29

TypeScript 7.0 深度解析:微软用 Go 重写编译器,14 年来最大变革如何让类型检查提速 10 倍——从逐行移植到共享内存并行,一个「不可能完成」的工程壮举的完整技术剖析

引言:一场酝酿了三年的「叛变」

2026 年 6 月 18 日,微软 TypeScript 团队发布了 7.0 RC(Release Candidate)。这不仅仅是一次常规版本更新——它标志着 TypeScript 自 2012 年诞生以来最底层、最彻底的一次重构:整个编译器从 TypeScript/JavaScript 栈移植到了 Go 语言

性能数据令人瞠目:

项目6.0 耗时7.0 耗时加速比
VS Code 代码库(150 万行)77.8 秒7.5 秒10.4x
Sentry 项目133 秒16 秒8.3x
TypeORM17.5 秒1.3 秒13.5x
Playwright11.1 秒1.1 秒10.1x
内存使用减半

这不是渐进式优化,这是数量级的跃迁。作为一名长期关注 TypeScript 生态的开发者,我认为这次重构的技术含量和工程难度,足以写入编程语言工具链开发的教科书。

本文将从为什么重写、如何重写、并行架构设计、LSP 语言服务器、迁移策略五个维度,深入剖析这次变革的每一个技术细节。


第一章:为什么要用 Go 重写?TypeScript 编译器的「天花板」

1.1 自举编译器的宿命困境

TypeScript 编译器(tsc)本身是用 TypeScript 编写的——这是一个经典的**自举(bootstrapping)**设计。TypeScript 源码经过编译生成 JavaScript,然后由 Node.js 运行时执行这个 JavaScript 来编译用户的 TypeScript 代码。

这种设计有一个优雅的闭环:语言用自己的语法编写自己的编译器,证明了语言自身的表达能力。但它也带来了一个根本性的性能瓶颈:

TypeScript 编译器受限于 JavaScript 运行时的性能天花板。

V8 引擎虽然极其优秀,但 JavaScript 作为动态语言,即使经过 JIT 编译,在以下几个方面依然无法与原生编译的语言匹敌:

// TypeScript 的类型检查涉及大量递归类型求值
// 这在 JS 运行时中是 CPU 密集型操作
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object 
    ? DeepPartial<T[P]> 
    : T[P];
};

// 条件类型嵌套越多,类型检查越慢
type InferRoute<T> = T extends `/${infer Segment}/${infer Rest}`
  ? { [K in Segment]: InferRoute<`/${Rest}`> }
  : T extends `/${infer Segment}`
    ? { [K in Segment]: null }
    : never;

1.2 内存管理的「天花板」

JavaScript 的垃圾回收机制虽然方便,但在处理大型类型检查任务时表现不佳。TypeScript 编译器需要维护:

  • AST(抽象语法树):每个源文件的完整语法结构
  • 类型符号表:所有变量、函数、类的类型信息
  • 检查器缓存:类型求值的中间结果
  • 诊断信息:错误和警告的上下文

对于百万行级别的代码库,这些数据结构的内存开销可以轻松达到数 GB。JavaScript 的 GC 在这种规模下会产生明显的暂停,影响编辑器中的实时响应体验。

1.3 并行化的「不可能」

JavaScript 的单线程模型使得真正的并行计算极其困难。虽然 Web Workers 提供了某种形式的并行,但它们:

  • 无法共享内存(需要通过消息传递)
  • 通信开销大
  • 不适合细粒度的任务分割

而类型检查天然是可以并行的——不同文件的类型检查在很多情况下可以独立进行。TypeScript 6.0 完全无法利用多核 CPU 的优势,这在 2026 年 16 核甚至 32 核处理器普及的背景下,是一种巨大的浪费。

1.4 为什么选择 Go?

微软在选择替代语言时,有几个关键考量:

性能:Go 编译为原生代码,没有 JIT 预热,没有 GC 暂停的长尾效应。Go 的 GC 延迟已经优化到亚毫秒级别。

并行:Go 的 goroutine + channel 模型是天然的并行编程范式。共享内存并行在 Go 中是一等公民。

可维护性:Go 的语法简洁,团队学习成本相对较低。TypeScript 团队不需要成为 Rust 专家。

跨平台:Go 的交叉编译能力优秀,可以轻松构建 Windows/macOS/Linux 的原生二进制。

生态:Go 标准库丰富,特别是 go/astgo/parsergo/types 等包提供了语言工具链开发的基础设施。

为什么不选 Rust?虽然 Rust 的性能可能更好,但它的学习曲线陡峭,所有权系统会显著增加移植工作的复杂度。对于一个需要「逐行翻译」的项目来说,Go 的简洁性是更好的选择。


第二章:逐行移植——一场精密的「外科手术」

2.1 不是重写,是翻译

这是理解 TypeScript 7.0 最关键的一点:微软没有用 Go 重新设计编译器,而是逐行将 TypeScript 源码翻译为 Go

官方博客明确表示:

"The new Go codebase was methodically ported from our existing implementation rather than rewritten from scratch, and its type-checking logic is structurally identical to TypeScript 6.0."

这意味着:

  1. AST 结构完全对应:TypeScript 的 SyntaxKind 枚举被映射到 Go 的 iota 常量
  2. Checker 逻辑一一对应:每个类型检查函数都有对应的 Go 版本
  3. 错误信息完全一致:相同的输入产生相同的诊断
  4. 测试套件全部通过:十年积累的测试用例验证了翻译的正确性

2.2 翻译过程中的核心挑战

2.2.1 类型系统的表达差异

TypeScript 的类型系统极其复杂,包含联合类型、交叉类型、条件类型、映射类型、模板字面量类型等。这些在 Go 中没有直接对应的概念。

// Go 中需要手动实现类型求值的等价逻辑
type Checker struct {
    // 类型求值缓存
    evalCache map[TypeKey]*EvalResult
    
    // 条件类型求值栈(防止无限递归)
    condStack []ConditionalFrame
    
    // 模板字面量类型的状态机
    templateFSM *TemplateLiteralFSM
}

2.2.2 JavaScript 语义的忠实移植

TypeScript 编译器需要理解和模拟 JavaScript 的运行时语义。这包括:

  • 原型链查找
  • this 绑定规则
  • 闭包捕获
  • 模块解析策略
  • 装饰器求值顺序

这些在 Go 中都需要从零实现,因为 Go 的类型系统和运行时模型完全不同。

2.2.3 文件系统抽象层

TypeScript 编译器通过 System 接口与文件系统交互,这个抽象层允许在不同环境(Node.js、浏览器、VS Code 扩展)中使用不同的实现。在 Go 中需要重新设计这个抽象:

type CompilerHost interface {
    ReadFile(path string) ([]byte, error)
    WriteFile(path string, data []byte) error
    FileExists(path string) bool
    DirectoryExists(path string) bool
    GetDirectories(path string) []string
    Realpath(path string) string
    GetCurrentDirectory() string
}

2.3 工程规模

这次移植涉及:

  • 数十万行代码从 TypeScript 翻译为 Go
  • 数千个测试用例全部移植并通过
  • 一年多的开发时间
  • 数十家企业的早期测试(Bloomberg、Canva、Figma、Google、Notion、Slack、Vercel 等)

这种规模的语言间翻译工程,在编程语言工具链的历史上是极其罕见的。


第三章:并行架构——TypeScript 7.0 的核心突破

3.1 三阶段并行流水线

TypeScript 7.0 将编译过程分为三个可并行的阶段:

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   Parsing   │ ──▶ │ Type-Checking│ ──▶ │   Emitting  │
│  (并行解析)  │     │ (并行类型检查)│     │  (并行输出)  │
└─────────────┘     └──────────────┘     └─────────────┘

解析(Parsing):将源码转换为 AST。这一步几乎可以完全独立地并行进行——每个文件的解析不依赖其他文件。

类型检查(Type-Checking):这是最复杂的一步,因为文件之间存在类型依赖。TypeScript 7.0 采用了一种巧妙的并行策略。

输出(Emitting):将 AST 转换为 JavaScript 代码和声明文件。这也可以大范围并行。

3.2 Checker 并行:核心创新

类型检查的并行化是最大的技术挑战。问题是:如果文件 A 导入了文件 B 的类型,那么检查 A 必须知道 B 的类型信息。完全独立并行是不可能的。

TypeScript 7.0 的解决方案是:

type CheckerPool struct {
    workers    []*CheckerWorker
    sharedView *SharedTypeView
    fileQueue  chan *SourceFile
}

type CheckerWorker struct {
    id         int
    localCache map[TypeKey]*TypeResult
    view       *SharedTypeView  // 共享的只读类型视图
}

关键设计

  1. 固定数量的 Worker:默认 4 个(可通过 --checkers 配置)
  2. 共享只读视图:所有 Worker 共享全局类型信息的只读视图
  3. 确定性分割:给定相同的输入文件列表,Worker 的任务分配是确定性的
  4. 结果一致性:无论使用多少个 Worker,最终的检查结果必须完全一致

这种设计的核心洞察是:虽然类型检查有依赖关系,但依赖图是稀疏的。大多数文件只依赖于少数几个直接导入的文件,而大部分检查工作(如检查函数体内的类型使用)可以独立进行。

3.3 Project References 并行

TypeScript 7.0 引入了第二个并行维度:项目引用并行

在 monorepo 中,通常有多个 TypeScript 项目通过 references 相互关联。传统上,这些项目必须按照依赖顺序串行构建。TypeScript 7.0 允许多个项目同时构建:

Monorepo:
├── packages/core (基础库)
├── packages/ui (依赖 core)
├── packages/api (依赖 core)
└── packages/app (依赖 ui, api)

传统构建: core → ui → api → app (串行)
TS 7.0:   core → [ui, api] 并行 → app

通过 --builders 标志控制并行构建器的数量。注意 --checkers--builders 是乘法关系:--checkers 4 --builders 4 理论上可以有 16 个类型检查器同时运行。

3.4 单线程模式

对于调试和性能对比,TypeScript 7.0 提供了 --singleThreaded 标志,强制所有操作在单线程中执行。这对于:

  • 调试编译器问题
  • 与 TypeScript 6.0 进行公平的性能对比
  • 资源受限的 CI 环境

非常有用。


第四章:基于 LSP 的语言服务器——编辑器体验的质变

4.1 从 tsserver 到 LSP

TypeScript 6.0 及之前版本的语言服务器(tsserver)使用自定义的协议与编辑器通信。TypeScript 7.0 转向了标准的 Language Server Protocol (LSP)

这意味着:

  1. 标准化:任何支持 LSP 的编辑器都可以获得原生级别的 TypeScript 支持
  2. 可扩展性:可以与 Copilot CLI 等工具集成
  3. 多线程请求处理:LSP 天然支持并发请求

4.2 编辑器中的性能提升

VS Code 用户可以通过安装 TypeScript Native Preview 扩展来体验 7.0 的编辑器性能。这个扩展已经实现了:

  • ✅ 自动导入(Auto Import)
  • ✅ 悬停提示(Hover)
  • ✅ 内嵌提示(Inlay Hints)
  • ✅ 代码透镜(CodeLens)
  • ✅ 诊断信息(Diagnostics)
  • ✅ 代码补全(Code Completion)
  • ✅ 重构(Refactoring)

模糊测试显示,语言服务器命令的失败率降低到了 TypeScript 6.0 的 1/20

4.3 实际编辑体验

在大型项目中的编辑体验提升是革命性的:

TypeScript 6.0

  • 打开大文件后需要等待数秒才能看到类型错误
  • 代码补全有时会卡顿
  • 重构大型项目时经常超时

TypeScript 7.0

  • 类型错误几乎实时显示
  • 代码补全响应在毫秒级
  • 重构操作流畅无阻塞

这对于使用 TypeScript 的 AI 编程工具(如 Cursor、Copilot)来说意义重大——更快的类型检查意味着更快的上下文理解,从而提供更准确的代码建议。


第五章:迁移策略——如何从 6.0 过渡到 7.0

5.1 安装与使用

# 安装 7.0 RC
npm install -D typescript@rc

# 验证版本
npx tsc --version
# Version 7.0.1-rc

# 安装 VS Code 扩展
# 搜索 "TypeScript Native Preview"

5.2 与 TypeScript 6.0 共存

由于 7.0 的稳定 API 要到 7.1 才会发布,微软提供了 @typescript/typescript6 兼容包:

{
  "devDependencies": {
    "typescript": "npm:@typescript/typescript6@^6.0.0",
    "typescript-7": "npm:typescript@rc"
  }
}

这样你可以:

  • 使用 tsc6 调用 TypeScript 6.0
  • 使用 tscnpx tsc 调用 TypeScript 7.0
  • 两者互不干扰

5.3 需要注意的破坏性变更

TypeScript 7.0 继承了 6.0 的所有新默认值和废弃项硬错误:

{
  "compilerOptions": {
    // 新默认值
    "strict": true,
    "module": "esnext",
    "target": "es2024",
    "noUncheckedSideEffectImports": true,
    "libReplacement": false,
    "stableTypeOrdering": true,
    
    // 可能需要手动设置
    "rootDir": "./src",
    "types": ["node", "jest"]
  }
}

已移除的选项(硬错误):

  • target: es5 — 不再支持
  • moduleResolution: node/node10 — 使用 nodenextbundler
  • module: amd/umd/systemjs/none — 使用 esnextpreserve
  • baseUrl — 使用相对于项目根目录的 paths
  • esModuleInterop: false — 必须为 true

5.4 迁移路径建议

Phase 1: 升级到 TypeScript 6.0,解决所有废弃警告
    ↓
Phase 2: 开发环境使用 7.0 RC,CI 保持 6.0
    ↓
Phase 3: CI 也切换到 7.0,验证构建结果一致性
    ↓
Phase 4: 全面切换到 7.0 稳定版

第六章:watch 模式的重构——来自 Parcel 的灵感

6.1 文件监听的挑战

Go 标准库没有提供跨平台的文件监听 API。TypeScript 团队探索了多种方案:

  1. 轮询方案:定期检查文件修改时间

    • 优点:跨平台兼容
    • 缺点:CPU 开销大,尤其在 node_modules 很大的项目中
  2. 第三方库:各种 Go 文件监听库

    • 稳定性、性能、跨平台支持参差不齐
  3. 移植 @parcel/watcher:最终选择

    • VS Code 已经使用多年,稳定可靠
    • C++ 代码移植到 Go,避免引入 C++ 工具链

6.2 Parcel Watcher 的 Go 移植

// 从 C++ 到 Go 的移植策略
type Watcher struct {
    backend  FileWatcherBackend  // 平台特定实现
    events   chan Event
    watchers map[string]*watchEntry
}

// 平台特定实现
type FileWatcherBackend interface {
    Watch(path string) error
    Unwatch(path string) error
    Close() error
}

// macOS: FSEvents
// Linux: inotify
// Windows: ReadDirectoryChangesW

移植过程中使用了少量汇编 shim 来调用平台特定的系统调用,避免了引入完整的 C++ 工具链。最终结果是:

  • 资源占用显著降低
  • 跨平台行为更加一致
  • 与 VS Code 的文件监听行为保持同步

第七章:对生态的影响

7.1 工具链层面

构建工具:Vite、Webpack、esbuild 等工具中集成的 TypeScript 转译可能会逐渐切换到 7.0 的原生编译器,进一步提升构建速度。

ESLint / Biometypescript-eslint 需要适配新的编译器 API,但由于 LSP 标准化,这个过程应该相对平滑。

IDE 支持:除了 VS Code,其他编辑器(Vim、Neovim、Emacs、WebStorm)通过 LSP 获得一致的 TypeScript 支持体验。

7.2 开发者体验层面

CI/CD 加速:对于大型 TypeScript 项目,CI 构建时间可能从 10 分钟缩短到 1 分钟。这不仅仅是效率提升——它改变了开发工作流。

AI 编程工具:更快的类型检查意味着 AI 工具可以更快地理解代码上下文,提供更准确的建议。这对于 Cursor、Copilot 等工具是巨大的利好。

Monorepo 友好:并行构建使得 monorepo 的维护成本大幅降低,更多团队可能愿意采用 monorepo 架构。

7.3 编程语言工具链的趋势

TypeScript 7.0 的 Go 重写反映了更大的行业趋势:

  • Rust 重写:esbuild(Go)、SWC(Rust)、Rspack(Rust)
  • 原生编译器:从解释型/JIT 向 AOT 编译的转变
  • 并行优先:多核时代的必然选择

这不仅仅是 TypeScript 的故事,而是整个前端工具链「原生化」浪潮的一部分。


第八章:实战——在现有项目中体验 TypeScript 7.0

8.1 快速开始

# 创建测试项目
mkdir ts7-demo && cd ts7-demo
npm init -y

# 安装 TypeScript 7.0 RC
npm install -D typescript@rc

# 创建一个有复杂类型的文件
cat > complex.ts << 'EOF'
// 深层嵌套的条件类型
type DeepUnwrap<T> = T extends Promise<infer U>
  ? DeepUnwrap<U>
  : T extends Array<infer U>
    ? DeepUnwrap<U>[]
    : T extends object
      ? { [K in keyof T]: DeepUnwrap<T[K]> }
      : T;

// 模板字面量类型路由解析
type ParseRoute<T extends string> = 
  T extends `/${infer Segment}/${infer Rest}`
    ? { [K in Segment]: ParseRoute<`/${Rest}`> }
    : T extends `/${infer Segment}`
      ? { [K in Segment]: null }
      : {};

// 复杂的泛型约束
interface Repository<T extends { id: string | number }> {
  findById(id: T['id']): Promise<T | null>;
  findAll(filter?: Partial<T>): Promise<T[]>;
  create(data: Omit<T, 'id'>): Promise<T>;
  update(id: T['id'], data: Partial<Omit<T, 'id'>>): Promise<T>;
  delete(id: T['id']): Promise<boolean>;
}

// 使用示例
interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;
}

const userRepo: Repository<User> = {
  async findById(id) { return null; },
  async findAll(filter) { return []; },
  async create(data) { return { id: '1', ...data }; },
  async update(id, data) { return { id, name: '', email: '', ...data }; },
  async delete(id) { return true; },
};
EOF

# 运行类型检查
time npx tsc --noEmit complex.ts

8.2 性能对比脚本

#!/bin/bash
# compare-ts-versions.sh

echo "=== TypeScript 6.0 ==="
npx -p typescript@6 tsc --noEmit --generateTrace trace-6 complex.ts

echo "=== TypeScript 7.0 RC ==="
npx -p typescript@rc tsc --noEmit --generateTrace trace-7 complex.ts

echo "=== 生成性能报告 ==="
# 使用 @typescript/trace-importer 分析 trace 文件
npx @typescript/trace-importer trace-6
npx @typescript/trace-importer trace-7

8.3 并行配置调优

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "target": "es2024",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "outDir": "./dist",
    "rootDir": "./src"
  }
}
# 默认 4 个 checker
npx tsc --build

# 增加到 8 个 checker(适合 16 核机器)
npx tsc --build --checkers 8

# Monorepo 项目引用并行
npx tsc --build --checkers 4 --builders 4

# 单线程模式(调试用)
npx tsc --build --singleThreaded

第九章:深入编译器内部——Go 实现的关键数据结构

9.1 AST 节点表示

// TypeScript AST 在 Go 中的表示
type Node struct {
    Kind     SyntaxKind
    Flags    NodeFlags
    Pos      int
    End      int
    Parent   *Node
    Children []*Node
}

type SyntaxKind uint16

const (
    // 声明
    SourceFile        SyntaxKind = 300
    FunctionDeclaration SyntaxKind = 252
    VariableDeclaration SyntaxKind = 253
    ClassDeclaration  SyntaxKind = 254
    
    // 类型
    TypeReference     SyntaxKind = 180
    UnionType         SyntaxKind = 181
    IntersectionType  SyntaxKind = 182
    ConditionalType   SyntaxKind = 183
    MappedType        SyntaxKind = 184
    TemplateLiteralType SyntaxKind = 185
    
    // 表达式
    CallExpression    SyntaxKind = 205
    PropertyAccess    SyntaxKind = 206
    ElementAccess     SyntaxKind = 207
)

9.2 类型求值引擎

type TypeChecker struct {
    // 全局状态
    types       map[TypeKey]*Type
    symbols     map[SymbolKey]*Symbol
    diagnostics []Diagnostic
    
    // 并行控制
    pool        *WorkerPool
    sharedState *SharedCheckerState
    
    // 缓存
    typeCache   sync.Map  // 并发安全的类型缓存
    subtypeCache sync.Map // 子类型关系缓存
}

func (c *TypeChecker) checkType(source *Type, target *Type) bool {
    // 快速路径:相同类型
    if source == target {
        return true
    }
    
    // 缓存查找
    cacheKey := TypePair{source.id, target.id}
    if result, ok := c.subtypeCache.Load(cacheKey); ok {
        return result.(bool)
    }
    
    // 实际检查
    result := c.checkTypeWorker(source, target)
    c.subtypeCache.Store(cacheKey, result)
    return result
}

9.3 模块解析器

type ModuleResolver struct {
    host       CompilerHost
    options    CompilerOptions
    
    // 解析缓存
    cache      map[ModuleKey]*ResolvedModule
    
    // 路径映射
    pathMap    map[string][]string  // paths 配置
    baseUrl    string
}

func (r *ModuleResolver) resolveModule(
    moduleName string,
    containingFile string,
) *ResolvedModule {
    // 1. 相对路径解析
    if isRelative(moduleName) {
        return r.resolveRelative(moduleName, containingFile)
    }
    
    // 2. 路径映射
    if mapped, ok := r.pathMap[moduleName]; ok {
        return r.resolvePaths(mapped, containingFile)
    }
    
    // 3. node_modules 查找
    return r.resolveNodeModules(moduleName, containingFile)
}

第十章:总结与展望

10.1 这次重写的意义

TypeScript 7.0 的 Go 重写不仅仅是一次性能优化,它代表了几个重要趋势:

  1. 工具链原生化:前端工具链正在从 JavaScript 运行时走向原生编译
  2. 并行计算普及:多核 CPU 的优势终于被编程语言工具充分利用
  3. 标准化协议:LSP 的采用使得编辑器生态更加统一
  4. 工程可行性验证:证明了大型语言工具链的跨语言移植是可行的

10.2 未来展望

  • TypeScript 7.1:稳定的编程 API,第三方工具可以正式适配
  • 更多原生工具:可能催生基于 Go 的 TypeScript bundler、linter
  • AI 集成:更快的类型检查为 AI 编程工具提供更好的实时反馈
  • 语言特性:性能提升后,可以考虑实现更复杂的类型系统特性

10.3 给开发者的建议

  1. 现在就开始测试:在开发环境中安装 7.0 RC,提前发现问题
  2. 关注废弃警告:如果还在用 5.x,尽快升级到 6.0
  3. 优化 CI:利用 --checkers--builders 调优构建速度
  4. 拥抱 LSP:选择支持 LSP 的编辑器,获得最佳体验

TypeScript 7.0 的发布,标志着前端工程化进入了新的纪元。当编译器从 77 秒变成 7 秒,当内存占用减半,当编辑器体验如丝般顺滑——这不仅仅是数字的变化,而是开发体验的根本性重塑。

对于每一个 TypeScript 开发者来说,这是值得铭记的时刻:14 年来最大的变革,已经到来。


参考资料

  1. Announcing TypeScript 7.0 RC - TypeScript Official Blog
  2. TypeScript 7.0 RC Release Notes
  3. TypeScript Native Preview VS Code Extension
  4. @parcel/watcher - Cross-platform File Watcher
  5. Language Server Protocol Specification

推荐文章

7种Go语言生成唯一ID的实用方法
2024-11-19 05:22:50 +0800 CST
php内置函数除法取整和取余数
2024-11-19 10:11:51 +0800 CST
Vue3中如何处理状态管理?
2024-11-17 07:13:45 +0800 CST
初学者的 Rust Web 开发指南
2024-11-18 10:51:35 +0800 CST
Gai:AI 原生的 Go Web 全栈框架
2026-05-21 16:19:43 +0800 CST
Vue3中如何进行性能优化?
2024-11-17 22:52:59 +0800 CST
介绍Vue3的Tree Shaking是什么?
2024-11-18 20:37:41 +0800 CST
Vue3中的响应式原理是什么?
2024-11-19 09:43:12 +0800 CST
程序员茄子在线接单