后端微服务云原生【免费下载链接】midway A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 项目地址https://gitcode.com/gh_mirrors/mi/midway点击查看免费下载本文基于仓库 openspec/changes/refactor-functional-dev-source-loader-boundary 变更提案及配套 设计文档、任务清单 与 需求规格 编写。它解答 Midway 前端一体化functional开发模式下为什么要把开发期源码直跑兼容逻辑从midwayjs/core迁移到midwayjs/mock、新引入的 dev source loader 如何工作、Vite/Rspack 如何共享同一套加载器以及 Node 20 ESM TypeScript decorator 场景下的回归保障。背景为什么源码加载逻辑出现在 core 里是一种“越界”Midway 的 functional 一体化开发模式允许开发者在项目根目录的src/server下直接用 TypeScript 编写 Controller、Service 与configuration.ts由前端构建链路的开发插件在启动时直接加载源码源码直跑无需先编译到dist。这条链路的完整调用链是Vite/Rspack dev plugin - midwayjs/mock.createApp() - initializeGlobalApplicationContext() - findProjectEntryFile() - loadModule() 直接加载 src/server 源码在 Node 20 ESM TypeScript decorator 场景下源码直跑并不简单需要额外处理五类问题见 设计文档.jsspecifier 要能对应到.ts源文件ESM 严格后缀解析与 TS 源文件不一致TypeScript ESM 转译decorator 与emitDecoratorMetadataMidway 依赖装饰器元数据做依赖注入bare package import 解析dev watch 对临时文件的排除避免自己生成的临时产物触发重复 reload。此前为兼容 Node 20 下的这套场景源码 fallback/转译逻辑被临时塞进了midwayjs/core的loadModule()。这带来明显的架构问题这些逻辑只属于开发期且只在 mock 源码直跑链路中需要却被沉入 core 这一基础加载层core 承担了不属于通用运行时职责的开发期细节职责边界模糊增加了 core 的维护负担与回归风险——基础加载器一旦被改动影响面是全部运行时。提案 proposal.md 明确指出需要把逻辑从 core 迁回midwayjs/mock让loadModule()恢复标准模块加载能力。重构目标与边界Goals / Non-Goals设计文档 明确了本次重构的目标与不做的事Goals将开发期源码加载兼容逻辑归位到midwayjs/mock保持 Vite 与 Rspack dev 插件共享同一套 source loader使midwayjs/core.loadModule()回到标准加载器职责保持 functional 一体化开发链路在 Node 20 下可用。Non-Goals明确不在本次范围内不改变 functional API 用户侧 DSL不切换到新的默认 dev runtime如tsx不把传统dist驱动开发链路改为源码直跑。设计文档特别解释了一条决策本次先保留当前兼容策略思路不立即切换到tsx/ts-node作为默认运行时。理由是历史上完整 TS runtime 在 Midway/Koa 开发链路里存在启动/重建性能成本本次优先修正边界而非切换 runtime 路线design.md。这是一条重要的克制决策重构的是职责归属而不是运行策略本身。核心决策dev source loader 归位 mockVite/Rspack 共用一套需求规格 spec.md 将三条核心决策固化为系统需求1. 开发期源码加载逻辑必须限定在 mock 内系统 SHALL 将 functional 一体化开发期的源码加载兼容逻辑限定在midwayjs/mock的开发链路内而不是放入midwayjs/core的通用模块加载器。对应场景当用户通过 Vite 或 Rspack 开发插件启动 functional 一体化项目时针对 TypeScript/ESM/decorator 的源码加载兼容逻辑由midwayjs/mock提供而midwayjs/core不需要感知 dev-only 的源码 fallback 或临时文件机制。2. core.loadModule() 保持标准加载职责其职责限定为标准require/import与safeLoad语义不承担 functional 一体化开发期的源码转译或 specifier fallback 逻辑。3. Vite 与 Rspack 复用同一套 mock source loader两条链路都通过midwayjs/mock的统一 source loader 加载src/server源码不为不同 bundler 维护两套独立的 TS/ESM 兼容实现。规格还补充了一条容易被忽略的硬性要求临时产物不触发重复 reload——当 mock source loader 在开发期生成内部临时产物或缓存文件时Vite/Rspack watcher 不得把这些内部文件视为业务源码变更应用不能因内部临时文件产生重复 reload。落地实现mock 内部的统一 dev source loader重构的落地成果集中在新文件 packages/mock/src/sourceLoader.ts它把此前散落/下沉在 core 里的兼容逻辑收敛为createSourceModuleLoader()一个出口import { loadModule, ModuleLoader, ModuleLoadOptions } from midwayjs/core; // ... export function createSourceModuleLoader( baseLoader: ModuleLoader loadModule ): ModuleLoader { return async (p: string, options: ModuleLoadOptions {}) { // 默认值enableCache true, safeLoad false, loadMode commonjs // 若为 esm 模式且开启缓存且非 .json 文件先尝试标准 baseLoader // 失败后进入 importWithSpecifierFallback()见下 // 最终兜底 safeLoad 语义加载失败时按需 warn 并返回 undefined }; }createSourceModuleLoader()是一个标准的ModuleLoader工厂签名与 core 的loadModule对齐(p, options) Promiseany因此它既能作为moduleLoader选项注入initializeGlobalApplicationContext()也能在失败时优雅降级到safeLoad语义warnOnLoadError且非MODULE_NOT_FOUND/ERR_MODULE_NOT_FOUND/ENOENT时才console.warn最终返回undefined见 sourceLoader.ts。内部机制一相对 specifier 的扩展名解析resolveRelativeEsmSpecifierPathESM 的模块解析要求相对导入必须带完整后缀而 TS 源码里通常写import { UserService } from ../service/user.service.js去指向user.service.ts或者干脆省略后缀。resolveRelativeEsmSpecifierPath()负责把./、../开头的 specifier 解析为真实存在的文件若 specifier 以.mjs/.cjs/.js结尾依次尝试替换为.mts/.cts/.ts/.tsx若 specifier 无扩展名则按顺序尝试.mts/.cts/.ts/.tsx/.mjs/.cjs/.js/.json以及index.*系列。见 sourceLoader.ts。这解决了.jsspecifier 对应.ts源文件的问题。内部机制二ESM 源码改写rewriteRelativeEsmSource对于from ...和import(...)两处相对导入将 specifier 改写为解析后的真实相对路径且仅当from xxx.js实际指向xxx.ts这类表面相同、实际不同时才改动改写结果与原文一致则不动见 sourceLoader.ts。改写后若源码内容发生变化即判定需要走 fallback。内部机制三TypeScript ESM 转译 decorator metadatacreateCompiledEsmFallbackGraph当标准import()直接失败典型错误码是ERR_UNKNOWN_FILE_EXTENSION即 Node 不认识.ts扩展名或抛SyntaxError即装饰器等 TS 语法无法被原生解析时进入 fallback 图编译流程sourceLoader.ts在入口文件同目录下创建临时目录.midway-esm-fallback-*递归编译整个依赖图每个源文件以 sha1 哈希命名生成.mjs临时产物.json直接转成export default {...}所有相对导入在编译产物之间重写为相对 specifierTypeScript 文件调用ts.transpileModule()转译compilerOptions固定为{ module: tsCompiler.ModuleKind.ESNext, target: tsCompiler.ScriptTarget.ES2020, moduleResolution: tsCompiler.ModuleResolutionKind.NodeNext, esModuleInterop: true, allowSyntheticDefaultImports: true, resolveJsonModule: true, experimentalDecorators: true, emitDecoratorMetadata: true, // 关键保留装饰器元数据Midway 注入依赖它 useDefineForClassFields: false, jsx: tsCompiler.JsxEmit.ReactJSX, }注意emitDecoratorMetadata: true与useDefineForClassFields: false的组合正是为了确保Inject/Provide等 Midway 装饰器在开发期直跑时属性注入与 reflect metadata 行为与标准tsc编译结果保持一致对应规格中的 Node 20 下保留 decorator metadata 行为 场景。加载编译后的 entry.mjs并在finally中递归清理临时目录rmSync(tempDir, { recursive: true, force: true })。TypeScript 编译器通过require.resolve(typescript, { paths: [dirname(sourceFile), process.cwd(), __dirname] })按源码目录 → 项目根 → mock 自身逐级寻找并缓存避免依赖全局安装若最终找不到编译器会抛出明确错误[mock]: can not transpile esm typescript file ..., please install typescript in current project。内部机制四bare package import 与 importQueryfallback 图只重写相对导入bare package import如import { Inject } from midwayjs/core在转译后的.mjs产物中由 Node 原生 ESM 解析器按node_modules查找天然保持可解析。此外importWithSpecifierFallback()支持mwImportQuery查询参数透传Vite HMR 场景通过环境变量MIDWAY_HMR_IMPORT_QUERY注入见 vite.ts保证热更新场景下模块可被正确失效与重建。createApp 如何接入 source loaderpackages/mock/src/creator.ts 是midwayjs/mock的入口中枢create/createApp/createLightApp/createFunctionApp/createBootstrap。接入点有两处关键逻辑探测模块类型if (!options.moduleLoadType) { const pkgJSON await loadModule(join(appDir, package.json), { safeLoad: true, enableCache: false, }); options.moduleLoadType pkgJSON?.type module ? esm : commonjs; }即依据package.json的type: module决定走 ESM 还是 CommonJS 加载。按条件注入 dev source loaderif ( !options.moduleLoader options.moduleLoadType esm isTypeScriptEnvironment() ) { options.moduleLoader createSourceModuleLoader(); }只有同时满足ESM 项目 TypeScript 环境MIDWAY_TS_MODE/ts-node等标识时才启用 dev source loader其余场景CommonJS、纯 JS、或用户显式传入moduleLoader仍走标准loadModule。这正是开发期源码加载逻辑只在 mock 的开发链路内生效的体现——options.moduleLoader随后会被透传给initializeGlobalApplicationContext()creator.ts从而影响整个应用上下文对src/server源码的加载。createFunctionApp中也有完全相同的注入逻辑creator.ts说明 serverless 函数链路同样受益。Vite 与 Rspack dev 插件只做桥接与 reload不承载源码兼容重构前 Vite/Rspack 插件承担或间接依赖了源码兼容细节重构后两个插件被收敛为纯粹的请求桥接 watch reload职责源码加载统一经由mock.createApp()内部的 source loader。Vite 侧packages/mock/src/vite.ts核心是devPlugin(options)options必须提供appDir否则抛[midway:mock] devPlugin requires appDirbaseDir默认srcbasePath默认/api通过 Vite middleware 拦截匹配basePath的请求交给 Midway app 的 request handler通过server.watcher.on(all, ...)监听文件变化命中watchInclude且未被watchExclude排除时才触发reloadApp()关闭旧 app、使 route manifest 模块失效并推送 full-reload通过 Vite 虚拟模块virtual:midway-route-manifest暴露MidwayWebRouterService.getRouteManifest()的路由清单供前端侧生成路由/请求层代码。关键的 watcher 排除规则Vite 与 Rspack 完全一致见 vite.ts 与 rspack.tsconst watchExclude options.watch?.exclude || [ /\.d\.ts$/, /\/\.midway-esm-fallback-[^/]\/.$/, ];第二条正则正是针对 source loader 生成的临时目录.midway-esm-fallback-*而设直接落实了规格中临时产物不触发重复 reload的需求。Rspack 侧packages/mock/src/rspack.ts同样暴露devPlugin(options)插件名midway-dev-runtime-rspack通过compiler.hooks.invalid监听文件失效事件命中排除规则的不触发 reload通过compiler.options.devServer.setupMiddlewares注入请求中间件并保留用户已有的setupMiddlewares链compiler.hooks.shutdown时统一执行reloadApp()清理。两插件通过ensureApp()惰性创建并复用同一个createApp()promise且在创建期间设置MIDWAY_HMR_IMPORT_QUERY1环境变量保证 HMR 语义一致。回归保障Node 20 ESM TS decorator fixture 与 watcher 测试重构任务清单 tasks.md 明确要求为 mock 新增回归测试并完成pnpm -C packages/core build、pnpm -C packages/mock build与 openspec 校验。仓库中的测试证据如下ESM 源码直跑集成测试packages/mock/test/new.test.ts测试通过fork子进程以ts-node/register运行 fixturecheck.cjs验证在createLightApp(__dirname, { moduleLoadType: esm })下configuration.ts被成功加载fixtureName对象被注册为esm-functional带装饰器的 Controller 注入 Service 正常userController.show() hello world完成后process.send(ready)通知父进程。对应 fixturepackages/mock/test/fixtures/base-app-light-esm-functional本身就是 Node 20 场景的缩影configuration.tsdefineConfigurationESModuleFileDetectorignore: [configuration.ts, index.ts, support.ts]并使用.js后缀相对导入./support.jsindex.tsexport { default } from ./configuration.jsapi/user.controller.tsProvideInject装饰器依赖注入service/user.service.tsProvide服务实现。这一组 fixture 恰好覆盖了.jsspecifier 指.ts源文件、ESM 转译、decorator metadata、相对依赖解析四类兼容问题并验证了 bare package importmidwayjs/core保持可解析。watcher 排除回归测试packages/mock/test/vite.test.ts 与 packages/mock/test/rspack.test.ts两个测试用 mock 的 server/compiler 对象断言同一个行为向 watcher 推送.midway-esm-fallback-abc/hash.mjs这类临时产物变更时logger.info不会被调用不触发 reload而推送真实业务文件src/server/user.ts变更时日志包含reload midway app触发 reload。这直接验证了临时产物不触发重复 reload的规格场景。迁移路径与遗留问题设计文档 给出的迁移计划为在packages/mock引入统一 dev source loader让mock.createApp()在源码入口探测与初始化阶段走 source loader让 Vite/Rspack 插件只负责请求桥接与 reload将core.loadModule()回退为纯加载器用 Node 20 functional fixture 验证 React/Vue 一体化开发链路。从任务清单看14 与验证步骤均已标记完成tasks.md 中 2.12.4、3.13.6 全部[x]且openspec validate ... --strict --no-interactive已通过。回退后的midwayjs/core.loadModule()目前实现为标准语义packages/core/src/util/index.tsenableCache下 CommonJS 走require含extraModuleRoot逐级require.resolveESM 走pathToFileURLimport.json使用with: { type: json }safeLoad失败时按warnOnLoadError决定是否告警并返回undefined禁用缓存时退化为JSON.parse(readFileSync(...))。它不再包含开发期源码 fallback 与临时文件逻辑恢复了通用加载器边界。设计文档同时记录了两个 Open Questionsdesign.mdmock source loader 是保持显式新函数createSourceModuleLoader方式接入还是通过initializeGlobalApplicationContext()的可配置 loader hook 接入更合适decorator-heavy 业务在 mock source loader 中是否继续保留局部 transpile 策略还是后续单独评估运行时替换方案。从当前实现看选择了显式工厂函数 moduleLoader选项注入的方案且MockBootstrapOptions.moduleLoader类型被显式保留packages/mock/src/interface.ts说明该注入点是面向用户与内部共享的稳定接口。小结边界即架构这次重构的核心价值在于明确开发期与运行时的职责边界midwayjs/core保持标准模块加载语义任何开发期兼容细节都不再污染基础层降低回归风险midwayjs/mock成为 functional 一体化开发期源码加载的唯一归属方Vite 与 Rspack 通过createApp()共享同一套createSourceModuleLoader()避免双实现漂移watcher 排除、HMR importQuery、临时产物清理等开发体验细节在同一层闭环Node 20 ESM TypeScript decorator 的兼容能力由专门的 fixture 与 watcher 测试锁定可长期回归。对于正在维护 functional 一体化项目的开发者而言理解这条边界意味着当遇到src/server源码加载类问题TS 转译、.js→.tsspecifier、装饰器元数据丢失、开发期重复 reload排查与修复都应聚焦在midwayjs/mock的 sourceLoader.ts、creator.ts、vite.ts、rspack.ts 这条开发链路上而不是去动 core 的通用加载器——这既是本次提案的结论也是 Midway 该功能可持续演进的基础。赞分享后端微服务云原生【免费下载链接】midway A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 项目地址https://gitcode.com/gh_mirrors/mi/midway点击查看免费下载相关推荐NemoClaw src/lib 分层架构地图从 commands 到 core 的职责边界与迁移指南NemoClaw src/lib 分层架构地图从 commands 到 core 的职责边界与迁移指南 src/lib/README.md 是 NemoCla终极终端聊天工具imsg告别手机高效沟通新方式终极终端聊天工具imsg告别手机高效沟通新方式 还在为回复消息频繁切换终端和手机而烦恼吗imsg让你无需离开终端界面直接在命令行中通过iMessage与即时通讯Three20代码重构实例从臃肿类到单一职责原则Three20代码重构实例从臃肿类到单一职责原则 你是否曾面对过一个超过2000行的巨型类文件其中混杂着网络请求、UI渲染和数据解析逻辑Three20作为上一篇一个创新的在线画板工具LHRUN/Paint-Board下一篇VRoom! RISC-V CPU云服务器级的高端RISC-V实现创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考