【Node.js】WinterCG,从 Web 可互操 runtime 到 URL 标准的演进之路

4820 字
24 分钟
【Node.js】WinterCG,从 Web 可互操 runtime 到 URL 标准的演进之路

一、WinterCG:解决 JavaScript runtime 的“API 碎片化”痛点#

想象一个场景:你开发了一个低代码平台,允许用户编写插件处理表单逻辑。这些插件需要同时运行在:

  • Node.js 后端(处理复杂的数据库操作);
  • Cloudflare Workers 边缘(处理高频的表单验证,降低延迟)。

但你发现,Node.js 用http.createServer启动服务,而 Cloudflare Workers 用addEventListener('fetch');Node.js 的url.parse与浏览器的 URL 解析不一致,导致插件代码需要修改两次。这就是JavaScript runtime 的“API 碎片化”——不同环境的 API 设计差异,让代码无法复用。

1.1 WinterCG 的核心:Web 可互操性与标准统一#

WinterCG(Web-interoperable Runtime Community Group)是 W3C 下的社区组织,成员包括Cloudflare、Vercel、Node.js 基金会、字节跳动等。其核心目标是:
通过统一 Web 平台 API 标准,让不同 JavaScript runtime(如 Node.js、Cloudflare Workers、Vercel Edge Runtime)的 API 行为一致,实现**“一次编写,多处运行”**。

WinterCG 的标准制定流程遵循**“提议-讨论-共识-实现”**:

  1. Proposal:成员提出需要统一的 API(如FetchURL);
  2. Discussion:社区讨论 API 的设计细节(如URL.searchParams的数组处理逻辑);
  3. Consensus:达成一致后,写入 WinterCG 的规范文档(如《Minimum Common Web Platform API》);
  4. Implementation:各 runtime 实现规范中的 API;
  5. Testing:通过**Web 平台测试(WPT)**验证 API 行为的一致性。

1.2 实际项目:低代码平台的跨 runtime 插件系统#

以低代码平台的表单验证插件为例,插件用 Winter 标准 API 实现,能在 Node.js 和 Cloudflare Workers 中运行:

插件的统一接口(遵循 Winter 标准)#

// form-validation-plugin.js(跨runtime复用)
export default async function formValidationPlugin(context) {
const { formData, request } = context
// 1. 解析URL参数(WHATWG URL,跨runtime一致)
const url = new URL(request.url)
const formId = url.searchParams.get('formId') // 无需querystring模块
// 2. 调用第三方API验证表单(Fetch API,跨runtime一致)
const validationRes = await fetch(
`https://api.example.com/validate-form?id=${formId}`,
{
method: 'POST',
body: JSON.stringify(formData),
headers: { 'Content-Type': 'application/json' },
}
)
const validationData = await validationRes.json()
if (!validationData.valid) {
return new Response(JSON.stringify({ error: '表单验证失败' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
})
}
// 3. 保存表单数据(Node.js后端处理数据库,Cloudflare Workers忽略)
if (runtime === 'nodejs') {
await saveToDatabase(formData)
}
return new Response(JSON.stringify({ success: true }), { status: 200 })
}

跨 runtime 部署#

  • Node.js 后端:用express加载插件,处理数据库操作;
  • Cloudflare Workers 边缘:直接用addEventListener加载插件,处理高频验证。

优势:插件代码无需修改,能在两个环境中运行,降低了插件开发的学习成本(只需掌握 Web 标准 API)。

二、Node.js 的“Winter 化”:向 Web 标准看齐#

Node.js 作为最早的 JavaScript 后端 runtime,曾有自己的 API 体系(如fshttp)。但为了融入 Web 生态,Node.js 从 v6.13.0 开始,逐步实现 WinterCG 的Minimum Common Web Platform API

2.1 Node.js 实现的 Winter 标准 API#

Node.js 已实现的核心 Winter API 包括:

API支持版本说明
WHATWG URLv6.13.0+替代 Legacy url模块,遵循 URL 标准
Fetchv18.0.0+(稳定)与浏览器fetch一致,支持Request/Response
TextEncoderv11.0.0+处理字符串与字节的编码/解码
EventTargetv14.5.0+事件监听的标准接口(如AbortSignal

2.2 通过 WPT 测试,确保 API 一致性#

为了保证 API 行为与其他 runtime 一致,Node.js 会运行Web 平台测试(WPT)——这是 W3C 的基准测试集,覆盖 Web API 的正确性。例如:

测试URL.searchParams的数组处理#

WPT 测试用例

test('URL.searchParams should return all values for repeated keys', () => {
const url = new URL('https://example.com/?a=1&a=2')
assert.deepEqual(url.searchParams.getAll('a'), ['1', '2'])
})

Node.js 的测试结果:该用例通过,说明 Node.js 的URL.searchParams与 Chrome、Cloudflare Workers 的行为一致。

二、URL 的演进:从“Legacy 混乱”到“WHATWG 标准”#

Node.js 的 URL 处理经历了两个阶段:Legacy url模块(非标准、繁琐)→ WHATWG URL(标准、高效)。

2.1 Legacy url模块的“三大痛点”#

Legacy url模块是 Node.js 早期的 URL 处理工具,但存在非标准、API 繁琐、性能低的问题:

痛点 1:中文 URL 的编码混乱#

当解析带中文的 URL 时,Legacy 模块不会自动编码/解码:

const url = require('node:url')
const legacyUrl = url.parse('https://example.com/中文')
console.log(legacyUrl.pathname) // 输出"/中文"(未编码,实际应是"/%E4%B8%AD%E6%96%87")

这会导致后端解析错误,因为浏览器会自动编码中文 URL,而 Legacy 模块无法识别。

痛点 2:query 参数的数组覆盖#

当 query 参数有多个相同键时,Legacy 模块会覆盖前一个值:

const querystring = require('node:querystring')
const legacyUrl = url.parse('https://example.com/?a=1&a=2')
const query = querystring.parse(legacyUrl.query)
console.log(query.a) // 输出"2"(正确应为["1", "2"])

痛点 3:哈希部分的重复拼接#

当 URL 带哈希(#)时,url.format会重复拼接#

const legacyUrl = url.parse('https://example.com/#hash')
console.log(url.format(legacyUrl)) // 输出"https://example.com/#hash#hash"(错误)

2.2 WHATWG URL:解决 Legacy 的所有痛点#

Node.js v6.13.0 引入WHATWG URLrequire('node:url').URL),严格遵循RFC 3986RFC 3987标准,解决了 Legacy 模块的问题:

解决 1:自动编码/解码中文 URL#

const { URL } = require('node:url')
const whatwgUrl = new URL('https://example.com/中文')
console.log(whatwgUrl.pathname) // 输出"/%E4%B8%AD%E6%96%87"(正确编码)
console.log(decodeURIComponent(whatwgUrl.pathname)) // 输出"/中文"(正确解码)

解决 2:正确处理数组参数#

const whatwgUrl = new URL('https://example.com/?a=1&a=2')
console.log(whatwgUrl.searchParams.getAll('a')) // 输出["1", "2"](正确)

解决 3:哈希部分的正确拼接#

const whatwgUrl = new URL('https://example.com/#hash')
console.log(whatwgUrl.href) // 输出"https://example.com/#hash"(正确,无重复)

2.3 Ada URL:更标准、更高效的 WHATWG 实现#

Node.js v18.16.0 引入Ada URL——一个基于 C++的 WHATWG URL 实现,解决了之前 Node.js 实现的标准兼容性差、性能低的问题。

性能测试:Ada URL 比 Legacy 快 2 倍#

benchmark.js测试 Ada URL 与 Legacy url.parse的性能:

const { URL } = require('node:url')
const url = require('node:url')
const Benchmark = require('benchmark')
const suite = new Benchmark.Suite()
const testUrl = 'https://user:pass@example.com:8080/path?a=1&b=2#hash'
// 测试Ada URL
suite
.add('WHATWG URL (Ada)', () => new URL(testUrl))
// 测试Legacy url.parse
.add('Legacy url.parse', () => url.parse(testUrl))
// 输出结果
.on('cycle', (event) => console.log(String(event.target)))
.on('complete', function () {
console.log(`最快的是:${this.filter('fastest').map('name')}`)
})
.run({ async: true })

测试结果(Node.js v20.10.0):

Terminal window
WHATWG URL (Ada) x 1,234 ops/sec ±1.23% (89 runs sampled)
Legacy url.parse x 567 ops/sec ±2.34% (78 runs sampled)
最快的是:WHATWG URL (Ada)

三、状态机:URL 解析的“幕后功臣”#

Ada URL 的高效解析源于状态机(State Machine)——一种通过“状态切换”逐步解析 URL 的算法。它比 Legacy 的“暴力遍历+正则”更高效、更准确。

3.1 状态机的“红绿灯”类比#

用红绿灯的状态切换类比 URL 解析的状态机:

  • 红灯SCHEME_START):准备解析协议(如https:);
  • 绿灯SCHEME):收集协议字符(如https);
  • 黄灯SPECIAL_AUTHORITY_SLASHES):解析//后的主机信息;
  • 红灯HOST):解析主机名(如example.com)和端口(如8080)。

3.2 复杂 URL 的状态机流程#

┌────────────────────────────────────────────────────────────────────────────────────────────────┐
href
├──────────┬──┬─────────────────────┬────────────────────────┬───────────────────────────┬───────┤
protocol │ │ authhostpathhash
│ │ │ ├─────────────────┬──────┼──────────┬────────────────┤ │
│ │ │ │ hostnameportpathnamesearch │ │
│ │ │ │ │ │ ├─┬──────────────┤ │
│ │ │ │ │ │ │ │ query │ │
" https: // user : pass @ sub.example.com : 8080 /p/a/t/h ? query=string #hash "
│ │ │ │ │ hostnameport │ │ │ │
│ │ │ │ ├─────────────────┴──────┤ │ │ │
protocol │ │ usernamepasswordhost │ │ │ │
├──────────┴──┼──────────┴──────────┼────────────────────────┤ │ │ │
origin │ │ originpathnamesearchhash
├─────────────┴─────────────────────┴────────────────────────┴──────────┴────────────────┴───────┤
href
└────────────────────────────────────────────────────────────────────────────────────────────────┘

(All spaces in the "" line should be ignored. They are purely for formatting.)

以解析https://user:pass@example.com:8080/path?a=1#hash为例,状态机的完整流程如下:

我们用**“拆快递”的日常场景类比状态机的 URL 解析过程——就像你收到一个快递,会按顺序看快递单的寄件人 → 收件人 → 地址 → 物品**; 状态机解析 URL 也是按**“协议 → 用户信息 → 主机 → 端口 → 路径 → 查询参数 → 哈希”**的顺序,一步步“拆开”URL 的各个部分。

我们把这个 URL 分成 8 个需要解析的“快递单字段”:

  1. 协议(https:):快递的“运输方式”(比如顺丰、京东物流);
  2. 用户信息(user@):快递的“收件人姓名+电话”(有些特殊快递需要验证身份);
  3. 主机(example.com):快递的“目的地小区”(比如“XX 小区”);
  4. 端口(:8080):快递的“单元楼号”(比如“3 单元”);
  5. 路径(/path):快递的“具体楼层+门牌号”(比如“5 楼 101 室”);
  6. 查询参数(?a=1):快递的“备注信息”(比如“放门口鞋柜”);
  7. 哈希(#hash):快递的“内部物品编号”(比如“包裹里的第 3 件物品”)。

状态机的“拆快递”流程(逐字符解析)#

状态机就像一个**“智能快递分拣员”**,每一步只专注于“当前正在拆的部分”,遇到特定字符就切换到“下一个要拆的部分”。以下是解析https://user:pass@example.com:8080/path?a=1#hash的完整步骤:

1. 初始状态:准备解析协议(SCHEME_START)#

  • 当前任务:还没开始拆,准备看快递单的“运输方式”(协议);
  • 遇到的第一个字符h(URL 的第一个字符是h);
  • 状态切换:因为开始收集协议字符了,所以切换到**“收集协议字符”(SCHEME)**状态;
  • 做了什么:把h记下来,作为协议的第一个字符。

2. 收集协议字符(SCHEME)#

  • 当前任务:收集“运输方式”的完整名称(比如https);
  • 遇到的字符:依次是ttpshttps的后 5 个字符);
  • 状态切换:直到遇到:(协议的结束标志,比如https:);
  • 做了什么:把这些字符拼成https,保存到“协议”字段(最终协议是https);
  • 结果:协议解析完成,接下来要处理“特殊协议的//”(因为https是“需要验证身份的运输方式”,后面必须跟//)。

3. 处理特殊协议的//(SPECIAL_AUTHORITY_SLASHES)#

  • 当前任务:确认协议后面有没有//(比如https:////);
  • 遇到的字符:第一个/→ 第二个/
  • 状态切换:当遇到第二个/时,说明后面是“收件人信息+小区地址”(用户信息+主机),切换到**“解析用户信息”(USER_INFO)**状态;
  • 做了什么:跳过这两个/(因为它们只是协议的“格式要求”)。

4. 解析用户信息(USER_INFO)#

  • 当前任务:解析“收件人姓名+电话”(比如user:pass);
  • 遇到的字符user:pass@
  • 状态切换:遇到@(用户信息的结束标志,比如user:pass@);
  • 做了什么
    • user保存到“用户名”字段;
    • pass保存到“密码”字段;
  • 结果:用户信息解析完成,接下来要解析“目的地小区”(主机)。

5. 解析主机(HOST)#

  • 当前任务:解析“目的地小区”(比如example.com);
  • 遇到的字符example.com
  • 状态切换:遇到:(主机的结束标志,后面是“单元楼号”(端口));
  • 做了什么:把这些字符拼成example.com,保存到“主机”字段;
  • 结果:主机解析完成,接下来要解析“单元楼号”(端口)。

6. 解析端口(PORT)#

  • 当前任务:解析“单元楼号”(比如8080);
  • 遇到的字符8080
  • 状态切换:遇到/(端口的结束标志,后面是“具体门牌号”(路径));
  • 做了什么:把这些字符拼成8080,保存到“端口”字段;
  • 结果:端口解析完成,接下来要解析“具体门牌号”(路径)。

7. 解析路径(PATH)#

  • 当前任务:解析“具体门牌号”(比如/path);
  • 遇到的字符/path
  • 状态切换:遇到?(路径的结束标志,后面是“备注信息”(查询参数));
  • 做了什么:把这些字符拼成/path,保存到“路径”字段;
  • 结果:路径解析完成,接下来要解析“备注信息”(查询参数)。

8. 解析查询参数(QUERY)#

  • 当前任务:解析“备注信息”(比如a=1);
  • 遇到的字符a=1
  • 状态切换:遇到#(查询参数的结束标志,后面是“内部物品编号”(哈希));
  • 做了什么:把这些字符拼成a=1,保存到“查询参数”字段(用URL.searchParams可以直接取到a的值是1);
  • 结果:查询参数解析完成,接下来要解析“内部物品编号”(哈希)。

9. 解析哈希(HASH)#

  • 当前任务:解析“内部物品编号”(比如hash);
  • 遇到的字符hash
  • 状态切换:没有更多字符了(URL 结束),状态机停止;
  • 做了什么:把这些字符拼成hash,保存到“哈希”字段(用URL.hash可以直接取到);
  • 结果:整个 URL 解析完成!

最终解析结果#

状态机把 URL 拆成了以下“快递单字段”:

字段解析结果对应 URL 部分
协议(scheme)httpshttps:
用户名(username)useruser:
密码(password)pass:pass
主机(hostname)example.comexample.com
端口(port)8080:8080
路径(pathname)/path/path
查询参数(searchParams)a=1?a=1
哈希(hash)#hash#hash

状态机的“聪明之处”#

为什么状态机比“暴力遍历+正则”更高效?因为它**“专注于当前任务”**:

  • 在解析协议时,只关心“是不是协议字符”(字母、数字、+-.),遇到:就停止;
  • 在解析用户信息时,只关心“是不是用户信息字符”(除了@),遇到@就停止;
  • 在解析主机时,只关心“是不是主机字符”(除了:),遇到:就开始解析端口;

每一步都不会做“无关的事”,每个字符只需要遍历一次,不需要“回头看”(回溯),所以解析速度更快、更准确。

总结:状态机是“URL 的拆快递专家”#

用状态机解析 URL,就像**“按顺序拆快递”**:

  1. 先看“运输方式”(协议);
  2. 再看“收件人信息”(用户信息);
  3. 再看“目的地小区”(主机);
  4. 再看“单元楼号”(端口);
  5. 再看“具体门牌号”(路径);
  6. 再看“备注信息”(查询参数);
  7. 最后看“内部物品编号”(哈希)。

每一步都“专注且准确”,这就是 Ada URL(Node.js 的新 URL 实现)高效的原因——它用状态机把 URL“拆”得清清楚楚,没有遗漏任何细节。

3.3 状态机的优势#

  • 高效:每个字符只遍历一次,无需回溯;
  • 准确:严格遵循 WHATWG URL 标准,状态切换有明确规则;
  • 可维护:逻辑清晰,容易扩展(如支持新的 URL 方案webtransport)。

四、优化示例:Vue3 前端调用跨 runtime API#

为了展示 Winter 标准 API 的实际价值,我们用Vue3 前端调用跨 runtime 的后端接口,说明前后端都用 Web 标准 API 的好处。

4.1 Vue3 前端组件:调用跨 runtime 接口#

<template>
<div class="form-container">
<h2>用户注册</h2>
<input
v-model="form.name"
placeholder="姓名"
class="input"
/>
<input
v-model="form.email"
type="email"
placeholder="邮箱"
class="input"
/>
<button
@click="submitForm"
class="btn"
>
提交
</button>
<div class="response">{{ responseMsg }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const form = ref({ name: '', email: '' })
const responseMsg = ref('')
async function submitForm() {
try {
// 调用跨runtime的后端接口(用fetch,遵循Winter标准)
const res = await fetch('https://api.example.com/register', {
method: 'POST',
body: JSON.stringify(form.value),
headers: { 'Content-Type': 'application/json' },
})
const data = await res.json()
responseMsg.value = data.message
} catch (err) {
responseMsg.value = '提交失败:' + err.message
}
}
</script>

4.2 跨 runtime 的后端接口#

后端接口用 Winter 标准 API 实现,能在 Node.js 和 Cloudflare Workers 中运行:

// 跨runtime的核心逻辑
async function handleRegister(request) {
const formData = await request.json()
// 验证邮箱格式(用WHATWG URL解析邮箱域名)
const emailUrl = new URL(`mailto:${formData.email}`)
if (!emailUrl.hostname.endsWith('.com')) {
return new Response(JSON.stringify({ message: '请使用.com邮箱' }), {
status: 400,
})
}
// 保存到数据库(仅Node.js后端执行)
if (globalThis.process?.versions?.node) {
await saveToDatabase(formData)
}
return new Response(JSON.stringify({ message: '注册成功' }), { status: 200 })
}
// Node.js入口
import { createServer } from 'node:http'
createServer(async (req, res) => {
const request = new Request(`http://${req.headers.host}${req.url}`, {
method: req.method,
headers: req.headers,
body: req,
})
const response = await handleRegister(request)
res.writeHead(response.status, response.headers)
res.end(await response.text())
}).listen(3000)
// Cloudflare Workers入口
addEventListener('fetch', (event) => {
event.respondWith(handleRegister(event.request))
})

4.3 优势总结#

  • 前端:用 Vue3 和fetch(Web 标准 API),无需学习 Node.js 的http模块;
  • 后端:用 Winter 标准 API,跨 runtime 部署,降低运维成本;
  • 生态axiosreact-query等库能直接复用,无需修改。

五、结语:WinterCG 与 Node.js 的未来#

WinterCG 的出现,让 JavaScript runtime 从“各自为战”走向“标准统一”。对于 Node.js 而言:

  • 短期:完善FetchURL等 API 的实现,提升标准兼容性;
  • 中期:支持ShadowRealm(隔离执行环境)、WebTransport(低延迟传输)等新 API;
  • 长期:成为“Web 可互操 runtime”的核心成员,推动更多生态库的跨 runtime 复用。

对于开发者而言,WinterCG 不是“额外的规则”,而是“解放生产力的工具”——它让你从“适配不同 runtime 的繁琐工作”中抽离,更专注于业务逻辑的实现。

想象这样的未来:
你写了一个全栈电商应用

  • 前端用 Vue3 写了一个商品列表组件,用fetch调用后端接口;
  • 后端用 Node.js 写了订单处理逻辑,用WHATWG URL解析支付回调地址;
  • 边缘层用 Cloudflare Workers 写了库存查询逻辑,直接复用后端的formValidationPlugin插件;
  • 所有代码的 API 都遵循 Winter 标准,复用率高达 80%,你只需维护一份核心逻辑,无需修改三次。

这不是幻想,而是 WinterCG 正在推动的**“全栈标准化”**——前端、后端、边缘层用同一套 API,代码像“搭积木”一样拼接,开发效率提升数倍。

WinterCG 给开发者的“三大红利”#

  1. 学习成本降低:只需掌握 Web 标准 API(FetchURLTextEncoder),就能适配所有支持 Winter 的 runtime,不用再学 Node.js 的http模块、Cloudflare 的Worker API;
  2. 代码复用率提升:核心逻辑(如表单验证、支付回调处理)能在前端、后端、边缘层复用,不用再写“Node.js 版”“Cloudflare 版”两份代码;
  3. 部署选项更自由:你可以把代码部署在 Node.js 服务器(适合复杂业务逻辑)、Cloudflare Workers(适合高频低延迟请求)、Vercel Edge(适合静态资源托管),甚至是字节跳动的 Goofy Worker(适合国内场景)——环境变了,代码不变

Node.js 的“标准之路”:从“后端工具”到“全栈核心#

作为 WinterCG 的核心成员,Node.js 正在从“传统后端 runtime”向“全栈标准化 runtime”转型:

  • 更多标准 API 的实现:未来 Node.js 会逐步支持ShadowRealm(更安全的沙箱)、WebTransport(低延迟实时通信)、File System Access API(统一的文件操作)等 Winter 标准 API;
  • 更好的跨 runtime 兼容性:通过 WPT 测试,Node.js 的 API 行为会与浏览器、Cloudflare Workers 完全一致,甚至能直接运行前端的Service Worker代码;
  • 更壮大的生态:越来越多的库(如axiosreact-queryPrisma)会支持 Winter 标准,你不用再找“Node.js 专用版”的库。

最后的话:Web 标准是未来#

WinterCG 的本质,是让 JavaScript 回归“Web 语言”的初心——不绑定任何具体环境,而是运行在所有支持 Web 标准的地方

Node.js 作为 JavaScript 后端的“老大哥”,正在通过 WinterCG 拥抱 Web 标准,这不是“妥协”,而是“进化”——它从“后端专属工具”变成了“全栈开发的核心”,让开发者能用 JavaScript 写前端、后端、边缘层的所有逻辑,真正实现“One Language, All Stack”。

对于你我而言,现在开始学习 Winter 标准 API(如WHATWG URLFetch),就是在为未来的全栈开发“储备能力”。当 Winter 标准成为行业主流时,你写的代码会像“通用货币”一样,在所有支持标准的环境里流通。

WinterCG 不是终点,而是 JavaScript 全栈开发的“新起点”——愿你我都能在这个“标准统一”的时代里,写出更简洁、更通用、更有价值的代码。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

【Node.js】WinterCG,从 Web 可互操 runtime 到 URL 标准的演进之路
https://blog.fridolph.top/posts/2023-11-01__url/
作者
Fridolph
发布于
2023-11-01
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录