在项目优化阶段,我们决定采用 Seajs 实现代码模块化。起初参照官方五分钟教程快速上手,整个开发流程相当顺畅,未遇到任何障碍。直到同事准备将打包后的代码部署到设备上进行测试时,发现程序完全无法运行——这才意识到问题所在。

经过逐行单步调试,确认模块始终无法成功加载。于是深入分析 Seajs 的源码,终于弄清了根本原因。
Seajs 在 define 模块时解析依赖有两种方式:一是直接从 define(id, deps, factory) 中的 deps 参数获取;二是通过对模块代码进行正则解析,从其中的 require 调用中提取。以下是核心代码片段:
Module.define = function (id, deps, factory) {
var argsLen = arguments.length
// define(factory)
if (argsLen === 1) {
factory = id
id = undefined
}
else if (argsLen === 2) {
factory = deps
// define(deps, factory)
if (isArray(id)) {
deps = id
id = undefined
}
// define(id, factory)
else {
deps = undefined
}
}
// Parse dependencies according to the module factory code
if (!isArray(deps) && isFunction(factory)) {
deps = parseDependencies(factory.toString())
}
...
如果传入了 deps 参数,则不再解析源码;否则会使用正则扫描 factory 函数体内的 require 调用:
var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|/*[Ss]*?*/|/(?:\\/|[^/rn])+/(?=[^/])|//.*|.s*require|(?:^|[^$])brequires(s*(["'])(.+?)1s*)/g
var SLASH_RE = /\\/g
function parseDependencies(code) {
var ret = []
code.replace(SLASH_RE, "")
.replace(REQUIRE_RE, function(m, m1, m2) {
if (m2) {
ret.push(m2)
}
})
return ret
}
由此可见,Seajs 依赖 require 这个关键词进行正则匹配。也就是说,在 Seajs 模块中,require 相当于一个具有特殊含义的“关键字”。
问题恰恰出在这里。大多数 JavaScript 压缩工具都会对变量名进行重命名(例如替换成简短的名字),但 require 并非标准的 JS 关键字,因此一经压缩,require 就可能被重命名为 a、b 之类的随机标识符。结果 Seajs 的正则解析再也无法匹配到 require,模块自然无法正常加载。
解决办法主要有两种:
1. 使用 Seajs 官方推荐的构建工具
Seajs 团队提供了一套标准模块构建方案:spm。采用 spm 进行构建,可以有效规避压缩导致的异常。
2. 更换压缩工具,配置不压缩 require 变量
目前主流的三个压缩工具——YUI Compressor、Google Closure Compiler、UglifyJS——前两者似乎都不支持自定义保留变量名(至少无法直接针对 require 做豁免),而 UglifyJS 提供了 -r 选项,可以指定某些变量名不被混淆。例如:
UglifyJS hello.js -o hello.min.js -m -c -r require
通过 -r require 参数,就能确保 require 保持原样。
从实际维护角度来看,如果项目深度依赖 Seajs,建议优先采用官方构建工具 spm,这样可以避免各种兼容性陷阱。当然,如果只是临时需要压缩模块,使用 UglifyJS 并加上 -r require 也不失为一种立竿见影的应急手段。
