2018 年 6 月,JSConf EU 上发生了一件让整个 JavaScript 社区记忆深刻的事情:Node.js 的创造者 Ryan Dahl 站上讲台,做了一场名为「10 Things I Regret About Node.js」的演讲。
一个框架的作者公开批判自己的作品——这在技术圈不常见。但 Ryan 说的不是客气话,他列出了一系列他认为在 Node.js 设计中犯下的根本性错误:
npm install 就可能让恶意脚本为所欲为。index.js——方便了开发者,却让模块解析逻辑变得极度复杂。fs、path、http),与浏览器的 Web API 体系渐行渐远。这场演讲不只是反思。Ryan 在结尾亮出了他的答案:Deno——一个用 Rust(后期)和 V8 构建的全新 JavaScript/TypeScript 运行时。
Deno 不是 Node 的升级版,而是 Ryan 对「如果重来一次,JS 运行时该怎么设计」这个问题的回答。
Deno 是 Node 的字母重排(anagram):No-De → De-No。这本身就是一种态度——它不是继承,而是重构。
Deno 最初的原型用 Go 编写,但 Ryan 很快意识到两件事:
于是 Deno 转向了 Rust,利用 Rust 的内存安全性和 Tokio 异步运行时来构建底层。这个决定奠定了 Deno 的技术基调:安全、高性能、现代化。
经过两年多的开发,Deno 1.0 正式发布。它带着一套相当理想化的设计理念亮相:
--allow-read、--allow-net 等标志显式授权。tsconfig.json、ts-node、ts-loader 等任何额外配置,deno run main.ts 直接运行。node_modules,通过 URL 直接引入依赖,首次运行时下载并缓存到全局目录。deno fmt、deno lint、deno test、deno doc 全部自带。fetch()、WebSocket、TextEncoder 等浏览器 API 原生可用。// Deno 1.0 风格的代码
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
const server = serve({ port: 8000 })
console.log('Server running on http://localhost:8000')
for await (const req of server) {
req.respond({ body: 'Hello from Deno!\n' })
}1.0 时代的 Deno 非常纯粹,但也因此面临一个尴尬的现实:NPM 生态太大了,完全绕开它不现实。
1.x 阶段是 Deno 从理想走向务实的过程:
--unstable 标志下的 FFI(外部函数接口),允许调用原生库。npm: 前缀,可以直接引入 NPM 包——这是一个重大妥协,也是一个务实的转折。deno.json 配置文件逐渐成熟,支持 import map、编译选项等。package.json 自动检测——Deno 开始能够直接运行包含 package.json 的项目。这个阶段的核心矛盾是:Deno 想保持「干净世界」的设计哲学,但又不得不向 NPM 生态伸出橄榄枝。
Deno 2.0 是一个标志性版本,它正式宣告了 Deno 的务实路线:
npm: 前缀开箱即用,node_modules 可选生成。deno.json 作为统一配置中心:替代了过去散落各处的配置方式。deno.land/x 共存。// Deno 2.0 风格:可以直接用 NPM 包
import express from 'npm:express@4'
const app = express()
app.get('/', (req, res) => {
res.send('Hello from Deno 2.0!')
})
app.listen(3000)到了 2025–2026 年,Deno 的迭代进入了一个「完善期」:
dx 命令:等价于 npx,可以直接运行 npm/JSR 包中的二进制命令。deno audit:内建安全审计工具,检查依赖中的已知漏洞。tsgo 实验性支持:基于 Go 重写的 TypeScript 类型检查器,显著加速类型检查。抛开版本号,Deno 有几条贯穿始终的设计原则:
# 不给任何权限,脚本无法做任何 I/O
deno run script.ts
# 只允许读取当前目录,只允许访问特定域名
deno run --allow-read=. --allow-net=api.example.com script.ts
# 允许所有权限(开发时的偷懒用法)
deno run -A script.tsNode.js 的默认权限模型是「全部允许」,Deno 反过来:默认全部拒绝。这在安全意识日益增强的今天显得尤为重要——尤其是在供应链攻击频发的 NPM 生态中。
Deno 尽可能与浏览器保持一致:
fetch() 而不是 http.request()Request / Response 对象而不是 req / resURL、URLSearchParams、TextEncoder、TextDecoder 全部原生支持这意味着你在 Deno 中写的代码,很多可以几乎不修改地运行在浏览器或 Cloudflare Workers 等边缘环境中。
// 不需要任何配置,直接运行
// deno run greet.ts
function greet(name: string): string {
return `Hello, ${name}!`
}
console.log(greet('Deno'))没有 ts-node、没有 tsx、没有 tsconfig.json 的必要配置。TypeScript 在 Deno 中就是默认语言。
| 能力 | Node.js 需要 | Deno 内建 |
|---|---|---|
| TypeScript | ts-node / tsx | 直接运行 |
| 格式化 | Prettier | deno fmt |
| 代码检查 | ESLint | deno lint |
| 测试 | Jest / Vitest | deno test |
| 文档生成 | TypeDoc | deno doc |
| 安全审计 | npm audit | deno audit |
| 执行包命令 | npx | dx |
| 基准测试 | 手动搭建 | deno bench |
说了这么多背景,来看一个实际的例子。用 Deno 2.6 写一个简单的 REST API:
# 创建项目目录
mkdir my-api && cd my-api
# 初始化 deno.json
deno init生成的 deno.json:
{
"tasks": {
"dev": "deno run --watch -A main.ts"
}
}// main.ts
const kv = await Deno.openKv() // Deno KV:内建的键值存储
// 使用标准库的 Web 标准 HTTP 服务器
Deno.serve({ port: 8000 }, async (req: Request): Promise<Response> => {
const url = new URL(req.url)
if (url.pathname === '/api/notes' && req.method === 'GET') {
const notes: { id: string; text: string }[] = []
for await (const entry of kv.list({ prefix: ['notes'] })) {
notes.push(entry.value as { id: string; text: string })
}
return Response.json(notes)
}
if (url.pathname === '/api/notes' && req.method === 'POST') {
const body = await req.json()
const id = crypto.randomUUID()
const note = { id, text: body.text }
await kv.set(['notes', id], note)
return Response.json(note, { status: 201 })
}
return new Response('Not Found', { status: 404 })
})
console.log('Server running on http://localhost:8000')注意几个亮点:
Deno.serve() 基于 Web 标准的 Request / Response,与 Cloudflare Workers 的 API 几乎一致。Deno.openKv() 是 Deno 内建的键值存储,本地开发时使用 SQLite,部署到 Deno Deploy 时自动切换为分布式数据库。crypto.randomUUID() 是 Web 标准 API,不需要导入任何库。# 开发模式(带文件监听)
deno task dev
# 指定权限运行
deno run --allow-net --allow-read --allow-write main.ts// main_test.ts
import { assertEquals } from 'jsr:@std/assert'
Deno.test('GET /api/notes returns array', async () => {
const res = await fetch('http://localhost:8000/api/notes')
const data = await res.json()
assertEquals(Array.isArray(data), true)
})
Deno.test('POST /api/notes creates a note', async () => {
const res = await fetch('http://localhost:8000/api/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: '测试笔记' }),
})
const data = await res.json()
assertEquals(res.status, 201)
assertEquals(data.text, '测试笔记')
})deno test --allow-net# 安装 deployctl CLI
deno install -gArf jsr:@deno/deployctl
# 部署
deployctl deploy从编写到部署,没有 webpack、没有 tsconfig.json、没有 node_modules、没有 Docker——这就是 Deno 追求的开发体验。
经过这几年的观察和实践,我认为 Deno 在以下场景中表现尤为出色:
到了 2026 年,JavaScript 运行时的格局已经很清晰了:
有意思的是,这三者正在相互影响:
--experimental-permission),这显然是 Deno 的影响。--experimental-strip-types)。竞争的结果是所有人受益。无论你最终选择哪个运行时,Deno 的存在都在推动整个 JavaScript 生态变得更安全、更标准、更现代。
Deno 的故事是一个关于「技术反思」的故事。
Ryan Dahl 没有选择在 Node.js 上修修补补——他选择了重来一次。从 2018 年的那场演讲到 2026 年的今天,Deno 走过了八年。它不再是那个「只有理想主义者才用」的实验品,而是一个拥有 LTS 版本、生产级部署平台、完善 NPM 兼容的成熟运行时。
如果你还没有尝试过 Deno,现在正是一个好时机。不需要迁移你的所有项目,不需要放弃你的 Node 知识——只需要:
curl -fsSL https://deno.land/install.sh | sh
deno run https://examples.deno.land/hello-world.ts然后,体验一下「如果 JavaScript 运行时重来一次」会是什么感觉。