由于公司要做前后端分离,我在搭建单页面应用的前端路由框架时引入了RequireJS,但由于RequireJS本身基于AMD规范实现时只提供了针对JS模块的加载和移除API,其他资源(Resource)是通过插件的方式扩展的,由于我这里需要在组件切换时加载对应组件的CSS模块并移除上一个组件的CSS模块,所以必须要引入对应的CSS插件,但进一步发现的问题是,由于AMD规范并没有在插件中提出移除相关的规范,以至于RequireJS在实现时,并未要求插件要实现对应的移除API,所以原本官方在设计CSS插件上只有加载插件的load方法和标准化方法normalize的实现,若要移除CSS模块那么必须修改对应CSS插件和RequireJS的源码。
经过几天对AMD规范进一步的考察以及RequireJS和CSS插件源码的理解,具体源码修改方案如下。
在原本CSS插件要求实现的load方法上新添加参数:
//other code
cssAPI.load = function (cssId, req, load, config,id) {
(useImportLoad ? importLoad : linkLoad)(req.toUrl(cssId + '.css'), load,id);
};
//other code
添加的id
最终是代表模块module
的ID,后面会详细说明。上述CSS插件考虑到浏览器兼容性,会根据用户当前浏览器判断使用哪种方式加载,第一个importLoad
方法是针对IE <9以及Firefox < 18版本,第二个linkLoad
是针对其他版本浏览器,由于我这里面向的用户群体的浏览器版本在IE10+和其他主流浏览器,所以我这里只修改对应内部的linkLoad
方法,对应linkLoad方法修改如下:
// other code
var linkLoad = function linkLoad(url, callback,id) {
var link = document.createElement('link');
link.type = 'text/css';
link.rel = 'stylesheet';
if (useOnload) link.onload = function () {
link.onload = function () {
};
// for style dimensions queries, a short delay can still be necessary
setTimeout(callback, 7);
};else var loadInterval = setInterval(function () {
for (var i = 0; i < document.styleSheets.length; i++) {
var sheet = document.styleSheets[i];
if (sheet.href == link.href) {
clearInterval(loadInterval);
return callback();
}
}
}, 10);
link.href = url;
link.setAttribute("data-requiremodule-css",id);
head.appendChild(link);
};
// other code
在原来添加link
元素设置属性的基础上,我再添加了data-requiremodule-css
属性并将前面传入的id作为属性值,CSS插件代码部分修改完毕。
上述相关修改的考虑我会在最后统一给出原因,大家如果有兴趣可以参考,赶时间照着上面修改即可。
在原来的AMD对插件的load方法规范上,再添加一个参数,用于传递模块的ID,该id的传入我们需要修改RequireJS代码中关于调用插件部分(callPlugin
)的代码,修改如下:
// 略
callPlugin: function () {
var map = this.map,
id = map.id,
//Map already normalized the prefix.
pluginMap = makeModuleMap(map.prefix);
// 这里是新赋值的地方
var mid = id;
// 略
plugin.load(map.name, localRequire, load, config,mid);
// 略
},
// 略
在上述RequireJS调用插件的 callPlugin
方法中,这里我为了区分,再添加了mid变量的创建并将原有的id变量赋值,然后在RequireJS调用插件的load方法中再添加了参数并将这里的mid传入,它对应前面修改CSS插件的load
方法中接受的id
参数。
再修改RequireJS原本移除JS模块统一提供的API处的代码:
// 略
if (!relMap) {
localRequire.undef = function (id) {
takeGlobalQueue();
var map = makeModuleMap(id, relMap, true),
mod = getOwn(registry, id);
mod.undefed = true;
//修改部分
if (id.indexOf("css!")>-1){
removeCss(id);
}else{
removeScript(id);
}
// 略
修改的核心就是在RequireJS统一提供移除模块的API的undef
方法进行修改,我们在原本它只删除JS模块的方法removeScript(id);
基础上增加一层判断分支,若是CSS插件加载的模块,那么该模块会带有css!
标识,css!
前部分 css
是我们引入CSS插件的名称,它是在RequireJS入口通过 config
配置的,若是其他命名这里对应修改即可。若是关于插件的移除,则调用我们这里添加的removeCss
方法,该方法在内部定义如下:
function removeCss(name) {
if (isBrowser) {
each(csss(), function (cssNode) {
if (cssNode.getAttribute('data-requiremodule-css') === name ) {
cssNode.parentNode.removeChild(cssNode);
return true;
}
});
}
}
该函数的位置可以放在原来的removeScript
函数同级的位置下。获取的元素的标准就是前面我们在CSS插件添加link
元素时添加的data-requiremodule-css
属性。
OK~
现在,我们要移除CSS插件加载的CSS模块时,我们只需要同样调用requirejs.undef
即可,将我们要移除的CSS模块ID传入即可,该模块ID就是我们在define
方法中的使用的,如:
define(['css!/css/foo.css'], function () {
//code
});
移除时,调用:
requirejs.undef('plugin/css!/css/foo')
即可。前面plugin/css
是我在config
中配置该CSS插件的路径,部分代码如下:
require.config({
// 略
map: {
//优先载入css模块插件,用于支持导入css文件,
'*': {
'css': 'plugin/css'
}
},
// 略
具体大家以各自的配置为准。
这里补充一下为什么调用移除时,原本上述在define
中设置的'css!/css/foo.css'
的.css
文件后缀不用添加,是因为该CSS插件实现了标准化normalize方法,将该后缀截掉了,该部分在CSS插件部分源码如下:
cssAPI.normalize = function (name, normalize) {
if (name.substr(name.length - 4, 4) == '.css') name = name.substr(0, name.length - 4);
return normalize(name);
};
这里插件实现的normalize
目的主要是用于做相对路径与资源完整路径的映射处理(mapping),它在AMD针对插件实现规范中是可选,如果不实现,那么会调用RequireJS默认的normalize
方法。
为什么要这样修改?
修改源码其实是迫不得已,因为AMD规范中并未要求插件提供全局作用域用于移除插件加载的相关资源模块的API对象,相反,插件的设计也是要求模块化定义,即,以define
方法进行定义,那么这样也可以知道如果要提供相关的移除API,是不可能让插件去注册的,而必须由RequireJS进行注册。移除的API虽然AMD规范并没有制定,但RequireJS本身进行实现时提供移除的API,即:
requirejs.undef("module-ID")
移除时它会调用如下方法对js模块进行remove:
function removeScript(name) {
if (isBrowser) {
each(scripts(), function (scriptNode) {
if (scriptNode.getAttribute('data-requiremodule') === name &&
scriptNode.getAttribute('data-requirecontext') === context.contextName) {
scriptNode.parentNode.removeChild(scriptNode);
return true;
}
});
}
}
这也说明了RequireJS只针对已加载的js模块的移除进行了实现,对于不同插件加载的相关资源模块并没有提供对应API。如果从框架设计层面来考虑,由于RequireJS并不知晓不同的插件是加载哪一种类型的资源文件,所以RequireJS并不能提供具体的实现,而应该规范插件进行相关移除功能的实现,而RequireJS只需要设计统一的去像调用插件的load
方法那样类似的钩子函数即可,然后对应的调用插件实现的移除API即可,只可惜AMD并未针对该部分制定规范,那么唯一的办法只能修改对应的CSS插件和RequireJS的源码才能达到目的。
上述修改是否会出现问题?
不会。这里我修改的核心主要是在原有基础上进行扩展,并未修改原来的逻辑,主要包括将对应加载的CSS模块ID设置到添加的link
元素中,以便后续移除时选中这类元素。
而我为什么会考虑在原来RequireJS已有的undef
方法上进行修改,而不是新添加一个接口?其实这样工作量会更大,因为这样需要新定义一个钩子并在CSS插件上添加移除的API供RequireJS调用,其实不利于短期尽快的解决问题。还有一个原因就是原来undef
方法中除了原本涉及的移除对应js模块的dom操作的方法,还涉及对相关模块缓存对象的操作,这个设计是必要的,比如当前我们加载了一个module-A
,后续其他模块在定义时也依赖于module-A
,此时是没有必要再重复加载的,RequireJS会对每一次异步加载的模块进行轮询检查,当成功的加载对应的模块后会加入到一个defined
对象中进行缓存,之后有其他模块加载时会先检查当前defined
对象中是否已经加载过对应模块,如果有,那么就不会加载,原来undef
方法中也做了这一步移除对应模块的缓存的操作,所以我这里决定把对CSS模块的移除也放在原来undef
方法中,虽然CSS和JS是不同类型的模块,但RequireJS会统一存储到defined
对象中管理。当然这种做法并不算完美,因为后续如果有需要加载其他类型的资源文件,那么得继续修改这部分代码,不过鉴于AMD没有这部分的规范,那么对于其他资源类型的插件也没有提供相关移除的API,其实这样的处理方式也就能接受。
其实我在做单页面应用组件切换时,事先做了对应模块ID名称的缓存,所以在移除时并不需要手动拼接对应的CSS模块ID,大家根据各自需求来进一步修改即可。
最后
大家如果这里修改有遇到问题,也可以评论或私信我,新年快乐~