CHAPTER 01 · 起步
生态全景地图
TS 生态之所以让人眼花缭乱,是因为 Java 里一个 JDK 包办的事情,在这里被拆成了五六个可替换的工具。拆开看每一层,其实都能在你熟悉的世界里找到对应物。
- 看到
node、pnpm、tsc、vite、next、wrangler这些命令时,说出它们分别处于哪一层。 - 解释为什么“代码能运行”不代表“类型是正确的”。
- 说出 2026 年三条主流技术路线(Vercel / Cloudflare / 自托管)的典型组合及它们的共同部分。
开场:一个项目,七八个命令
你让 Agent 生成了一个项目,package.json 的 scripts 里写着:"dev": "vite dev"、"build": "vite build"、"typecheck": "tsc --noEmit"、"deploy": "wrangler deploy"。你用 pnpm 安装依赖,用 node 运行脚本。为什么需要这么多工具?只写 TypeScript 然后“编译运行”不行吗?
答案方向:把 TS 源码变成“用户能用的应用”,要经过一条流水线:管理依赖 → 检查类型 → 去掉类型 → 打包优化 → 在某个运行时上执行 → 发布到某个平台。在 Java 世界,JDK + Maven 把其中大部分合在一起;在 TS 世界,每一步都有独立的、可替换的工具。上面每个命令,就是流水线上的一个工位。
先用你熟悉的世界做对照
下表把 TS 生态的每一层映射到 Java / Go / Python。这张表是整章的“锚点”,后面每一节都是在展开其中一行。
| 层 | Java / Go / Python | TypeScript 生态 | 深度 |
|---|---|---|---|
| 语言 | Java / Go / Python | TypeScript(= JavaScript + 类型标注) | 原理 |
| 类型检查 | javac / go build(检查与编译一体) | tsc(只检查,通常不产出文件) | 原理 |
| 转译(去类型) | —(编译器一起做了) | esbuild、SWC、Oxc、Node 内置的类型剥离 | 知道 |
| 运行时 | JVM / Go runtime / CPython | Node.js、Bun、Deno、浏览器、Cloudflare workerd | 会用 |
| 包管理 | Maven / go mod / pip、uv | pnpm、npm、bun(仓库:npm registry) | 会用 |
| 打包 | 打 jar / 静态二进制 | Vite(内部用 Rolldown)、Turbopack(Next.js) | 知道 |
| Web 后端框架 | Spring / Gin / FastAPI | Hono、Express、Fastify、NestJS | 会用 |
| 全栈框架 | Rails / Django(有模板) | Next.js、TanStack Start、React Router | 会用 |
| UI 库 | 模板引擎 / 无 | React(另有 Vue、Svelte、Solid) | 原理 |
| ORM | Hibernate / GORM / SQLAlchemy | Drizzle、Prisma | 会用 |
| 测试 | JUnit / go test / pytest | Vitest(单元)、Playwright(端到端) | 会用 |
| 格式化 / Lint | gofmt / black / ruff | Biome、Prettier、ESLint、Oxlint | 知道 |
| 部署平台 | K8s / 云主机 | Vercel、Cloudflare、自托管 Docker | 会用 |
有两行在你熟悉的世界里没有真正的对应物,也正是新人最容易糊涂的地方:“类型检查与转译分离”和“打包”。我们先讲它们。
流水线:从 .ts 文件到运行中的应用
核心原理:类型检查和转译是两件事 原理
在 Java 里,类型错误意味着编译失败,没有 .class 文件,程序根本跑不起来。TS 不一样。TypeScript 的设计目标之一是:类型只存在于编写阶段,运行时完全不存在。这叫类型擦除(type erasure)。转译器要做的事情简单到近乎机械:把类型标注删掉,剩下的就是合法的 JavaScript。
正因为“删类型”不需要理解类型,esbuild、SWC 这类用 Go / Rust 写的转译器才能快到每秒处理几十万行代码。从 Node.js 23.6 起,Node 可以直接运行 .ts 文件(内置的类型剥离,type stripping),Node 26 更把它作为稳定的默认行为。但请注意:Node 只是删掉类型,它不做类型检查。
Agent 说“我运行了一下,没问题”,只能说明转译后的 JS 能跑,不能说明类型正确。你的验收标准里应该明确要求类型检查通过(例如 pnpm tsc --noEmit 零错误),并且在 CI 里强制执行。第 10 章会把它变成流水线的一部分。
2026 年 7 月发布的 TypeScript 7 把编译器用 Go 重写为原生程序,大型项目类型检查快了约 10 倍。它改变的是速度,不是语言:你写的代码、类型规则都一样。对你的实际影响是:类型检查便宜了,没理由再跳过它。
为什么前端需要“打包” 知道
Go 把程序编译成一个二进制,服务器从本地磁盘加载,文件多大、分几个文件都无所谓。浏览器不一样:代码要通过网络下载到每个用户的设备上。一个项目可能有几千个模块文件(你的代码加上 node_modules 里的依赖),逐个下载会慢到无法接受。打包器(bundler)要解决的就是这个问题:
- 合并:把几千个模块合成少量文件,减少请求数。
- 摇树(tree shaking):只保留真正被用到的代码,删掉没用到的导出。
- 代码分割(code splitting):按页面或路由拆分,首屏只下载首屏需要的代码。
- 压缩(minify):去空格、缩短变量名,减小体积。
- 处理非 JS 资源:CSS、图片、字体,给文件名加内容哈希以便长期缓存。
2026 年你最常遇到的两个名字:Vite(Vite 8 起内部统一使用 Rust 写的 Rolldown 打包器;TanStack Start、React Router、Astro、SvelteKit 等都基于它)和 Turbopack(Next.js 的默认打包器)。它们属于“知道”档:你几乎不需要手动配置,出问题时也通常是明确的构建报错。
运行时:JS 代码实际在哪里执行
JavaScript 引擎(如 V8)只负责执行语言本身:变量、函数、对象。“读文件”“发网络请求”“操作页面”这些能力,是宿主环境提供的。不同的宿主环境,就是不同的运行时(runtime)。
| 运行时 | 在哪里 | 特点 | 深度 |
|---|---|---|---|
| 浏览器 | 用户设备 | 有 DOM(页面结构)、事件;没有文件系统;代码对用户完全可见 | 原理 |
| Node.js | 服务器 / 你的电脑 | 事实标准;生态最全;偶数版本是长期支持(LTS)版本 | 会用 |
| Bun | 服务器 / 你的电脑 | 一体化:运行时 + 包管理 + 测试 + 打包;启动快;高度兼容 Node | 知道 |
| Deno | 服务器 / 你的电脑 | 默认安全(需显式授权文件和网络访问);兼容 npm 包 | 知道 |
| workerd | Cloudflare 边缘节点 | V8 isolate(隔离沙箱),启动极快;有 CPU 和内存限制;兼容大部分 Node API | 会用 |
一个让这些运行时能共享代码的关键事实:它们都在向Web 标准 API靠拢——fetch、Request、Response、URL、ReadableStream、crypto.subtle。如果你的服务器代码只用这些标准 API,它就能在 Node、Bun、Deno 和 Cloudflare 上原样运行。这就是 Hono 这类框架“一份代码到处部署”的原理。
可以把 V8 想成 JVM 里的“字节码执行引擎”,把运行时想成“JDK 标准库 + 启动器”。Java 在服务器和 Android 上是同一门语言、不同的标准库;JS 在浏览器和 Node 上也是同一门语言、不同的宿主 API。类比的失配点:JS 没有统一的“标准库发行方”,各运行时 API 的交集才是可移植的部分。
包管理:package.json 和 node_modules
每个 JS/TS 项目的根目录有一个 package.json,相当于 pom.xml 或 go.mod:
scripts:项目的“命令菜单”。pnpm dev就是运行vite dev。读一个陌生项目时,先看这里。dependenciesvsdevDependencies:运行时需要的依赖 vs 只在开发和构建时需要的工具。^19.2.0:语义化版本(semver)范围,“兼容 19.2.0 的任何 19.x 版本”。实际装了哪个版本,由锁文件(lockfile)决定,例如pnpm-lock.yaml。锁文件必须提交到 Git。"type": "module":声明使用现代的 ES 模块(import/export),第 03 章会解释它和旧的 CommonJS(require)的区别。
用哪个包管理器?推荐 pnpm:它把所有包存在一个全局的内容寻址仓库里、通过链接复用,磁盘占用小、安装快;而且它的依赖结构是“严格”的——你没在 package.json 里声明的包,代码里就 import 不到,避免了“在我机器上能跑”的幽灵依赖。npm 是 Node 自带的默认选择;bun 速度很快,适合已经在用 Bun 运行时的项目。
npm 生态依赖层级很深,一个项目动辄上千个间接依赖,近几年多次发生恶意包、账号被盗后发布带毒版本的事件。几条最低限度的习惯:提交锁文件;CI 里用 pnpm install --frozen-lockfile;pnpm 10 起默认不执行依赖的安装脚本(postinstall),不要随手全局放开;让 Agent 新增依赖时,要求它说明为什么需要、周下载量、最近维护情况。
框架分层:谁管界面,谁管路由,谁管数据
“框架”这个词在 TS 生态里指代的东西差异很大。按职责分三层来看就清楚了:
和 Java 世界最大的不同在于:一个元框架项目同时是前端和后端。Next.js 应用里既有 React 组件,又有处理 HTTP 请求、读写数据库的服务端代码,它们在同一个仓库、同一种语言、甚至可以互相 import 类型。这带来巨大的便利(端到端类型安全,第 08 章),也带来新的风险(服务器代码被误打包进浏览器,第 07 章)。
2026 年的三条主流路线
下面三种组合覆盖了绝大多数新项目。它们的共同部分远大于差异:TypeScript + React + Tailwind + Zod + Drizzle + Vitest 在三条路线里都适用。差异主要在元框架和“运行在哪”。
▲ Vercel 路线
Next.js(App Router、React Server Components、Server Actions)+ shadcn/ui + Postgres(Neon / Supabase)+ Better Auth 或 Clerk + AI SDK → 部署到 Vercel。
长处:资料最多、Agent 最熟悉、部署零配置。
☁ Cloudflare 路线
TanStack Start / React Router / Hono + Vite + D1(SQLite)/ KV / R2 + Durable Objects + Agents SDK → 部署到 Workers。
长处:成本低、全球低延迟、存储与有状态计算原生集成,适合 AI Agent 和实时应用。
🖥 自托管路线
TanStack Start / Next.js(standalone 输出)/ Hono 跑在 Node 或 Bun 上 + Postgres + Redis → Docker 部署到 VPS(可用 Coolify、Dokploy、Kamal)。
长处:无平台限制、成本可预测、数据完全自控。
先学共同部分,平台最后选。第一次做项目,选你能最快部署上线的那条——通常是 Vercel + Next.js(资料最多)或 Cloudflare + TanStack Start/Hono(成本最低)。第 09 章会给出更细的决策树。
版本快照(2026 年 9 月)
这张表会过时,但它能帮你判断 Agent 给出的代码是否“太旧”(模型的训练数据有截止时间,经常写出上一代 API)。
| 项目 | 当前状态 | 对你意味着什么 |
|---|---|---|
| TypeScript | 7.x(Go 原生编译器) | 类型检查快约 10 倍,语言本身几乎不变 |
| Node.js | 24 为 LTS;26 于 2026 年 10 月转 LTS | 可直接运行 .ts(只剥离类型);新项目选最新 LTS |
| React | 19.x;React Compiler 1.0 已稳定 | 手写 useMemo/useCallback 的需求大幅减少 |
| Next.js | 16.x | App Router 是默认;Turbopack 是默认打包器 |
| TanStack Start | v1 | 基于 Vite 的全栈框架,类型安全路由是招牌 |
| Vite | 8(内置 Rolldown) | 生产构建显著变快 |
| Tailwind CSS | v4 | 配置写在 CSS 里而不是 tailwind.config.js;Agent 常写成 v3 风格 |
| Zod | v4 | 运行时校验的事实标准 |
| AI SDK(Vercel) | v6 | 提供 Agent 抽象(ToolLoopAgent)、工具审批等 |
pnpm tsc --noEmit 和 pnpm build,把输出贴给我。”Intl.RelativeTimeFormat;如果必须加依赖,先列出候选、体积和维护状态,等我确认。”精确版本做了三件事:指定了每一层的选择(包管理、框架、样式、平台),限定了范围(先不加什么),给出了可检查的完成标准。
问:TypeScript 和 JavaScript 是什么关系?TS 代码是怎么运行的?
答题要点:TS 是 JS 的超集,只增加了静态类型系统;类型在编译期检查、运行时被完全擦除。运行前要把 TS 转成 JS——可以由 tsc 完成,但实践中通常由 esbuild / SWC / Vite 或 Node 内置的类型剥离完成,而 tsc 只负责类型检查(--noEmit)。所以类型不能替代运行时校验,外部输入仍需用 Zod 之类的库校验。
自检
1. 同事用 node app.ts 运行成功了,于是说“类型没问题”。他对吗?
不对。Node 的类型剥离只删除类型标注,不检查类型。代码能运行只说明转译后的 JS 语法合法。要确认类型正确,必须跑 tsc --noEmit(或编辑器里的 TS 语言服务)。
2. 一段服务器代码用了 fs.readFileSync 读取本地模板文件,现在要把它部署到 Cloudflare Workers。你预计会遇到什么问题?
Workers 运行在 V8 isolate 里,没有传统意义上可写、可任意读取的本地文件系统。虽然有 Node 兼容层,但“读部署机器上的任意文件”这种假设不成立。通常做法是:在构建时把模板打包进代码、放进静态资源,或存到 R2/KV 中读取。这说明“运行时的形状”会影响代码写法。
3. 为什么 Go 后端不需要“打包器”这个概念,前端却离不开?
因为前端代码要通过网络下载到每个用户的浏览器里执行。文件数量、体积、缓存策略直接决定加载速度。打包器负责合并、摇树、分割、压缩和给文件加哈希。Go 二进制从本地磁盘加载,没有这个问题。