1. 为什么一个TS库的打包值得单独拎出来说如果你写过几个工具函数或者维护过一个内部组件库大概率经历过这样的阶段代码在项目里跑得好好的一旦要发布成独立的包给别人用问题就全冒出来了。要么是产出的js文件在别人的构建流程里报错要么是类型声明文件缺失导致使用方满屏飘红要么是package.json里的入口字段配得乱七八糟import和require两种引入方式各挂各的。这个项目的核心就是解决这件事用rollup把一个TypeScript库打包成可直接消费的js文件同时产出配套的类型声明文件。听起来只有一句话但里面涉及的配置取舍、产物结构设计、类型声明生成方式、模块格式兼容每一项都有坑。适合的读者是那些已经能写TS、但还没完整走过库发布流程的开发者也适合正在维护内部组件库、想把手上的打包配置理顺的人。我先说清楚这个标题背后的真实需求。一个TS库的交付物本质上不是能跑的代码而是别人能无痛接入的代码。使用方可能用webpack可能用vite可能在Node环境里直接require也可能在ESM项目里import。你的产物要同时覆盖这些场景还要保证类型提示完整。rollup在这件事上的定位很明确它擅长把ESM源码打成干净的库产物tree-shaking做得好产物结构可控配合插件体系能顺手把类型声明一起处理掉。相比直接用tsc编译rollup能输出多种模块格式并做依赖外置相比用打包应用的方式去打库rollup的产物更素不会塞进一堆运行时垫片。所以这个项目的价值不在于学会一个命令而在于建立一套库产物的交付标准入口怎么设计、格式怎么选、类型声明放哪、外部依赖怎么处理、发布前怎么验证。下面我按实际操作的顺序把每个环节的考量和细节拆开讲。2. 整体方案设计与关键取舍2.1 产物形态先定配置才有方向动手写配置之前必须先想清楚最终要交付什么。一个规范的TS库产物通常包含三部分编译后的js文件、类型声明文件.d.ts、以及描述入口和依赖关系的package.json。这三者是一个整体缺一个都会让使用方难受。js文件的模块格式是第一个要拍板的事。常见的选择有三种只出ESM、只出CommonJS、或者两者都出。我的建议是双格式输出也就是同时产出ESM和CJS。原因很实际现在新项目基本都用ESM但大量存量项目和Node脚本仍然走CJS你只出一种格式就等于主动砍掉一半使用场景。rollup的output配置支持数组一次打包输出多份成本很低。类型声明文件则是第二个关键点。TS库如果没有.d.ts使用方在编辑器里就得不到任何提示等于把一个TS库降级成了JS库这是不可接受的。类型声明的生成有两条主流路线一是用tsc单独跑一遍只出声明二是用rollup插件在打包过程中顺带生成。两条路线各有适用场景后面会详细对比。还有一个容易被忽略的决策依赖要不要打进产物。库和应用不一样库的第三方依赖通常应该外置external交给使用方的项目去解析否则同一个依赖在产物和使用方项目里各存一份体积翻倍还可能引发实例不一致的问题。这个决策直接决定了external字段和peerDependencies的写法。2.2 为什么是rollup而不是别的工具有人会问tsc本身就能编译TS为什么还要套一层rollup。答案在于产物的干净程度和格式灵活性。tsc的输出是逐文件对应的一个源文件出一个js模块格式由module配置统一决定想同时出ESM和CJS就得跑两次并改目录管理起来很别扭。而且tsc不做打包产物是一堆散文件入口的聚合需要自己写。rollup的强项正好补上这块它从单一入口出发把内部模块合并成少量产物文件tree-shaking能剔除未使用的导出output.format可以一次配多种格式。对于库这种入口明确、产物要精简的场景rollup的模型非常贴合。至于打包应用常用的那些工具它们更偏向处理样式、静态资源、代码分割等应用级需求用来打库反而显得重产物里容易混入运行时辅助代码。这里要强调一个取舍rollup负责打包和格式转换类型声明交给专门的工具处理两者分工明确。不要指望rollup本身理解TS类型它只是通过插件把TS转成js类型信息的提取是另一条链路。2.3 目录结构与入口设计在写配置前先把目录约定好后面所有路径都围绕它展开。我习惯的结构是这样的源码放src入口文件是src/index.ts它负责把各个子模块的导出聚合起来。构建产物统一输出到dist并在dist下按格式分子目录比如dist/esm和dist/cjs类型声明单独放dist/types。这种分目录的做法有个直接好处package.json里的入口字段可以精确指向对应文件使用方无论走哪种模块解析都能命中正确的产物。如果所有格式混在一个目录里文件名容易冲突入口字段也会写得很绕。入口文件的设计也有讲究。src/index.ts应该只做导出聚合不写具体逻辑这样rollup以它为入口时能清晰地看到整个库的导出面tree-shaking的边界也明确。如果库有多个子入口比如按功能拆分的core、utils可以用多个input配置但初期建议先保持单入口降低复杂度。3. 核心配置拆解与实操要点3.1 依赖安装与版本选择先把工具链装齐。核心依赖是rollup本身、TypeScript、以及把两者接起来的插件。我用的组合是rollup、typescript、rollup/plugin-typescript负责TS转js、rollup-plugin-dts负责把声明文件合并成单文件、rollup/plugin-node-resolve解析node_modules里的模块、rollup/plugin-commonjs把CJS依赖转成ESM以便rollup处理。npm install -D rollup typescript rollup/plugin-typescript rollup-plugin-dts rollup/plugin-node-resolve rollup/plugin-commonjs版本上有个实操心得rollup/plugin-typescript对TypeScript版本有一定敏感性装的时候尽量让两者版本接近发布周期避免插件内部调用的TS API对不上。我踩过一次坑TS升到较新的大版本后插件报内部错误回退TS小版本就好了。所以装完先跑一次空构建验证链路通不通别等业务代码写完才发现工具链有问题。rollup-plugin-dts的作用值得单独说。它读取tsc产出的声明文件把多个.d.ts合并成一个入口声明文件。为什么需要合并因为如果声明文件是散落的多个使用方在package.json里指向入口声明时rollup或打包工具需要顺着引用关系去解析某些配置下会解析失败。合并成单文件后入口声明自包含兼容性最好。3.2 tsconfig的针对性配置库的tsconfig和应用的不一样重点在几个字段。declaration必须开这是产出类型声明的前提declarationDir指定声明输出目录outDir指定js输出目录module设成ESNext让rollup接管后续转换target按需要设库一般设ES2018或更高太老的target会引入不必要的降级代码。{ compilerOptions: { target: ES2018, module: ESNext, moduleResolution: node, declaration: true, declarationDir: dist/types, outDir: dist/tmp, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true }, include: [src] }这里有个细节outDir我设成了dist/tmp这是个临时目录只用于tsc单独生成声明文件时存放中间产物最终会被清理。为什么不直接让rollup插件输出到最终目录因为声明文件的生成和js打包是两条链路分开跑更可控也方便排查问题。skipLibCheck建议打开它能跳过对node_modules里声明文件的类型检查构建速度会明显提升而且能规避一些第三方声明文件自身的类型错误干扰。strict则建议保持开启库的类型质量直接影响使用方体验宽松配置下漏掉的类型问题最终会转嫁给使用者。3.3 rollup配置的逐段解析rollup配置文件我习惯写成rollup.config.mjs用ESM语法避免CJS和ESM混用带来的解析歧义。配置导出的是一个数组因为要跑两趟一趟打js一趟合并声明。第一趟打js的配置核心是input、plugins、external、output四块。input指向src/index.tsplugins里挂上node-resolve、commonjs、typescriptexternal把第三方依赖排除掉output用数组输出ESM和CJS两份。import typescript from rollup/plugin-typescript; import resolve from rollup/plugin-node-resolve; import commonjs from rollup/plugin-commonjs; export default [ { input: src/index.ts, external: [lodash-es], plugins: [ resolve(), commonjs(), typescript({ tsconfig: ./tsconfig.json, declaration: false, outDir: undefined }) ], output: [ { file: dist/esm/index.js, format: esm, sourcemap: true }, { file: dist/cjs/index.cjs, format: cjs, exports: named, sourcemap: true } ] } ];几个参数的选择理由要说清楚。external里列出的依赖不会被打进产物使用方项目需要自己安装这对应package.json里的peerDependencies。exports: named是CJS输出的关键它让CJS产物以命名导出的形式暴露避免使用方require后拿到一个带default包裹的对象减少互操作摩擦。sourcemap建议开库出问题时使用方能定位到源码排查成本低很多。CJS产物的文件扩展名我用.cjs而不是.js这是为了在package.json的type字段设为module时明确告诉Node这个文件是CJS。如果库的package.json没设type用.js也行但显式用.cjs更稳妥不依赖外部配置。第二趟合并声明的配置更简单input直接指向tsc产出的入口声明文件插件只用dts输出单个.d.ts。import { dts } from rollup-plugin-dts; export default [ { input: dist/types/index.d.ts, output: { file: dist/types/index.d.ts, format: esm }, plugins: [dts()] } ];注意这里的输入输出是同一个路径dts插件会读取散落的声明并合并覆盖。这种写法依赖tsc先跑完生成dist/types下的声明文件所以构建脚本的顺序必须是先tsc再rollup。3.4 package.json的入口字段设计产物打出来后package.json的字段配置决定了使用方能不能正确解析到它们。核心字段是main、module、types以及更现代的exports。{ name: your-lib-name, version: 1.0.0, type: module, main: ./dist/cjs/index.cjs, module: ./dist/esm/index.js, types: ./dist/types/index.d.ts, exports: { .: { types: ./dist/types/index.d.ts, import: ./dist/esm/index.js, require: ./dist/cjs/index.cjs } }, files: [dist], sideEffects: false, peerDependencies: { lodash-es: ^4.17.21 } }main指向CJS产物兼容老工具链module指向ESM产物让支持ESM的打包工具优先用它types指向声明入口。exports字段是更精细的解析规则types条件要放在最前面import和require分别对应两种引入方式。files限定发布内容只包含dist避免把源码和配置一起发出去。sideEffects: false告诉打包工具这个库没有副作用tree-shaking能更激进地剔除未使用代码。这里有个高频坑exports字段一旦存在就会覆盖main和module的解析行为某些老版本工具不认exports会回退到main。所以两套字段都要写对不能只配一边。另外types条件在exports里的位置很关键如果放在import后面某些解析器会先命中import而忽略类型导致类型提示丢失。4. 完整构建流程与现场记录4.1 构建脚本的编排整个构建分三步清理旧产物、tsc生成声明、rollup打包js并合并声明。写成npm scripts顺序执行。{ scripts: { clean: rimraf dist, build:types: tsc -p tsconfig.json --emitDeclarationOnly, build:js: rollup -c rollup.config.mjs, build: npm run clean npm run build:types npm run build:js } }--emitDeclarationOnly让tsc只出声明不出js避免和rollup的js输出重复。clean用rimraf保证跨平台删除目录别用rm -rfWindows下会挂。实际跑一遍build:types会在dist/types下生成每个源文件对应的.d.tsbuild:js先打js到dist/esm和dist/cjs然后第二趟配置读取dist/types/index.d.ts并合并覆盖。最终dist结构是三个子目录各司其职。4.2 产物验证的实操方法打完包不能只看没报错就完事要实际验证产物可用。我通常做三件事。第一在dist目录下用Node直接requireCJS产物跑一个简单调用确认运行时没问题。第二新建一个临时TS项目通过file:协议把库装进去写一段import代码看编辑器有没有类型提示、编译有没有报错。第三用打包工具比如vite建个最小项目引入这个库确认tree-shaking和模块解析正常。node -e const lib require(./dist/cjs/index.cjs); console.log(lib)这一步能快速暴露CJS产物的导出形态问题。如果打印出来是个带default的对象而不是预期的命名导出说明exports: named没生效或者配置位置不对。类型验证那步尤其重要。很多人打完包自己项目里用着没问题是因为自己项目里源码还在TS直接读源码类型。真正发布后使用方只拿到dist类型链路断了才发现。所以一定要在只有产物的环境里验证。4.3 参数与体积的实测记录拿一个中等规模的工具库实测源码约30个文件、导出约80个函数。构建耗时上tsc出声明约4秒rollup打双格式约6秒合并声明约2秒整体在12秒左右日常开发完全可接受。产物体积上ESM产物约48KB未压缩CJS约52KB声明合并后单文件约36KB。开启sourcemap后各多出约一倍体积但发布时通常不带map或者单独提供不影响主产物。这里有个观察CJS产物比ESM略大是因为CJS的模块包装和exports赋值有额外开销属于正常现象。如果体积敏感可以进一步检查是否有大依赖被误打进产物用external排除掉。5. 常见问题与排查速查5.1 类型声明相关的典型故障最常见的问题是使用方装了库但编辑器没提示。排查顺序是先看package.json的types字段路径对不对再看exports里的types条件是否在正确位置最后看合并后的.d.ts是否真的包含了所有导出。有时候dts插件合并时漏掉了某些通过export *转出的类型需要检查源文件的导出写法是否规范。另一个问题是声明文件和js对不上比如js里有的导出声明里没有。这通常是tsc和rollup跑的顺序或输入不一致导致的确保build:types在build:js之前且两者读的是同一份源码。5.2 模块格式与互操作问题CJS使用方require后拿到的结构和预期不符多半是exports模式没设成named。ESM使用方import报没有默认导出则要检查源文件是否用了export default而产物格式处理不当。双格式库最容易在默认导出上翻车我的建议是库尽量只用命名导出少用默认导出互操作摩擦最小。还有一个隐蔽的坑package.json设了type: module后所有.js文件都被当成ESM如果CJS产物用了.js扩展名Node会按ESM解析而报错。这就是前面强调CJS产物用.cjs扩展名的原因。5.3 依赖处理的边界情况external漏配会导致第三方依赖被打进产物体积暴涨且可能引发重复实例问题。判断方法是看产物里有没有出现依赖包的代码特征。反过来external配多了但peerDependencies没声明使用方装库后运行时报模块找不到。这两个字段要成对维护。下面这张表是我整理的速查对照遇到问题先对号入座。现象可能原因处理方式使用方无类型提示types路径错或exports条件顺序错核对路径types放exports首位CJS引入拿到default包裹未设exports: namedoutput里补该字段产物体积异常大依赖未external检查external与peerDependenciesNode报ESM解析错误CJS产物用了.js且type: moduleCJS产物改.cjs扩展名声明缺导出合并时export *未展开检查源导出写法必要时显式导出5.4 几个踩过的坑和独家心得第一个坑是sourcemap的路径问题。rollup默认生成的map里sourceRoot可能指向构建机的绝对路径发布后使用方看到的是无意义的本地路径。可以在output里配sourcemapPathTransform或者干脆发布时不带map需要调试时单独提供。第二个心得是关于构建速度的。库大了以后每次全量构建很慢可以给rollup加cache或者开发时只跑单格式。但发布构建一定要全量跑别图快跳过声明合并。第三个是版本管理上的经验peerDependencies的版本范围要写宽一点比如^4.0.0而不是锁死小版本否则使用方项目里依赖版本稍有不同就报冲突。库的依赖策略应该是尽量宽松只约束大版本。6. 发布前的最后一道检查打包配置调通只是第一步真正发布前还有几件事要做。先跑一次npm pack --dry-run看打包进tarball的文件列表是否符合预期dist在、源码和配置不在这是最基本的要求。然后在一个干净目录里npm install这个tarball完整走一遍引入和调用模拟真实使用方的体验。我个人在实际操作中的体会是库打包这件事的难点从来不在rollup配置本身配置抄一抄就能跑难的是站在使用方角度验证产物。你自己项目里能跑不代表别人装了能跑因为你的环境里有源码、有完整的node_modules、有各种隐式配置。每次发布前把自己当成一个什么都不知道的新用户从零装一遍、用一遍大部分问题在这一步就会暴露。这个习惯帮我挡掉过好几次发布后才发现类型断了的尴尬比任何自动化检查都直接。
企业数字化 ERP 产品动态
相关推荐
向量数据库在AI Agent架构中的核心应用与实践 1. 向量数据库如何重塑AI Agent工程架构在开发智能客服系统时,我们曾遇到一个典型场景:当用户询问"上周三与张经理的会议纪要"时,传统数据库只能返回关键词匹配的结果,而引入向量数据库后,系统可以自动关联&… · 2026/11/3 3:42:36
Rollup打包TypeScript库:输出JS与类型声明文件全攻略 1. 为什么一个TS库的打包值得单独拿出来说如果你写过一段时间TypeScript,大概率经历过这个阶段:本地开发爽得不行,类型提示、自动补全、重构跳转全都到位,结果一到“要给别人用”的时候就开始犯难。代码是TS写的,但用的… · 2026/11/3 3:42:36
基于AT89S52与DAC0832的0.1V步进数控直流电源设计 简介:这份资源面向参加全国电子设计大赛的学生及电子电路学习者,聚焦「D简易数控直流电源」赛题,提供一套完整的数字显示电路设计方案,帮助读者理解数控电源从原理到实现的整体思路。压缩包内共1个doc文档,约131KB&… · 2026/11/3 3:41:36
代码注释规范:避免多语言陷阱的技术实践 1. 项目背景与核心问题那天下午的代码评审会,整个技术部门都记得清清楚楚。新来的技术总监王总正对着投影屏幕皱眉,突然整个人僵在了那里——他面前的IDE里,密密麻麻全是西夏文注释的Java代码。会议室里鸦雀无声,只听见空调出风口… · 2026/11/3 4:10:43
测试文档编写指南:从目标定义到最佳实践 1. 项目概述"测试文章标题01"这个看似简单的标题背后,其实蕴含着丰富的可能性。作为从业多年的技术博主,我深知每个项目标题都可能是一个技术宝藏的入口。今天我们就来深度挖掘这个标题背后的技术内涵和应用场景。在实际工作中,我们… · 2026/11/3 4:10:43
HarmonyOS音频焦点管理:音乐播放器开发实战 1. 项目概述作为一名在HarmonyOS音频开发领域深耕多年的开发者,今天我想和大家深入探讨一个在音乐播放器开发中至关重要却常被忽视的技术点——音频焦点管理。在HarmonyOS 6环境下,AudioSession机制作为音频焦点管理的核心实现,其设计理念和实… · 2026/11/3 4:10:43
Excel跨工作簿搬运工作表:移动复制、外部链接与批量处理 日常做报表的人几乎都碰过这个场景:手里有一个明细表,想让它在另一个工作簿里变成一张独立的 sheet,方便和总表的其他数据放一起看。听起来是三秒钟的事,实际操作起来,有人复制粘贴搞到格式全崩,有人搬完之… · 2026/11/3 4:10:43
高德地图API 2.0点聚合与点标记事件实战:参数调优与性能优化 做过地图类页面的朋友大概都有这个体会:一张地图上撒几十个点,随便怎么玩都没事;一旦点位数量上了千、上了万,问题就全冒出来了。浏览器开始掉帧,图标叠成一团糊,用户点半天点不到自己想要的那个位置。这时… · 2026/11/3 4:10:43
hindsight复盘法:把事后视角变成可执行的决策能力 1. hindsight到底在说什么:把“回看”变成一种可执行的能力先说结论:hindsight不是让你整天沉浸在“早知道当初就怎样”的懊悔里,而是一套把事后视角变成决策养料的完整方法体系。很多人一听到“后见之明”就把它当成马后炮,觉得复… · 2026/11/3 4:09:43
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18