UMD通用模块定义
UMD(Universal Module Definition,通用模块定义)是一种JavaScript模块模式,旨在创建可以在多种环境中工作的模块。它结合了CommonJS、AMD和全局变量模式的特点,使模块能够在Node.js、浏览器和AMD加载器中都能正常工作。
什么是UMD
UMD是一种模块包装器模式,具有以下特点:
- 跨平台兼容: 同一个模块可以在不同环境中使用
- 自动检测: 自动检测当前环境并选择合适的模块系统
- 向后兼容: 支持旧版本浏览器和旧的模块系统
- 无依赖: 不需要额外的加载器或构建工具
- 灵活性: 可以根据需要定制检测逻辑
UMD基本模式
1. 标准UMD模式
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD环境
define(['dependency1', 'dependency2'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node.js/CommonJS环境
module.exports = factory(require('dependency1'), require('dependency2'));
} else {
// 浏览器全局变量环境
root.MyModule = factory(root.Dependency1, root.Dependency2);
}
}(typeof self !== 'undefined' ? self : this, function (dependency1, dependency2) {
'use strict';
// 模块实现
function MyModule() {
// 构造函数
}
MyModule.prototype.method1 = function() {
return 'method1 result';
};
MyModule.prototype.method2 = function() {
return dependency1.someFunction() + dependency2.someOtherFunction();
};
// 返回模块
return MyModule;
}));
2. 简化版UMD
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.MyModule = factory());
}(this, function () {
'use strict';
// 模块实现
var MyModule = {
version: '1.0.0',
init: function() {
console.log('MyModule initialized');
},
destroy: function() {
console.log('MyModule destroyed');
}
};
return MyModule;
}));
3. 无依赖UMD模式
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define([], factory);
} else if (typeof exports === 'object') {
// Node.js
module.exports = factory();
} else {
// 浏览器全局变量
root.Utils = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
var Utils = {
// 类型检查工具
isArray: Array.isArray || function(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
},
isObject: function(obj) {
return obj !== null && typeof obj === 'object' && !this.isArray(obj);
},
isFunction: function(obj) {
return typeof obj === 'function';
},
// 对象工具
extend: function(target) {
var sources = Array.prototype.slice.call(arguments, 1);
sources.forEach(function(source) {
for (var key in source) {
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
});
return target;
},
clone: function(obj) {
if (!this.isObject(obj)) return obj;
var cloned = {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = this.isObject(obj[key]) ? this.clone(obj[key]) : obj[key];
}
}
return cloned;
},
// 数组工具
unique: function(array) {
var result = [];
for (var i = 0; i < array.length; i++) {
if (result.indexOf(array[i]) === -1) {
result.push(array[i]);
}
}
return result;
},
// 字符串工具
camelCase: function(str) {
return str.replace(/-([a-z])/g, function(match, letter) {
return letter.toUpperCase();
});
},
kebabCase: function(str) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
};
return Utils;
}));
实际应用示例
1. 数学工具库
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.MathUtils = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
var MathUtils = {
// 常量
PI: Math.PI,
E: Math.E,
// 基础运算
add: function(a, b) {
return a + b;
},
subtract: function(a, b) {
return a - b;
},
multiply: function(a, b) {
return a * b;
},
divide: function(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
},
// 高级运算
power: function(base, exponent) {
return Math.pow(base, exponent);
},
sqrt: function(n) {
if (n < 0) throw new Error('Cannot calculate square root of negative number');
return Math.sqrt(n);
},
factorial: function(n) {
if (n < 0) throw new Error('Cannot calculate factorial of negative number');
if (n === 0 || n === 1) return 1;
var result = 1;
for (var i = 2; i <= n; i++) {
result *= i;
}
return result;
},
// 几何计算
circleArea: function(radius) {
return this.PI * radius * radius;
},
circleCircumference: function(radius) {
return 2 * this.PI * radius;
},
rectangleArea: function(width, height) {
return width * height;
},
triangleArea: function(base, height) {
return 0.5 * base * height;
},
// 统计函数
average: function(numbers) {
if (!Array.isArray(numbers) || numbers.length === 0) {
throw new Error('Input must be a non-empty array');
}
var sum = numbers.reduce(function(acc, num) {
return acc + num;
}, 0);
return sum / numbers.length;
},
median: function(numbers) {
if (!Array.isArray(numbers) || numbers.length === 0) {
throw new Error('Input must be a non-empty array');
}
var sorted = numbers.slice().sort(function(a, b) { return a - b; });
var middle = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 0) {
return (sorted[middle - 1] + sorted[middle]) / 2;
} else {
return sorted[middle];
}
},
max: function(numbers) {
if (!Array.isArray(numbers) || numbers.length === 0) {
throw new Error('Input must be a non-empty array');
}
return Math.max.apply(Math, numbers);
},
min: function(numbers) {
if (!Array.isArray(numbers) || numbers.length === 0) {
throw new Error('Input must be a non-empty array');
}
return Math.min.apply(Math, numbers);
}
};
return MathUtils;
}));
2. 事件发射器
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.EventEmitter = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
function EventEmitter() {
this._events = {};
this._maxListeners = 10;
}
EventEmitter.prototype = {
constructor: EventEmitter,
// 添加事件监听器
on: function(event, listener) {
if (typeof listener !== 'function') {
throw new TypeError('Listener must be a function');
}
if (!this._events[event]) {
this._events[event] = [];
}
this._events[event].push(listener);
// 检查监听器数量
if (this._events[event].length > this._maxListeners) {
console.warn('MaxListenersExceededWarning: Possible memory leak detected. ' +
this._events[event].length + ' ' + event + ' listeners added.');
}
return this;
},
// 添加一次性事件监听器
once: function(event, listener) {
if (typeof listener !== 'function') {
throw new TypeError('Listener must be a function');
}
var self = this;
function onceWrapper() {
listener.apply(this, arguments);
self.removeListener(event, onceWrapper);
}
onceWrapper.listener = listener;
return this.on(event, onceWrapper);
},
// 移除事件监听器
removeListener: function(event, listener) {
if (typeof listener !== 'function') {
throw new TypeError('Listener must be a function');
}
if (!this._events[event]) {
return this;
}
var listeners = this._events[event];
for (var i = listeners.length - 1; i >= 0; i--) {
if (listeners[i] === listener || listeners[i].listener === listener) {
listeners.splice(i, 1);
break;
}
}
if (listeners.length === 0) {
delete this._events[event];
}
return this;
},
// 移除所有监听器
removeAllListeners: function(event) {
if (event) {
delete this._events[event];
} else {
this._events = {};
}
return this;
},
// 触发事件
emit: function(event) {
if (!this._events[event]) {
return false;
}
var listeners = this._events[event].slice();
var args = Array.prototype.slice.call(arguments, 1);
for (var i = 0; i < listeners.length; i++) {
try {
listeners[i].apply(this, args);
} catch (error) {
console.error('Error in event listener:', error);
}
}
return true;
},
// 获取监听器列表
listeners: function(event) {
return this._events[event] ? this._events[event].slice() : [];
},
// 获取监听器数量
listenerCount: function(event) {
return this._events[event] ? this._events[event].length : 0;
},
// 设置最大监听器数量
setMaxListeners: function(n) {
if (typeof n !== 'number' || n < 0) {
throw new TypeError('n must be a non-negative number');
}
this._maxListeners = n;
return this;
},
// 获取最大监听器数量
getMaxListeners: function() {
return this._maxListeners;
}
};
// 静态方法
EventEmitter.listenerCount = function(emitter, event) {
return emitter.listenerCount(event);
};
return EventEmitter;
}));
3. HTTP客户端
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.HttpClient = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
// 检测环境
var isNode = typeof module === 'object' && module.exports;
var XMLHttpRequest = isNode ? null : (
typeof XMLHttpRequest !== 'undefined' ? XMLHttpRequest :
typeof ActiveXObject !== 'undefined' ? function() {
return new ActiveXObject('Microsoft.XMLHTTP');
} : null
);
function HttpClient(baseURL, defaultOptions) {
this.baseURL = baseURL || '';
this.defaultOptions = defaultOptions || {};
this.interceptors = {
request: [],
response: []
};
}
HttpClient.prototype = {
constructor: HttpClient,
// 请求方法
request: function(options) {
var self = this;
// 合并配置
var config = this._mergeConfig(options);
// 应用请求拦截器
config = this._applyRequestInterceptors(config);
if (isNode) {
return this._nodeRequest(config);
} else {
return this._browserRequest(config);
}
},
// GET请求
get: function(url, options) {
return this.request(this._extend({ method: 'GET', url: url }, options));
},
// POST请求
post: function(url, data, options) {
return this.request(this._extend({ method: 'POST', url: url, data: data }, options));
},
// PUT请求
put: function(url, data, options) {
return this.request(this._extend({ method: 'PUT', url: url, data: data }, options));
},
// DELETE请求
delete: function(url, options) {
return this.request(this._extend({ method: 'DELETE', url: url }, options));
},
// 浏览器环境请求
_browserRequest: function(config) {
var self = this;
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(config.method, self._buildURL(config.url), true);
// 设置请求头
if (config.headers) {
for (var header in config.headers) {
xhr.setRequestHeader(header, config.headers[header]);
}
}
// 设置超时
if (config.timeout) {
xhr.timeout = config.timeout;
}
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
var response = {
data: self._parseResponse(xhr.responseText, xhr.getResponseHeader('Content-Type')),
status: xhr.status,
statusText: xhr.statusText,
headers: self._parseHeaders(xhr.getAllResponseHeaders()),
config: config
};
response = self._applyResponseInterceptors(response);
if (xhr.status >= 200 && xhr.status < 300) {
resolve(response);
} else {
reject(new Error('Request failed with status ' + xhr.status));
}
}
};
xhr.onerror = function() {
reject(new Error('Network Error'));
};
xhr.ontimeout = function() {
reject(new Error('Request Timeout'));
};
// 发送请求
var data = config.data ? self._serializeData(config.data) : null;
xhr.send(data);
});
},
// Node.js环境请求
_nodeRequest: function(config) {
var self = this;
return new Promise(function(resolve, reject) {
var http = require('http');
var https = require('https');
var url = require('url');
var parsedUrl = url.parse(self._buildURL(config.url));
var isSecure = parsedUrl.protocol === 'https:';
var client = isSecure ? https : http;
var requestOptions = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || (isSecure ? 443 : 80),
path: parsedUrl.path,
method: config.method,
headers: config.headers || {}
};
var req = client.request(requestOptions, function(res) {
var data = '';
res.on('data', function(chunk) {
data += chunk;
});
res.on('end', function() {
var response = {
data: self._parseResponse(data, res.headers['content-type']),
status: res.statusCode,
statusText: res.statusMessage,
headers: res.headers,
config: config
};
response = self._applyResponseInterceptors(response);
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(response);
} else {
reject(new Error('Request failed with status ' + res.statusCode));
}
});
});
req.on('error', function(error) {
reject(error);
});
if (config.timeout) {
req.setTimeout(config.timeout, function() {
req.abort();
reject(new Error('Request Timeout'));
});
}
// 发送数据
if (config.data) {
req.write(self._serializeData(config.data));
}
req.end();
});
},
// 添加拦截器
addRequestInterceptor: function(interceptor) {
this.interceptors.request.push(interceptor);
},
addResponseInterceptor: function(interceptor) {
this.interceptors.response.push(interceptor);
},
// 辅助方法
_mergeConfig: function(options) {
return this._extend({}, this.defaultOptions, options);
},
_buildURL: function(url) {
if (url.indexOf('http') === 0) {
return url;
}
return this.baseURL + url;
},
_extend: function(target) {
var sources = Array.prototype.slice.call(arguments, 1);
sources.forEach(function(source) {
for (var key in source) {
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
});
return target;
},
_serializeData: function(data) {
if (typeof data === 'string') return data;
return JSON.stringify(data);
},
_parseResponse: function(data, contentType) {
if (contentType && contentType.indexOf('application/json') !== -1) {
try {
return JSON.parse(data);
} catch (e) {
return data;
}
}
return data;
},
_parseHeaders: function(headerStr) {
var headers = {};
if (!headerStr) return headers;
headerStr.split('\r\n').forEach(function(line) {
var parts = line.split(': ');
if (parts.length === 2) {
headers[parts[0].toLowerCase()] = parts[1];
}
});
return headers;
},
_applyRequestInterceptors: function(config) {
var result = config;
this.interceptors.request.forEach(function(interceptor) {
result = interceptor(result);
});
return result;
},
_applyResponseInterceptors: function(response) {
var result = response;
this.interceptors.response.forEach(function(interceptor) {
result = interceptor(result);
});
return result;
}
};
// 静态方法
HttpClient.create = function(baseURL, defaultOptions) {
return new HttpClient(baseURL, defaultOptions);
};
return HttpClient;
}));
UMD变体和优化
1. 返回工厂函数的UMD
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['jquery'], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('jquery'));
} else {
root.MyPlugin = factory(root.jQuery);
}
}(typeof self !== 'undefined' ? self : this, function ($) {
'use strict';
// 返回工厂函数而不是构造函数
return function(options) {
var defaults = {
width: 300,
height: 200,
animation: true
};
var settings = $.extend({}, defaults, options);
return {
init: function() {
console.log('Plugin initialized with settings:', settings);
},
destroy: function() {
console.log('Plugin destroyed');
},
getSettings: function() {
return settings;
}
};
};
}));
2. 支持ES6模块的UMD
(function (global, factory) {
if (typeof exports === 'object' && typeof module !== 'undefined') {
// CommonJS
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
// AMD
define(factory);
} else {
// 浏览器全局变量
(global = global || self).MyModule = factory();
}
}(this, function () {
'use strict';
class MyClass {
constructor(options = {}) {
this.options = Object.assign({
debug: false,
timeout: 5000
}, options);
}
async fetchData(url) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
if (this.options.debug) {
console.error('Fetch error:', error);
}
throw error;
}
}
processData(data) {
return data.map(item => ({
...item,
processed: true,
timestamp: Date.now()
}));
}
}
// 同时支持类和工厂函数
MyClass.create = function(options) {
return new MyClass(options);
};
return MyClass;
}));
3. 懒加载UMD
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['exports'], factory);
} else if (typeof exports === 'object' && typeof module !== 'undefined') {
factory(exports);
} else {
factory((root.LazyModule = {}));
}
}(typeof self !== 'undefined' ? self : this, function (exports) {
'use strict';
var modules = {};
var loading = {};
function loadModule(name, factory) {
if (!modules[name]) {
modules[name] = {
loaded: false,
factory: factory,
exports: null
};
}
}
function getModule(name) {
var module = modules[name];
if (!module) {
throw new Error('Module not found: ' + name);
}
if (!module.loaded) {
module.exports = module.factory();
module.loaded = true;
}
return module.exports;
}
async function getModuleAsync(name) {
if (loading[name]) {
return loading[name];
}
if (modules[name] && modules[name].loaded) {
return modules[name].exports;
}
loading[name] = new Promise(function(resolve) {
setTimeout(function() {
resolve(getModule(name));
delete loading[name];
}, 0);
});
return loading[name];
}
// 预定义一些模块
loadModule('utils', function() {
return {
formatDate: function(date) {
return date.toISOString().split('T')[0];
},
generateId: function() {
return Math.random().toString(36).substr(2, 9);
}
};
});
loadModule('validator', function() {
return {
isEmail: function(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
},
isUrl: function(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
}
};
});
// 导出API
exports.load = loadModule;
exports.get = getModule;
exports.getAsync = getModuleAsync;
exports.list = function() {
return Object.keys(modules);
};
}));
构建工具支持
1. Webpack UMD输出
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
path: __dirname + '/dist',
filename: 'my-library.js',
library: 'MyLibrary',
libraryTarget: 'umd',
globalObject: 'typeof self !== "undefined" ? self : this'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
};
2. Rollup UMD输出
// rollup.config.js
import babel from '@rollup/plugin-babel';
import { terser } from 'rollup-plugin-terser';
export default {
input: 'src/index.js',
output: {
file: 'dist/my-library.umd.js',
format: 'umd',
name: 'MyLibrary',
globals: {
'lodash': '_',
'jquery': '$'
}
},
external: ['lodash', 'jquery'],
plugins: [
babel({
exclude: 'node_modules/**',
babelHelpers: 'bundled'
}),
terser()
]
};
测试UMD模块
1. 多环境测试
// test/umd-test.js
(function() {
'use strict';
// 测试不同环境
function testEnvironments() {
var results = {
amd: false,
commonjs: false,
global: false
};
// 模拟AMD环境
if (typeof define === 'undefined') {
global.define = function(deps, factory) {
results.amd = typeof factory() === 'object';
};
global.define.amd = true;
// 重新加载模块
// 这里需要重新执行UMD模块代码
delete global.define;
}
// 模拟CommonJS环境
if (typeof module === 'undefined') {
global.module = { exports: {} };
global.exports = global.module.exports;
// 重新加载模块
// 这里需要重新执行UMD模块代码
results.commonjs = typeof global.module.exports === 'object';
delete global.module;
delete global.exports;
}
// 测试全局变量环境
// 重新加载模块
// 这里需要重新执行UMD模块代码
results.global = typeof global.MyModule === 'object';
return results;
}
// 功能测试
function testFunctionality(module) {
var tests = {
instantiation: false,
methods: false,
properties: false
};
try {
var instance = new module();
tests.instantiation = true;
if (typeof instance.method1 === 'function') {
tests.methods = true;
}
if (instance.hasOwnProperty('someProperty')) {
tests.properties = true;
}
} catch (error) {
console.error('Test error:', error);
}
return tests;
}
console.log('UMD Environment Tests:', testEnvironments());
// console.log('UMD Functionality Tests:', testFunctionality(MyModule));
})();
优缺点分析
优点
- ✅ 跨平台兼容: 同一代码可在多种环境中使用
- ✅ 无构建依赖: 不需要额外的构建步骤
- ✅ 向后兼容: 支持旧版本浏览器和模块系统
- ✅ 灵活部署: 可以选择最适合的分发方式
- ✅ 简单易用: 使用标准JavaScript语法
缺点
- ❌ 代码冗余: 包装代码增加了文件大小
- ❌ 复杂性: 模块包装逻辑较为复杂
- ❌ 性能开销: 运行时环境检测有微小开销
- ❌ 调试困难: 包装器可能影响调试体验
- ❌ 维护负担: 需要考虑多种环境的兼容性
现代替代方案
1. 构建时转换
// 源码使用ES模块
export class MyClass {
constructor() {
// ...
}
}
export default MyClass;
// 构建工具自动生成UMD版本
2. 条件导出
{
"name": "my-package",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"browser": "./dist/index.umd.js",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"browser": "./dist/index.umd.js"
}
}
}
总结
UMD作为过渡期的解决方案,解决了模块系统碎片化的问题:
- 🎯 历史价值: 在模块标准化过程中发挥了重要作用
- 🎯 实用性: 对于需要广泛兼容的库仍然有用
- 🎯 现状: 逐渐被ES模块和现代构建工具取代
虽然UMD在现代开发中使用频率降低,但理解其设计原理对于构建跨平台JavaScript库和理解模块化发展历程仍然很有价值。随着ES模块的普及和构建工具的完善,UMD正在从“必需品“转向“兼容性选项“。
下一章: 模块化工具与实践 →