前端老手都在用的模块规范:JS UMD如何融合AMD与CommonJS
- 前端老手都在用的模块规范:JS UMD如何融合AMD与CommonJS
- 引言:当你的代码既要跑在浏览器又要兼容Node
- UMD到底是个啥?一文说清它的来龙去脉
- 深入UMD的内部结构:三段式写法揭秘
- 逐行解剖
- AMD和CommonJS的核心差异快速回顾
- UMD如何巧妙识别运行环境并自动适配
- UMD的典型使用场景:库作者的必备武器
- 实际开发中怎么写出一个标准的UMD模块
- 需求
- 步骤一:先写裸逻辑(完全不考虑模块)
- 步骤二:把依赖也做成 UMD
- 步骤三:主模块合并依赖并导出
- 步骤四:验证三种场景
- UMD带来的好处:一份代码,多端兼容
- UMD也有坑:常见兼容性问题和边界情况
- 调试UMD模块时的排查思路:从加载失败到作用域错乱
- 提升UMD开发效率的小技巧:工具链与模板推荐
- 别再手写UMD了!自动化构建方案大赏
- UMD之外还有谁?顺带聊聊ES Modules的冲击
- 那些年我们踩过的UMD雷区:真实项目复盘
- 复盘一:第三方广告脚本冲突
- 复盘二:服务端渲染内存泄漏
- 给未来的自己留条后路:UMD模块的可维护性设计
前端老手都在用的模块规范:JS UMD如何融合AMD与CommonJS
如果把前端模块化比作江湖,AMD 是潇洒的剑客,CommonJS 是沉稳的刀客,而 UMD 就是那个左右逢源、见人说人话见鬼说鬼话的——江湖百晓生。今天咱们就掀开他的马甲,看看他到底怎么在浏览器和 Node 之间两头骗吃骗喝。
引言:当你的代码既要跑在浏览器又要兼容Node
故事从一个加班夜开始。
凌晨两点,测试小姐姐在群里甩了一张截图:
“为什么同样的utils.js,在 Chrome 里好好的,一到 Jest 就报define is not defined?”
你揉揉惺忪的睡眼,心里万马奔腾:
“老子写的是 AMD,Node 当然不认识define啊!”
于是你痛定思痛,决定写一份**“哪里都能跑”的代码。
UMD(Universal Module Definition)就这样被提上了日程。
它不是什么新框架,也不是什么黑魔法,只是一段“环境识别 + 分支导出”**的样板代码,却能让你的模块在浏览器、Node、甚至 WebWorker 里都能愉快地自我介绍:
“Hi,我是 utils,无论你们叫
define还是require,我都能接住。”
UMD到底是个啥?一文说清它的来龙去脉
UMD 诞生于 2012 年,那会儿前端圈正上演“三国演义”:
- AMD(RequireJS 为代表):浏览器异步加载,推崇“依赖前置”。
- CommonJS(Node.js 为代表):同步加载,文件即模块,简洁直接。
- Globals:老牌
<script>标签,简单粗暴,一不小心就污染全局。
三方割据,库作者苦不堪言:
“我想让 jQuery 既能被require(['jquery'], ...)又能被const $ = require('jquery')还能被<script src>直接引,怎么办?”
于是社区大佬们拍脑袋写出了 UMD 的雏形:
一份立即执行函数(IIFE),里面做三次“if 判断”,谁认识我我就跟谁走。
没有规范文档,只有一段约定俗成的“三段式”模板,却奇迹般地跑通了**“浏览器 + 服务器 + 全局变量”**三大场景。
深入UMD的内部结构:三段式写法揭秘
UMD 的骨架只有 30 行,却处处是心眼。咱们先上一段“标准模板”,再逐行拆骨:
/** * 标准 UMD 模板 * @param {string} name 全局变量名,浏览器直接引用时挂在 window 上 * @param {function} factory 模块工厂函数,返回你要导出的东西 */(function(root,factory){// 段一:AMD 环境识别if(typeofdefine==='function'&&define.amd){define([],factory);}// 段二:Node 环境识别elseif(typeofmodule==='object'&&module.exports){module.exports=factory();}// 段三:全局变量兜底else{root.helloUmd=factory();}}(typeofself!=='undefined'?self:this,function(){// 真正的模块逻辑functionsayHi(){console.log('Hi, I am UMD, I can run everywhere!');}// 暴露 APIreturn{sayHi};}));逐行解剖
外层 IIFE
把全局变量root和模块工厂factory作为参数传进来,避免在内部硬编码window或global,这样就能在浏览器、Node、WebWorker 里通用。段一:AMD
define.amd是 RequireJS 留下的“暗号”,只有 AMD 加载器才会挂这个属性。
判断通过就调用define([], factory),把模块注册给加载器。段二:CommonJS
module.exports是 Node 的“身份证”。
注意这里直接执行factory()并把返回值赋给module.exports,不会传require进去,所以工厂函数里如果用到依赖,得自己提前require好(后面会讲优化方案)。段三:全局变量
如果既不在 AMD 也不在 CommonJS,就把返回值挂在root上。
浏览器里root === window,Node 里root === global,WebWorker 里root === self,一条语句全覆盖。工厂函数
真正的业务逻辑写在这里,返回值就是模块导出的内容。
可以是对象、函数、甚至构造函数,随你开心。
AMD和CommonJS的核心差异快速回顾
在继续深挖 UMD 之前,先花 30 秒复习一下两位“前任”的性格差异,方便后面理解 UMD 为什么要“见风使舵”。
| 特性 | AMD | CommonJS |
|---|---|---|
| 加载方式 | 异步 | 同步 |
| 依赖声明 | 前置数组['dep1','dep2'] | 就近require() |
| 导出方式 | 返回值给define | 给module.exports |
| 典型场景 | 浏览器、按需加载 | 服务器、打包工具 |
| 代码示例 | define(['a'], a => {...}) | const a = require('a') |
一句话总结:
AMD 像“点餐”,先点好后上菜;CommonJS 像“自助餐”,随吃随拿。
UMD 的任务就是:“不管你们餐厅啥规矩,我都能坐下吃饭。”
UMD如何巧妙识别运行环境并自动适配
上面那段模板已经展示了“识别”的核心:
靠全局变量嗅探,而不是用户配置。
这样做的好处是零配置、零依赖,坏处是稍微不慎就误判。
咱们把常见的“嗅探链”拉长一点,看看有哪些坑:
// 更严谨的 AMD 嗅探constisAmd=typeofdefine==='function'&&define.amd;// 为什么不用 define.constructor?因为某些打包工具会改写 define// 更严谨的 Node 嗅探constisNode=typeofmodule!=='undefined'&&module.exports&&typeofrequire!=='undefined'&&!isAmd;// 防止某些打包后把 module 也带上还有更极端的场景:
Electron 主进程里既有module.exports又有window,此时你想让代码被当成 Node 模块而不是全局变量,就得把段二放在段三前面,模板里顺序很关键!
UMD的典型使用场景:库作者的必备武器
开源库
jQuery、Lodash、Moment.js 早期版本全部自带 UMD。
用户想<script>就<script>,想require就require,想define就define,不挑环境才是好库。公司级 SDK
比如支付、埋点、监控这类必须**“投放到第三方页面”**的脚本,
你永远不知道对方是 React、Vue 还是 10 年前的 jQuery 残局,
一份 UMD 丢过去,立刻可用,大大减少对接成本。微前端遗留模块
老项目用 RequireJS,新项目用 Webpack,
中间层写个 UMD 做“翻译官”,让新旧子应用都能引用同一套工具库,
老板再也不用在“重构”和“不重构”之间反复横跳。
实际开发中怎么写出一个标准的UMD模块
下面咱们把“模板”升级成“实战”,写一个带依赖的 UMD 工具库:
tiny-event-bus——一个只有 2KB 的发布订阅器。
需求
- 支持 AMD、CommonJS、全局三种方式
- 依赖一个
tiny-uuid用来生成事件 ID - 提供
on、off、emit三个 API
步骤一:先写裸逻辑(完全不考虑模块)
// tiny-event-bus.core.jsfunctioncreateBus(){constevents=Object.create(null);functionon(type,handler){(events[type]||(events[type]=[])).push(handler);}functionoff(type,handler){if(events[type]){constidx=events[type].indexOf(handler);if(idx>-1)events[type].splice(idx,1);}}functionemit(type,...args){(events[type]||[]).forEach(fn=>fn(...args));}return{on,off,emit};}步骤二:把依赖也做成 UMD
// tiny-uuid.umd.js(function(root,factory){if(typeofdefine==='function'&&define.amd){define([],factory);}elseif(typeofmodule==='object'&&module.exports){module.exports=factory();}else{root.tinyUuid=factory();}}(typeofself!=='undefined'?self:this,function(){returnfunctionuuid(){return'u-'+Math.random().toString(36).slice(2)+Date.now().toString(36);};}));步骤三:主模块合并依赖并导出
// tiny-event-bus.umd.js(function(root,factory){if(typeofdefine==='function'&&define.amd){// AMD 环境下,依赖数组里声明 tiny-uuiddefine(['./tiny-uuid.umd'],factory);}elseif(typeofmodule==='object'&&module.exports){// Node 环境下,同步 requireconstuuid=require('./tiny-uuid.umd');module.exports=factory(uuid);}else{// 浏览器全局,先拿全局变量root.TinyEventBus=factory(root.tinyUuid);}}(typeofself!=='undefined'?self:this,function(uuid){// 这里放前面的裸逻辑functioncreateBus(){constevents=Object.create(null);functionon(type,handler){(events[type]||(events[type]=[])).push(handler);}functionoff(type,handler){if(events[type]){constidx=events[type].indexOf(handler);if(idx>-1)events[type].splice(idx,1);}}functionemit(type,...args){(events[type]||[]).forEach(fn=>fn(...args));}return{on,off,emit};}// 返回构造函数,方便用户 new 或直接调用returncreateBus;}));步骤四:验证三种场景
- 浏览器
<script>标签
<scriptsrc="tiny-uuid.umd.js"></script><scriptsrc="tiny-event-bus.umd.js"></script><script>constbus=newTinyEventBus();bus.on('hello',data=>console.log('received:',data));bus.emit('hello',{msg:'umd rocks'});</script>- RequireJS
requirejs(['tiny-event-bus.umd'],function(TinyEventBus){constbus=newTinyEventBus();bus.on('hello',console.log);bus.emit('hello','from amd');});- Node
constTinyEventBus=require('./tiny-event-bus.umd');constbus=newTinyEventBus();bus.on('hello',console.log);bus.emit('hello','from node');全部跑通,一份代码,三端齐活。
此刻你可以自信地拍拍胸口:
“UMD?不过如此。”
UMD带来的好处:一份代码,多端兼容
零配置消费
用户不用管你是 Webpack 还是 RequireJS,直接拿来就用,降低心智负担。打包体积友好
没有额外运行时垫片,UMD 只是几行判断语句,对体积影响忽略不计。社区惯性
很多老牌库只提供 UMD,学会它你就能无痛源码贡献,而不是把人家整个仓库重构成 ESM。SEO/SSR 友好
服务端渲染时,Node 端直接require拿到结果,浏览器端再异步加载,同构渲染不尴尬。
UMD也有坑:常见兼容性问题和边界情况
Electron 双上下文
主进程有module.exports也有window,如果你的段三写在段二后面,会被误判成全局变量。
解决:把 CommonJS 判断提前,或显式exports.__esModule = true给打包器提示。Webpack 4 默认
libraryTarget: 'umd'不处理外部依赖
结果打出来的包把lodash也打包进去了,体积爆炸。
解决:externals: { lodash: 'lodash' },并在 UMD 模板里把依赖当成全局变量引入。Sea.js CMD 误判
早期 Sea.js 也实现了define,但没挂define.amd,导致 UMD 走到全局分支,找不到依赖。
解决:加&& define.amd判断即可,Sea.js 用户请自重。WebWorker 里
this指向self
模板里如果粗暴写this而不是self,会拿不到全局对象。
解决:用typeof self !== 'undefined' ? self : this兼容。
调试UMD模块时的排查思路:从加载失败到作用域错乱
先看网络面板
浏览器下报错define is not defined,99% 是 RequireJS 没引或者路径错了。
用requirejs.config({ paths: {...} })把别名配好。再看控制台打印
Node 下报factory is not a function,大概率你把factory写成了对象。
记住:UMD 的factory必须是一个返回导出的函数,而不是直接module.exports = {}。断点进 IIFE
在三段判断里分别console.log('amd')、console.log('cjs')、console.log('global'),
看走到哪一支,再对照环境就能快速定位误判。检查循环依赖
UMD 里如果 A 依赖 B,B 又依赖 A,Node 端会返回未完成副本,浏览器端可能直接undefined。
解决:把共享逻辑抽到 C,别学洋葱圈。
提升UMD开发效率的小技巧:工具链与模板推荐
- rollup-plugin-umd
一键把 ESM 转成 UMD,支持外部依赖映射,配置比 Webpack 少 10 倍。
// rollup.config.jsimport{nodeResolve}from'@rollup/plugin-node-resolve';exportdefault{input:'src/index.js',output:{file:'dist/bundle.umd.js',format:'umd',name:'MyUtils',// 全局变量名globals:{lodash:'_'// 告诉 rollup 外部依赖在全局下叫 _}},external:['lodash'],plugins:[nodeResolve()]};yo generator-umd
老牌脚手架,回答三个问题就能生成带测试用例的 UMD 仓库,单元测试 + ESLint + Travis CI一条龙。vscode 代码片段
把模板扔进用户代码片段,输入umd回车自动生成骨架,3 秒起步。
{"UMD Template":{"prefix":"umd","body":["(function (root, factory) {"," if (typeof define === 'function' && define.amd) {"," define($2, factory);"," } else if (typeof module === 'object' && module.exports) {"," module.exports = factory($3);"," } else {"," root.$1 = factory($4);"," }","}(typeof self !== 'undefined' ? self : this, function ($5) {"," $0","}));"]}}别再手写UMD了!自动化构建方案大赏
手写模板固然浪漫,但项目上了规模,依赖一多、版本一多,手写就是灾难。
下面给你三套“懒人套餐”,按项目规模自取:
rollup + terser
适合工具库,输出 ESM + UMD + min 三份,modern 模式还能打 Tree-shaking。Webpack 5
library.type: 'umd'
适合业务组件库,配合externals把 React、Vue 排除,**library.name支持对象路径library: { name: [‘MY’, ‘UI’], type: ‘umd’ },最终挂在window.MY.UI` 上,命名空间清清爽爽。Vite 4
lib.mode: 'umd'
开发时用 ESM,构建时--mode lib一键出 UMD,HMR + TypeScript体验丝滑,适合快速原型。
UMD之外还有谁?顺带聊聊ES Modules的冲击
UMD 再香,也只是**“过渡方案”。
2015 年 ES Modules 落地后,浏览器原生<script type="module">就能import,Node.js 14 也正式支持.mjs,“天下大同”**似乎就在眼前。
但现实是:
- 国内还有 30% 的 IE11 项目跑在国企内网
- 老掉牙的 CMS 只让
<script>引 - 服务端渲染要兼顾 CJS 缓存
所以短期看 UMD 不会消失,长期看会逐渐退居二线,变成**“兼容性兜底”角色。
新库建议“ESM 为主 + UMD 为辅”**:package.json里同时写
{"type":"module","main":"./dist/index.umd.js","module":"./dist/index.esm.js"}让现代打包器走 ESM,老项目走 UMD,两手都要硬。
那些年我们踩过的UMD雷区:真实项目复盘
复盘一:第三方广告脚本冲突
背景
页面里引了 A 广告商的 UMD 脚本,内部用了window.$,
后来 B 广告商也引了一个 UMD,同样挂window.$,
结果后面那个把前面覆盖,全站广告点不动,客户当场暴走。
根因
UMD 全局分支默认把名字挂死,没有命名空间。
解决
把脚本包一层自执行匿名函数,用const $ = window.$;保存快照,
或者让构建工具在 UMD 头部加noConflict(),主动释放命名权。
复盘二:服务端渲染内存泄漏
背景
Next.js 项目里用了一个 UMD 图表库,
每次渲染都require一次,结果缓存没命中,内存飙到 2G。
根因
UMD 模板在 Node 环境返回的是工厂函数执行结果,
如果里面用了闭包缓存,而 Next.js 每次require都重新执行,
就会把旧闭包留在内存里。
解决
把重量级对象提出工厂函数,做成单例,或者直接用 ESM,让 Node 本身做缓存。
给未来的自己留条后路:UMD模块的可维护性设计
语义化版本 + 变更日志
UMD 一旦发布,就无法在线热更新,所以严格遵守 SemVer,
哪怕改一行注释,也要提patch 版本,让下游放心锁版本。TypeScript 生成声明文件
提供index.d.ts,让现代编辑器智能提示,
同时把声明文件也打到 UMD 包里,全局变量也能提示。单元测试三端跑
用 Jest 跑 Node,用 Karma + RequireJS 跑 AMD,
用纯 HTML +<script>跑全局,CI 里三条流水线,
任何一段判断分支被改坏,立即飘红。文档里写清“全局变量名”
很多开发者不看源码,直接<script>引,
如果你在 README 里大字提示:“引完后会有window.TinyEventBus”,
能节省 50% 的 issue 提问。
写到这里,天已微亮。
你把最后一行注释敲完,push 仓库,打包发布。
测试小姐姐再次甩来截图——这次是一个绿色的对勾。
你长舒一口气,喃喃道:
“UMD 老矣,尚能饭否?”
答案是:“能,只要地球还在跑 IE,UMD 就永远有饭碗。”
于是你合上电脑,心里却悄悄给未来的自己留了一张便签:
“下一版,记得把 ESM 也加上。”
欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!
| 专栏系列(点击解锁) | 学习路线(点击解锁) | 知识定位 |
|---|---|---|
| 《微信小程序相关博客》 | 持续更新中~ | 结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等 |
| 《AIGC相关博客》 | 持续更新中~ | AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结 |
| 《HTML网站开发相关》 | 《前端基础入门三大核心之html相关博客》 | 前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识 |
| 《前端基础入门三大核心之JS相关博客》 | 前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。 通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心 | |
| 《前端基础入门三大核心之CSS相关博客》 | 介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页 | |
| 《canvas绘图相关博客》 | Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化 | |
| 《Vue实战相关博客》 | 持续更新中~ | 详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅 |
| 《python相关博客》 | 持续更新中~ | Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具 |
| 《sql数据库相关博客》 | 持续更新中~ | SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能 |
| 《算法系列相关博客》 | 持续更新中~ | 算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维 |
| 《IT信息技术相关博客》 | 持续更新中~ | 作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识 |
| 《信息化人员基础技能知识相关博客》 | 无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方 | |
| 《信息化技能面试宝典相关博客》 | 涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面 | |
| 《前端开发习惯与小技巧相关博客》 | 持续更新中~ | 罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等 |
| 《photoshop相关博客》 | 持续更新中~ | 基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结 |
| 日常开发&办公&生产【实用工具】分享相关博客》 | 持续更新中~ | 分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具 |
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!