后端Web框架【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa. https://307.run/eggcode项目地址https://gitcode.com/gh_mirrors/eg/egg点击查看免费下载本文介绍 Egg 框架基于 Node.js V8 Startup Snapshot 的启动加速方案在构建期把完全加载好的应用框架元数据、插件、Service、Router、tegg 模块固化成可序列化的堆快照blob恢复时跳过绝大部分启动开销直接续跑剩余生命周期并监听端口。读完本文你将掌握egg-bin snapshot build构建、egg-scripts start --snapshot-blob恢复的完整命令行流程理解buildSnapshot/restoreSnapshot对外 API 与四个快照生命周期钩子的用法并具备定位哪个模块不可快照化以及修复构建/恢复失败的实战能力。什么是 V8 启动快照Egg 可以把一个完全加载好的应用固化成 V8 启动快照从而让冷启动跳过绝大部分启动开销。构建快照时会加载整个模块图——框架元数据、插件、Service、Router 和 tegg 模块——并把生命周期跑到configWillLoad再把此时的堆序列化成一个 blob。恢复 blob 时只需继续执行剩余生命周期didReady并开始监听进程几乎可以立即对外服务。它构建在 Bundle 部署 之上快照是从单文件自包含 bundle 产出的因此可以把快照理解为「预启动好的 bundle」。从仓库源码看这个设计贯穿了三个包的分工构建期egg-bin的snapshot build命令tools/egg-bin/src/commands/snapshot.ts以快照模式打包应用然后包裹执行node --snapshot-blob blob --build-snapshot worker.js打包期eggjs/egg-bundler保持网络栈 external 且惰性加载见 tools/egg-bundler/src/lib/Bundler.ts 中snapshotLazyModules相关逻辑恢复期egg-scripts start --snapshot-blob以单进程方式从 blob 启动见 tools/scripts/src/commands/start.ts。Node.js 版本要求阶段命令Node.js构建快照egg-bin snapshot build 22恢复运行egg-scripts start --snapshot-blob 24::: warning 恢复必须使用 Node.js 24 快照可以在 Node.js 22 上构建但在 Node.js 22 上恢复一个非平凡的 Egg 堆时进程会在反序列化阶段以原生 fatal 错误崩溃Check failed: current end_slot_index属于 V8 的 bug。请始终在 Node.js 24上恢复。受支持的启动方式会强制拦截egg-scripts start --snapshot-blob在 Node.js 24 时会在启动任何进程前直接报清晰错误并拒绝启动。如果你绕过它、在 Node.js 22 上直接运行node --snapshot-blob进程仍会在反序列化阶段以上面的原生 fatal 崩溃——快照自带的运行时拦截只能在「能完成反序列化但仍低于 24」的版本上打印友好提示。因此请始终通过egg-scripts在 Node.js 24 上恢复。 :::这条版本约束在源码中可以得到印证egg-scripts的start命令会在 spawn 之前探测实际执行的 Node 主版本若小于 24 则直接抛出egg-scripts start --snapshot-blob requires Node.js 24 to restore a V8 snapshot错误见 tools/scripts/src/commands/start.ts。而egg-bin snapshot build在构建成功后也会打印提示恢复该快照需要 Node.js 24见 tools/egg-bin/src/commands/snapshot.ts。使用 CLI 构建与恢复推荐使用 bundler CLI。注意没有egg-bin snapshot start构建属于构建期egg-bin恢复属于生产运行期egg-scripts。构建 blob# 以快照模式打包单文件自包含 worker.js prelude并自动执行 # node --snapshot-blob blob --build-snapshot worker.js $ egg-bin snapshot build默认会把 bundle 写到./dist-bundleblob 写到./dist-bundle/snapshot.blob。常用参数参数说明--output dirbundle 输出目录worker.js所在目录。--blob path快照 blob 路径默认output/snapshot.blob。--force-external始终保持 external 的包可重复见「已知限制」。--skip-bundle从已有的worker.js构建 blob跳过打包。--inline-external pkg把被自动 externalize 的包强制塞回 bundle可重复。--pack-alias spectarget打包期重定向某个模块说明符可重复。--dry-run只打印将被执行的node --build-snapshot命令不实际 spawn。--mode modebundle 构建模式production默认或development。--framework pkg框架包名默认egg或读取pkg.egg.framework。以上标志的默认值与含义均与 tools/egg-bin/src/commands/snapshot.ts 中的 oclif flag 定义一一对应--output默认./dist-bundle--blob默认output/snapshot.blob--mode默认production。构建流程在源码中分为三步见 tools/egg-bin/src/commands/snapshot.ts若非--skip-bundle调用eggjs/egg-bundler的bundle()以snapshot: true模式打包产出单文件自包含worker.js与 prelude以继承 stdio 的方式 spawnnode --snapshot-blob blob --build-snapshot worker.js并通过环境变量EGG_BUNDLE_SNAPSHOTbuild让生成的入口进入快照构建模式加载元数据、执行snapshotWillSerialize钩子、注册 deserialize 主函数构建结束后校验 blob 文件确实存在否则显式报错snapshot build finished but no blob was written at pathnode --build-snapshot可能在没写出 blob 的情况下以 0 退出。值得一提的实现细节该命令用spawn而不是fork来启动子进程因为 IPC channel 是一种不可序列化的 libuv 资源会破坏--build-snapshot同时它会把SIGINT/SIGTERM/SIGQUIT转发给子进程避免构建中途 Ctrl-C 时留下孤儿进程。恢复并提供服务直接用egg-scripts从 blob 启动进程Node.js 24$ egg-scripts start --snapshot-blob ./dist-bundle/snapshot.blob --port 7001它会启动一个单文件自包含的node --snapshot-blob blob进程没有 egg-cluster也不做框架解析。快照主函数从PORT或--port读取监听端口执行snapshotDidDeserialize钩子然后调用app.listen()。从源码看tools/scripts/src/commands/start.tssnapshot 启动路径与 cluster 路径共享同一套 spawn daemon/foreground 生命周期管理只是最终 argv 不同[..., --snapshot-blob, blob, --titleegg-server-name-snapshot]且egg-scripts stop可通过进程名 grep 到它。对外 API如果需要自定义入口文件Egg 也从egg导出两个方法import { buildSnapshot, restoreSnapshot } from egg;buildSnapshot()会以快照模式启动 Egg加载元数据触发非可序列化资源的清理钩子并把应用对象写入 V8 快照负载。restoreSnapshot()会从快照中恢复应用重建运行期资源并继续执行剩余的 Egg 生命周期。这两个方法的实现位于 packages/egg/src/lib/snapshot.tsbuildSnapshot(options)packages/egg/src/lib/snapshot.ts调用startEgg({ ...options, snapshot: true })以快照模式启动先触发 agent若存在再触发 app 的snapshotWillSerialize最后通过v8.startupSnapshot.setDeserializeMainFunction()把应用对象注册为反序列化回调负载存入globalThis.__egg_snapshot_apprestoreSnapshot()packages/egg/src/lib/snapshot.ts从globalThis.__egg_snapshot_app取回应用先触发 agent 再触发 app 的snapshotDidDeserialize钩子返回已恢复的Application实例。构建入口import { buildSnapshot } from egg; await buildSnapshot({ baseDir: import.meta.dirname, });node --snapshot-blobsnapshot.blob --build-snapshot snapshot-entry.mjs恢复入口Node.js 24import { restoreSnapshot } from egg; const app await restoreSnapshot(); await app.listen(7001);restoreSnapshot()会从configDidLoad继续执行正常启动流程直到didReady因此返回的app已经可以继续执行运行期初始化逻辑例如启动服务或建立外部连接。工作原理在构建阶段Egg 会以snapshot: true运行。此时 Egg 会加载应用元数据但在configWillLoad之后停止因此configDidLoad、didLoad、willReady、didReady和serverDidReady等钩子都会延后到恢复阶段执行。在序列化堆之前Egg 会运行snapshotWillSerialize钩子释放不可序列化的资源timer、socket、原生句柄、logger 流。网络栈node:http、node:https、TLS/DNS、HTTP client会保持external 且惰性加载因此它们不可序列化的原生绑定不会被写进 blob而是在恢复后首次使用时重建。恢复阶段V8 先反序列化堆随后快照主函数运行snapshotDidDeserialize钩子重建这些运行期资源跑完延后的生命周期直到didReady然后开始监听。在打包器层面这一机制通过三步实现见 tools/egg-bundler/src/lib/Bundler.ts强制单文件V8 启动快照禁止在用户态require兄弟 chunk因此快照模式下即使应用配置了多 chunk也会强制singleFile: trueBundler.ts中const singleFile snapshot ? true : mergedPack?.singleFile网络栈保持 external默认的snapshotLazyModules覆盖 Node 网络栈 id并通过externalsMap传给utoo/pack避免http/tls/dns被内联后在快照构建期加载因不可序列化而失败注入惰性 external 派发在序列化前读取每个 external 的导出名把惰性 hook 注入到生成的externalRequirehelper 体中并把 prelude 前置到每个入口的worker.js之前。若检测到externalRequire被生成但惰性 hook 注入失败bundler 会故意 fail closed而不是产出一个会在构建期加载网络栈的 blob见 tools/egg-bundler/src/lib/Bundler.ts。保持 external 且惰性加载的模块集合默认是 Node 网络栈http、https、http2、tls、dns、inspector含它们的node:形式。如果该列表之外的某个 builtin 在 import 时初始化了原生状态可以通过应用package.json里的egg.snapshot.lazyModules扩展这个集合{ egg: { snapshot: { lazyModules: [node:zlib] } } }它会被合并到默认值之上即默认列表无需自行重复列出在构建期被打桩、在恢复时被真实加载。当某个第三方依赖或 builtin 破坏了构建或恢复时参见 快照故障排查了解如何定位罪魁祸首模块并修复。快照生命周期钩子如果你的app.js或agent.jsBoot 类管理了不能直接写入 V8 快照的资源可以实现下面两个钩子class AppBootHook { async snapshotWillSerialize() { // 在写入快照前关闭或解绑不可序列化资源 } async snapshotDidDeserialize() { // 在恢复后重新创建这些资源 } } module.exports AppBootHook;snapshotWillSerialize()会在写入快照前执行。snapshotDidDeserialize()会在进程从快照启动后执行。这两个钩子适合处理 timer、socket、process listener、logger 等需要在真实运行期重新建立的资源。需要特别注意的是 agent 进程在单进程快照模式下agent.jsBoot 类的钩子也会运行——agent 的snapshotWillSerialize/snapshotDidDeserialize在 app 的之前触发这一点在 packages/egg/src/lib/snapshot.ts 与 packages/egg/src/lib/snapshot.ts 中可以看到无论构建还是恢复都先app.agent.trigger...再app.trigger...——因此agent.js持有的资源需要同样的处理且恢复失败的错误也可能来自 agent 钩子。一个完整的实现示例class AppBootHook { constructor(app) { this.app app; } async snapshotWillSerialize() { // 在写入 blob 前关闭/解绑不可序列化资源 clearInterval(this.timer); this.timer null; } async snapshotDidDeserialize() { // 在恢复后的真实进程里重建 this.timer setInterval(() this.app.doWork(), 1000); } } module.exports AppBootHook;性能由于模块图已经加载、应用也已启动到configWillLoad恢复阶段只需要付出didReady与连接/监听的成本。在 cnpmcore 上实测启动方式恢复 → 监听普通 bundle 启动~942 ms快照恢复~233 ms快约 4 倍模块图越大插件、tegg 模块、Router 越多收益越明显——这正是快照在构建期提前承担的开销。已知限制恢复需要 Node.js 24见上文版本要求小节。仅单进程快照以单个自包含进程运行mode: single与 bundle 一致不支持 cluster 模式。原生 addon 为 external必须在部署目标上存在。第三方依赖受限任何在模块求值阶段就打开活跃资源或捕获不可序列化状态的依赖打开的 socket、原生 HTTP/2 绑定、后台 timer、文件句柄都必须要么保持 external--force-external要么实现快照生命周期钩子在序列化前释放、在恢复后重建。并不是每个包都开箱即可被快照化。Web 全局对象必须在调用处引用undici 支撑的全局对象fetch/Headers/Request/Response/FormData/WebSocket/……会在构建期被替换为桩触碰它们会拉起 undici 不可序列化的原生绑定并在恢复时重新安装因此在调用处使用时可正常工作。但在模块求值期捕获的绑定——const f fetch或class X extends globalThis.Request——会把构建期的桩固化进 blob 且不会被升级。请在使用处引用 Web 全局对象不要在模块顶层捕获。支持范围仍在演进中完整的已知限制与设计取舍记录在项目的 V8 快照 RFC 中。故障排查如果快照无法构建序列化期间原生中止或无法恢复参见 快照故障排查。其中涵盖了构建期与恢复期的错误特征、如何定位捕获了不可序列化状态的模块以及可用的修复手段--force-external、egg.snapshot.lazyModules、生命周期钩子。下面摘要其核心方法论。唯一的规则堆里不能有「活的」资源快照在构建期冻结堆、在恢复期解冻。有三类值无法通过这个往返过程原生C 支撑的绑定——llhttpHTTPParser、nghttp2、TLSSecureContext、DNSChannelWrap、原生 addon 等。libuv 句柄——打开的 socket、监听中的 server、timer、文件句柄、watcher、以及 IPC channel。Node 的惰性 web 全局 getter——fetch、Headers、Request、Response、FormData、WebSocket、EventSource、MessageEvent、CloseEvent以及Blob/File都是访问器属性首次触碰时会初始化 Node 内建 undici→ 原生 http/http2。访问器本身不可序列化。当一个包在模块求值阶段、或在configWillLoad构建停止点之前创建了上述任意一种资源它就是快照不安全的。同一个包如果把这些工作推迟到只在请求期运行的函数里通常就没问题。构建期失败 vs 恢复期失败快照可能在两个不同的点失败错误特征会告诉你是哪一个构建期失败当 V8 序列化器遇到不可序列化的值时会以原生方式中止进程——没有可捕获的 JS 错误。典型特征如node ... --build-snapshot worker.js was killed by signal SIGSEGV、exited with code 1以及snapshot build finished but no blob was written at output/snapshot.blob。如果应用在加载元数据时抛出普通错误配置错误、文件缺失worker 会在退出前打印[egg-bundler] failed to build snapshot: message。恢复期失败egg-scripts start --snapshot-blob blob恢复堆时常见特征与含义对照如下现象含义反序列化中途原生 fatalCheck failed: current end_slot_index在Node.js 24上恢复。请始终在 Node.js 24 上恢复。[egg-bundler] V8 snapshot restore requires Node.js 24, but this process is vX快照自带的守卫触发了绕过了egg-scripts或它的版本探测 fail-open。Error: Cannot find module pkg某个external依赖缺失。external 在恢复时会被require()真实加载且解析根锚定在worker.js所在目录——请让worker.js与包含这些依赖的node_modules放在一起。Aop Advice(X) not found in loadUnits某个 tegg 装饰类在 bundle 里保留了错误的源码路径。某个「打包前还好好的」库内部深处抛TypeError该库错误地处理了构建期的成员代理桩。globalThis.fetch(...)静默无反应恢复后 web 全局对象仍是空操作桩。[egg-bundler] failed to restore snapshot: err在完成延后生命周期snapshotDidDeserialize→didReady→listen时抛出的任何其他错误。定位罪魁祸首模块先检查构建环境egg-bin snapshot build在 spawn 子进程前会剥离它自己注入的 TypeScript loader但会继承 shell 环境的其余部分。如果NODE_OPTIONS装入了自定义 loader 或 hook--require ts-node/register、--loader …、--import …它可能把不可序列化状态拉进堆。请先在干净环境里构建$ unset NODE_OPTIONS # 去掉任何继承来的 --require / --loader / --import $ egg-bin snapshot build打开调试日志bundler 和启动器的每个阶段都通过util.debuglog打日志# bundler 流水线manifest → entry → pack → prelude $ NODE_DEBUGegg/bundler/*,egg/bin/commands/snapshot egg-bin snapshot build # 启动器恢复守卫、spawn $ NODE_DEBUGegg/scripts/commands/start egg-scripts start --snapshot-blob ./dist-bundle/snapshot.blob常用命名空间egg/bundler/bundler打包开始、externals 解析结果、prelude/惰性 hook 注入计数、egg/bundler/entry-generator收集到的 bundle 入口、生成的 worker 入口路径、egg/bundler/manifest-loader发现并 externalize 了哪些内容、egg/bundler/snapshot-prelude哪些 external 的导出名无法读取、egg/bin/commands/snapshot实际 spawn 的node --build-snapshot …命令。直接读原生中止信息构建以继承 stdio 的方式 spawn 子进程因此 V8 序列化器的中止信息已打印到终端。可以从输出目录手动执行被包裹的命令以更快迭代$ cd ./dist-bundle $ EGG_BUNDLE_SNAPSHOTbuild \ node --snapshot-blob ./snapshot.blob --build-snapshot ./worker.js若只想打印这条命令而不运行用egg-bin snapshot build --dry-run。用--skip-bundle二分--skip-bundle只对已有的worker.js重跑快照步骤跳过缓慢的打包。由于 bundle 是单一自包含文件可以在worker.js里注释掉某个import/require几秒钟内重跑# 1. 打包一次 $ egg-bin snapshot build --output ./dist-bundle # 2. 编辑 ./dist-bundle/worker.js——注释掉某个嫌疑模块的求值 # 3. 只重跑快照构建 $ egg-bin snapshot build --output ./dist-bundle --skip-bundle如果去掉某个模块的求值后 blob 能构建成功那这个模块就是元凶。用--force-external确认把嫌疑包推出 bundle 既是诊断手段也是修复手段。external 永远不会在构建期被求值——它在恢复时被真实require()——所以如果--force-external pkg让构建成功了说明该包在 import 时捕获了不可序列化状态$ egg-bin snapshot build --force-external some-native-client修复手段按最轻量优先让包保持 external——最适合在 import 时打开连接、启动 timer 或加载原生 addon 的第三方包$ egg-bin snapshot build \ --force-external undici \ --force-external some-native-driver该包及其自身依赖必须安装在部署目标上因为它在运行期加载并没有被烤进 blob。反向标志--inline-external pkg会把解析器自动 externalize 的包强制塞回 bundle。把 builtin 加入egg.snapshot.lazyModules——对于在 import 时初始化原生状态、但不在默认惰性列表里的 builtin或类 builtin 的 id{ egg: { snapshot: { lazyModules: [node:zlib, node:perf_hooks] } } }内建默认值已经覆盖了http、https、http2、tls、dns和inspector含它们的node:形式——这些无需自行列出。实现快照生命周期钩子——当你自己的Boot 代码持有不可序列化的资源timer、socket、logger 流、连接池时在序列化前释放、在恢复后重建示例见上文「快照生命周期钩子」一节。把工作移出模块作用域——最干净的修复往往就在自己的代码里把资源创建从顶层模块体移到一个在请求期运行的函数中或放进didReady/snapshotDidDeserialize。一个在求值期只定义类和函数的模块永远是快照安全的而一个在求值期就建立连接或启动 timer的模块则不是// ✗ 在模块求值期运行 → 被写入快照 const client new SomeClient({ keepAlive: true }); // ✓ 首次使用时、在真实进程里创建 let client; function getClient() { return (client ?? new SomeClient({ keepAlive: true })); }避开 web 全局对象——globalThis.fetch和其他 undici 支撑的全局对象在恢复后仍是空操作桩。请改用按需懒加载的 HTTP 客户端——把urllib或undici保持 external在恢复时真实 require// ✗ 恢复后空操作 await fetch(url); // ✓ 真实客户端恢复时真实加载 const { request } require(urllib); await request(url);两个易踩的失败模式tegg 装饰器「Aop Advice not found」tegg 装饰器SingletonProto、HTTPController、Advice等会在模块求值时从调用栈用硬编码的栈深度捕获类的源码路径。在 bundle 里所有用户栈帧都坍缩到worker.js上因此一个读取比常规更深栈帧的装饰器典型是Advice会捕获到worker.js而非自己的文件。bundler 会自动纠正这一点在序列化前根据 manifest 里 tegg 的decoratedFiles为每个装饰导出重新打上filePath。如果你写了自定义装饰器并撞上此错误请确认该装饰文件属于某个 tegg module这样它才会出现在decoratedFiles里。请求期文件缺失运行期资源一个能干净恢复的快照仍可能在某个处理器读取文件时ENOENT。只有app/下的非源码文件加上强制拷贝目录app/public、app/assets、app/static会被拷贝到worker.js旁边。源码扩展名文件、app/之外的资源、以及符号链接资源都不会被拷贝。由于 bundle 把__dirname和import.meta.url重写成了输出目录执行fs.readFileSync(path.join(__dirname, tpl.html))的模块会相对 bundle 输出目录解析。在module.yml里声明这些额外资源即可bundle: runtimeAssets: roots: [app, resources] forceCopyDirs: [app/public, resources/templates]配置参考机制位置用途--force-external pkgegg-bin snapshot build标志可重复把包留在 bundle 外恢复时真实加载。--inline-external pkgegg-bin snapshot build标志可重复把被自动 externalize 的包强制塞回 bundle。egg.snapshot.lazyModules应用package.json向惰性 external 集合添加 builtin/类 builtin 的 id合并到默认值之上。snapshotWillSerialize()/snapshotDidDeserialize()app.js/agent.jsBoot 类释放并重建你自己代码持有的资源。--pack-alias spectargetegg-bin snapshot build标志可重复打包期重定向某个模块说明符。--skip-bundleegg-bin snapshot build标志只对已有worker.js重跑快照步骤。--dry-runegg-bin snapshot build标志打印node --build-snapshot命令但不 spawn。bundle.runtimeAssets.roots/forceCopyDirs应用module.yml把额外的非源码文件拷进 bundle使其在请求期存在。--no-sourcemapegg-scripts start标志当自动注入的--import source-map-support/register干扰恢复启动时TypeScript 应用将其去掉。NODE_OPTIONS环境变量构建前必须不含自定义--loader/--require/--import它们会随快照子进程一起进入。NODE_DEBUGegg/bundler/*环境变量追踪 bundler/启动器流水线。相关源码路径想深入理解实现细节的读者可以在仓库中继续研读以下文件快照构建与恢复 API 实现packages/egg/src/lib/snapshot.tsegg-bin snapshot buildCLI 命令tools/egg-bin/src/commands/snapshot.tsegg-scripts start --snapshot-blob恢复启动tools/scripts/src/commands/start.ts打包期惰性 external 与 prelude 注入tools/egg-bundler/src/lib/Bundler.tsBundle 部署快照的基础site/docs/zh-CN/core/bundle.md快照故障排查完整指南site/docs/zh-CN/advanced/snapshot-troubleshooting.md赞分享后端Web框架【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa. https://307.run/eggcode项目地址https://gitcode.com/gh_mirrors/eg/egg点击查看免费下载相关推荐深入解析Agents-A1-8bit架构MoE专家混合模型的技术奥秘深入解析Agents A1 8bit架构MoE专家混合模型的技术奥秘 Agents A1 8bit是基于MLX框架的8位量化视觉语言模型采用创新的MoE专后端Web框架CubeSandbox 核心操作性能基准测试实战报告从冷启动到快照的全链路压测指南CubeSandbox 核心操作性能基准测试实战报告从冷启动到快照的全链路压测指南 本报告基于 CubeSandbox 官方基准测试文档 docs/blog/Agent 沙箱虚拟化云原生人工智能后端容器运行时FlutterUnit启动优化实战从3秒到0.5秒的冷启动加速指南FlutterUnit启动优化实战从3秒到0.5秒的冷启动加速指南 你是否遇到过这样的情况用户点击FlutterUnit图标后屏幕长时间停留在启动界面甚前端移动开发桌面应用教育上一篇React SSR Setup代码分割React.lazy与动态导入的SSR实现指南 下一篇Rabet高级操作模式解析Signer、Threshold与SetOptions实战创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
