我们在开发一个插件时,通常希望它既可以被服务端引用,也能被客户端浏览器使用,即适用多个平台。于是,在模块加载的处理上,我们通常会采用 UMD(Universal Module Definition)的写法。

所谓的 UMD,主要是针对 CommonJS 或 AMD 规范加载模块的差异,而特意封装的一种通用引入模块的解决方案。

UMD 的基本代码如下:

;(function (context, name, definition) {
if (typeof module != 'undefined' && module.exports) {
// CommonJS
module.exports = definition();
} else if (typeof define === 'function' && define.amd) {
// AMD
define(definition);
} else {
// Browser
context[name] = definition();
}

}(this, 'Countdown', function () {

// 插件主代码
class Countdown {}

return Countdown;
}));

上面的代码,也是我前段时间写的一个倒计时插件(Countdown)最前面的部分代码。当插件在本地开发完成,要开启定时器,只需要在页面里实例化 Countdown 构造函数即可,如:

new Countdown();

一切都进行的很顺利,但当我用webpack编译打包准备发布时,发现编译出错了,提示 Countdown is not defined。怎么回事?经过排查,发现原因出在模块加载上。我们来细看下 UMD:

if (typeof module != 'undefined' && module.exports) {
// CommonJS
module.exports = definition();
} else if (typeof define === 'function' && define.amd) {
// AMD
define(definition);
} else {
// Browser
context[name] = definition();
}

首先,判断是否处于node服务器环境,如果是,则把 插件 挂载到 module.exports。

如果不是,再判断当前环境有没有使用 AMD 规范,如果有,则将 插件 传入到第三方模块加载器(如RequireJS)定义方法中。

如果也没检测到使用 AMD 规范来加载模块,则直接将 插件 挂载到当前环境的全局变量(浏览器为 window),context[name] 中的 context 为 window,而 name 为插件名称,这个名称是包裹 umd 匿名函数调用时传入的参数(倒计时插件为 Countdown)。

而 webpack 编译时,可能是运行在node环境的原因,typeof module != 'undefined' && module.exports 的值竟然为 true,这就导致本该挂载在浏览器 window 对象的 插件,却挂载到了 module.exports。如此一来,浏览器自然获取不到 Countdown 这个构造函数。报 Countdown is not defined 也合乎情理。

那么,如何既保证代码以 UMD 形式输出,又可以让webpack编译通过呢?

一、webpack 处理 UMD

经翻阅文档发现,webpack 在 ouput输出选项上,提供了 libraryTarget 设置,如果用户添加了该选项,并把它的值设置为 umd,像这样:

output: {
...
libraryTarget: 'umd'
}

那么,打包后输出的模块,外部会套一层 umd 代码。

于是,我们需要删除原来插件里 umd 代码,并使用如下方式输出模块:

module.exports = Countdown;

再设置 libraryTarget 选项后,再用 webpack 编译打包一次。果然,打包后的代码前面被 umd 处理了,如下图:

与之类似的,还有一个 umdNamedDefine 选项,可参见这里

虽然解决了 umd 的输出问题,但控制台还是报 Countdown is not defined

前面说到,针对浏览器环境,我们可以会直接将 插件 挂载到当前环境的全局变量(浏览器为 window)上,即 context[name] = definition()。 而这里的 name 需要你手动指定,比如文章开头代码里的 'Countdown'

类似的,在webpack这里,光指定模块的输出形式是不够的,还需要指定模块的名称,同样的,webpack提供了 library 选项,它的值为模块名称。所以,将配置修改为:

output: {
...
libraryTarget: 'umd',
library: 'Countdown'
}

设置完成后,再webpack编译打包一次,倒计时正常运行了。

二、处理 ES6 的类

在前面处理中,通过 module.exports = Countdown 这种方式来输出模块。我们尝试改为 ES6 的语法,即:

export default class Countdown {
...
}

当webpack再次编译运行时,又报错了 Countdown is not a constructor

Countdown 不是一个构造函数?

在控制台打印 Countdown,发现它是一个包含 default 属性的对象,而 default 的值是一个函数,该函数正是我们所定义的构造函数。

于是,我们得先在页面存储 default 属性,再进行实例化才能正常运行倒计时:

var Countdown = Countdown.default;

new Countdown();

之所以会出现这种情况,是因为我在 webpack 中使用了 Babel 来编译 ES6模块,而 Babel 会将模块编译成一个对象,并把模块指定在该对象的 default 属性。

三、总结

webpack可通过配置 libraryTarget: 'umd' 选项来输出 umd 形式的模块,同时,你需要使用 library 属性来指定模块名称,这种配置一般出现在通用模块或插件中。

使用 Babel 编译的 ES6模块,输出的内容并非模块本身,它被包含在同名对象的 default 属性中。