socket.io-client 发布流程详解:从版本号管理到 npm 可信发布与 CDN 分发
本文以 socket.io 官方仓库中的发布说明文档 packages/socket.io-client/RELEASING.md 为主线,逐步骤拆解 socket.io-client 客户端包的完整发布流程:双 package.json 版本号管理、Changelog 生成、TypeScript 编译与 Rollup 打包、Git tag 触发,以及基于 npm Trusted Publishing 的自动化发布和 CDN 产物同步。读完后你将理解一个真实生产级 monorepo 中客户端库"从改版本号到用户可在 npm / CDN 上使用"的全链路机制,并能对照源码验证每一步的实际实现。
发布流程总览:九步走
官方文档 RELEASING.md 给出的完整发布步骤为:
- 更新
package.json中的版本号; - 更新
support/package.esm.json中的版本号; - 执行
conventional-changelog -p angular更新CHANGELOG.md; - 执行
npm run compile编译 TypeScript 源码; - 执行
npm run build生成浏览器用的 bundle; - 提交
package.json、support/package.esm.json、CHANGELOG.md和dist/目录的变更; - 创建形如
socket.io-client@x.y.z的 tag 并推送,由 CI 工作流 .github/workflows/publish.yml 通过 Trusted Publishing 安全地把包发布到 npm; - 创建一个 GitHub Release;
- 把 bundle 拷贝到官方 CDN 仓库,使其可以在
cdn.socket.io/域名下被直接引用。
下面结合仓库中的真实文件,逐步展开每个环节的实现细节。
为什么要维护两个版本号:package.json 与 support/package.esm.json
第一步和第二步看似重复,实则服务于不同产物。当前 packages/socket.io-client/package.json 的版本为 4.8.3,而 support/package.esm.json 内容极简:
{
"name": "socket.io-client",
"version": "4.8.3",
"type": "module"
}
原因在于主 package.json 声明了 "type": "commonjs"(第 17 行),这决定了 Node.js 把包内 .js 文件按 CJS 解析。而客户端同时需要发布 ESM 产物——exports 字段显示,import 条件下 default 指向 ./build/esm/index.js,require 条件下指向 ./build/cjs/index.js,两者各自携带独立的 .d.ts 类型声明。若 build/esm/ 目录下的 .js 仍落在一个 "type": "commonjs" 的包根之下,ESM 语义就会被破坏。
postcompile.sh 脚本第 3 行给出了答案:
cp ./support/package.esm.json ./build/esm/package.json
即在编译后向 build/esm/package.json 复制一份带 "type": "module" 标记的子包描述文件,让 ESM 产物被 Node 正确识别。因此两个文件中的版本号必须保持一致——它们最终都出现在发布物中(前者是 npm 包元信息,后者是 ESM 产物的模块标记),这正是文档把它们列为两个独立步骤、并要求在步骤 6 中一并提交的原因。
用 Conventional Changelog 生成变更日志
第三步要求用 conventional-changelog -p angular 更新 CHANGELOG.md。当前文件呈现出该工具生成的典型格式:
- 顶部是一张版本汇总表,列为 版本 / 发布日期 / UMD 压缩包大小(min+gzip),例如最新的
4.8.3对应14.4 KB; - 每个版本一节,包含按 angular 提交类型归类的 Bug Fixes、Features、Dependencies 等分组,并附带 commit 短哈希与 issue 链接,如 4.8.2 版本中的:
* **bundle** do not mangle the "_placeholder" attribute (bis)
* drain queue before emitting "connect"
采用 angular 预设意味着维护者的 commit message 遵循 Conventional Commits 规范(fix:、feat:、chore: 等前缀加 scope),变更日志才能被自动聚合。这对用户也有实际价值:Changelog 直接记录了依赖变更(如 engine.io-client@~6.6.1、ws@~8.18.3),帮助应用方判断是否需要升级。
编译阶段:npm run compile 到底做了什么
第四步的 npm run compile 对应 package.json 中的脚本定义:
"compile": "rimraf ./build && tsc && tsc -p tsconfig.esm.json && ./postcompile.sh"
它依次做四件事:
rimraf ./build:清理旧产物,保证可重现构建;tsc:按 tsconfig.json 将lib/编译为 CommonJS,outDir为build/cjs/,target为es2018(注释注明对应 Node.js 10,与包声明的"engines": { "node": ">=10.0.0" }一致),declaration: true会产出.d.ts;tsc -p tsconfig.esm.json:按 tsconfig.esm.json 再以module: "esnext"编译一遍到build/esm/;./postcompile.sh:做三项后处理。
postcompile.sh 的完整逻辑值得逐行看:
cp ./support/package.esm.json ./build/esm/package.json
cp -r ./build/esm/ ./build/esm-debug/
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' -e '/debug(/d' ./build/esm/*.js
else
sed -i -e '/debug(/d' ./build/esm/*.js
fi
# for backward compatibility with `const socket = require("socket.io-client")(...)`
echo -e '\nmodule.exports = lookup;' >> ./build/cjs/index.js
- 复制 ESM 标记文件:即前述的
build/esm/package.json; - 派生出
build/esm-debug/:从build/esm/整目录复制一份,之后再从build/esm/*.js中用sed删除所有debug(调用行。于是build/esm/成为剥离了debug包依赖的精简版(浏览器入口走这条路径),而build/esm-debug/保留了调试日志。这一设计直接反映在主package.json的exports中:import+node条件指向./build/esm-debug/index.js,浏览器default指向./build/esm/index.js; - CJS 尾部追加
module.exports = lookup;:脚本中的注释写明这是为了向后兼容const socket = require("socket.io-client")(...)这种直接调用默认导出的旧写法。
值得注意的是 prepack 脚本:"prepack": "npm run compile"。即使发布流程由 CI 执行,npm pack/publish 前也会自动重新编译一次,构成一道安全网。
构建浏览器 Bundle:npm run build 的三路输出
第五步 npm run build 执行三条 Rollup 命令(见 package.json 第 59 行):
"build": "rollup -c support/rollup.config.umd.js && rollup -c support/rollup.config.esm.js && rollup -c support/rollup.config.umd.msgpack.js"
UMD 双产物(开发版 + 压缩版)
support/rollup.config.umd.js 导出两个配置对象,分别产出:
| 产物 | 输入 | 后处理 |
|---|---|---|
dist/socket.io.js |
build/esm-debug/browser-entrypoint.js(保留 debug) |
Babel(@babel/preset-env + object-assign / classes 转换),带 sourcemap |
dist/socket.io.min.js |
build/esm/browser-entrypoint.js(已去 debug) |
额外经 Terser 压缩,mangle 时按正则 /^_/ 处理属性名并保留 _placeholder |
两者都以 umd 格式输出、全局名为 io(因此页面上可用 io(url) 创建连接),文件头带有 banner:
/*!
* Socket.IO v4.8.3
* (c) 2014-2026 Guillermo Rauch
* Released under the MIT License.
*/
其中版本号直接取自 package.json(配置第 6 行 require("../package.json").version)——再次印证了发布前必须先改版本号,否则 bundle banner 也会带错版本。reserved: ["_placeholder"] 这个细节并非偶然:CHANGELOG 4.8.2 条目专门记录了一次 "do not mangle the '_placeholder' attribute (bis)" 的修复,说明压缩保留规则是踩过坑后写死的。
ESM 压缩产物
support/rollup.config.esm.js 从 build/esm/index.js 打包出 dist/socket.io.esm.min.js(format: "esm" + Terser),供现代打包器直接消费 ESM 版本。
MsgPack 变体
support/rollup.config.umd.msgpack.js 复用 UMD 压缩配置的 output,仅改输出文件名并用 @rollup/plugin-alias 做一次关键替换:
alias({
entries: [
{
find: "socket.io-parser",
replacement: "socket.io-msgpack-parser",
},
],
})
即把标准 JSON 解析器 socket.io-parser 在打包时静态替换为 MsgPack 解析器,产出 dist/socket.io.msgpack.min.js,为需要二进制紧凑传输的用户提供免配置的 MsgPack 客户端。
发布物范围
主 package.json 的 files 字段为 ["dist/", "build/"],说明 npm 包最终同时携带 Node 产物(build/cjs、build/esm、build/esm-debug)与浏览器 bundle(dist/),这也解释了为什么文档第 6 步要求把 dist/ 目录一并提交进仓库——bundle 是预先构建好入库的,tag 触发发布时 CI 只需重新编译校验即可。
提交与打 tag:触发自动发布的钥匙
完成前六步并提交后,第 7 步是创建 tag socket.io-client@x.y.z 并推送。这里的 <package>@<version> 格式不是约定俗成,而是发布工作流的硬契约:
.github/workflows/publish.yml 的触发条件为:
on:
push:
tags:
# expected format: <package>@<version> (example: socket.io@1.2.3)
- '**@*'
工作流的权限声明是关键:
permissions:
contents: read
id-token: write
文件开头的注释点明其依赖的两大 npm 机制:trusted publishing(可信发布,凭 GitHub 工作流身份签发 OIDC token 换取 npm 发布权)与 staged publishing(暂存发布,新注册包名的自动审核期)。因此整个仓库不需要也不存储任何 NPM_TOKEN,安全性高于传统的"塞一个 npm 密钥进 secret"模式。
工作流的执行步骤依次是:
actions/checkout@v6检出代码;actions/setup-node@v6安装 Node.js 26 并指向registry.npmjs.org(注意 ci-socket.io-client.yml 中日常 CI 用 Node.js 24,发布用 26);npm ci按 lockfile 精确安装依赖;npm run compile --workspaces --if-present编译全部工作区——因为 package.json 根部的workspaces列出了socket.io-client及其依赖链(engine.io-parser、engine.io-client、socket.io-parser等);- 最终执行:
npm stage publish --workspace=${GITHUB_REF_NAME%@*} --access public
${GITHUB_REF_NAME%@*} 是 Bash 参数展开,去掉 tag 名中第一个 @ 及其之后的部分:tag socket.io-client@4.8.4 → workspace 名 socket.io-client,再配合 npm stage publish 完成带暂存发布语义的正式 publish。也就是说,tag 名本身编码了"发哪个包、发什么版本"的全部信息,这是把多包 monorepo 的发布编排压缩到一条 shell 命令里的巧妙设计。
收尾两步:GitHub Release 与 CDN 同步
第 8 步是在官方 GitHub 仓库创建一个 Release,附上该版本的变更摘要——这也是 CHANGELOG.md 中各版本锚点(如 #483-2025-12-23)对应的用户可见入口。
第 9 步则面向 <script src> 直连场景:把 dist/ 下的 bundle 同步到独立的官方 CDN 仓库,使用户可以直接引用形如 https://cdn.socket.io/4.8.3/socket.io.min.js 的地址,无需经过 npm。这一步是纯文件分发,与 npm 发布相互独立——即使 npm 侧包处于 staged 审核期,CDN 产物依然可用。
对照 CI 理解本地验证
发布前置条件隐含在 CI 中:ci-socket.io-client.yml 展示了改动 packages/socket.io-client/** 等路径时触发的验证链——先依次编译上游依赖(engine.io-parser、engine.io-client、socket.io-parser),再编译本包,然后跑 npm test(默认 Node 端 mocha 用例,test:node 脚本使用 tsx 直接加载 test/index.ts);push 事件下还会以 BROWSERS=1 追加浏览器端测试。发布者在本地执行步骤 4、5 前跑通 npm test,等价于提前复现了 CI 的编译链路。
小结
socket.io-client 的发布流程可以用一条数据流概括:
改版本号(package.json + support/package.esm.json)
→ conventional-changelog -p angular 更新 CHANGELOG.md
→ npm run compile(rimraf + 双 tsc + postcompile.sh:esm 标记 / esm-debug / 去 debug / cjs 兼容)
→ npm run build(UMD 双产物 + ESM min + MsgPack 变体)
→ 提交元信息与 dist/
→ push tag socket.io-client@x.y.z
→ publish.yml:Node 26 + npm ci + 全工作区编译 + npm stage publish(可信发布,无 token)
→ GitHub Release + CDN 仓库同步
其工程要点值得借鉴:版本号在元信息与构建产物中多处出现,靠脚本而非人工保证一致性(banner、esm 子包均由版本号派生);发布凭证零落库(OIDC 可信发布);tag 命名即发布指令(<package>@<version> 被 shell 展开直接解析出 workspace);npm 包与浏览器 bundle 双通道分发(build/ 面向 Node,dist/ 面向打包器与 CDN)。以上全部细节均可在仓库的 packages/socket.io-client 目录及 .github/workflows 中逐一对照验证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00