docs/workspace-trpc/experience.md
记录开发过程中的重要经验和最佳实践。
经验: 在 pnpm workspace (monorepo) 中,当一个包(如@core)需要同时服务于前端(Vite构建的@ui包)和后端(Node.js/Electron)时,处理导出和依赖关系需要特别小心,以避免构建冲突。
场景:
@core包,其中一部分代码(如tRPC路由)仅用于后端,另一部分是通用代码。@ui包(使用Vite构建),依赖@core包。@desktop包(使用Electron),也依赖@core包,并需要使用其仅后端的代码。问题:
@core包在其主入口 (index.ts) 导出了仅后端的代码,会导致前端应用打包进不必要的服务器依赖(如@trpc/server)。package.json的exports映射为后端代码创建单独入口,可能会破坏Vite的依赖解析机制,导致@ui包构建失败。@ui的Vite配置中将@core包设为external,会增加最终应用的配置复杂性,使其无法"开箱即用"。最佳实践 / 解决方案:
@ui 包的 vite.config.ts 中,移除内部依赖(如 @core)的 external 配置。让UI库成为一个完整的、内置所有必要依赖的自包含产品。@core 包中,使用 tsup 等工具配置多入口点构建。一个入口是提供给前端和大部分后端的公共API (index.ts),另一个是仅用于特定后端的专门文件(如 router.ts)。package.json 中为仅后端的代码创建复杂的 exports 映射。保持主 exports 干净、简单,只指向公共API。desktop/main.js),直接通过相对文件路径从 dist 目录中 require 编译后的文件。代码示例:
packages/core/package.json (scripts):
"build": "tsup src/index.ts src/services/trpc/router.ts --format cjs,esm --dts"packages/desktop/main.js (import):
const { createAppRouter } = require('@prompt-optimizer/core/dist/services/trpc/router.cjs');结论: 这种"公共API + 内部路径"的策略,优雅地解决了前后端对同一个包的不同需求,保证了Vite构建的顺利进行,也维持了后端功能的可用性。
核心原则: 必须同时满足现代前端构建工具(如Vite)和后端环境(Node.js)的模块解析规则。核心是遵守Node.js的exports封装性,并以此为基础解决Vite的兼容问题。
遇到的问题演进:
@core包的index.ts导出了仅服务器端的代码,导致浏览器报错。exports为后端代码创建单独入口,但这种多入口配置导致Vite无法解析依赖。exports字段存在时,所有访问必须经过它的允许。最终的最佳实践 (The Standard Way):
@ui 包的 vite.config.ts 中,移除内部依赖(如 @core)的 external 配置。让UI库成为一个完整的、内置所有必要依赖的自包含产品。这是解决问题的起点。@core 包的 package.json 中,使用 exports 字段明确声明所有需要被外部访问的路径,无论是给前端还是后端使用。tsup 等工具进行多入口点构建,确保 exports 中声明的每个路径都有对应的编译产物。exports 中声明的标准路径来导入模块 (e.g., '@prompt-optimizer/core' 或 '@prompt-optimizer/core/trpc-router')。dist/...)进行导入。代码示例 (最终正确配置):
packages/core/package.json:
"exports": {
".": { "import": "./dist/index.js", "require": "./dist/index.cjs" },
"./trpc-router": { "import": "./dist/services/trpc/router.js", "require": "./dist/services/trpc/router.cjs" }
},
"scripts": {
"build": "tsup src/index.ts src/services/trpc/router.ts --format cjs,esm --dts"
}
packages/desktop/main.js:
const { createAppRouter } = require('@prompt-optimizer/core/trpc-router');结论: 这个标准化的解决方案保证了 @core 包的强封装性,同时为不同环境的消费者提供了清晰、稳定、唯一的访问接口。如果在此基础上Vite仍然构建失败,那么下一步应该去调整Vite自身的配置(如 resolve.alias 或 optimizeDeps.exclude),而不是破坏包的封装规则。
concurrently并行执行多个任务,其中一个任务是Vite开发服务器(vite dev),另一个是其依赖库的监视构建任务(vite build --watch)。vite dev在启动时需要读取依赖库(如@ui)的构建产物(如dist/style.css),而vite build --watch在同一时间可能正在清理或重写该dist目录,导致文件读取失败,引发样式丢失等问题。src)进行实时编译和热更新。build --watch任务。vite dev)和其他必要的后端服务(如electron .)。让单个Vite实例全权负责所有前端代码的编译。concurrently "pnpm -F @ui watch" "pnpm -F @web dev"concurrently "pnpm -F @web dev" "pnpm -F @desktop dev" (假设web负责所有UI,desktop是后端)