也谈JavaScript模块化开发类库的实现
2014-05-04 15:23
190 查看
“五一”看了下sea.js,关于JavaScript模块化开发有些体会所以记录下来。一直以来,谈到web规范,都是很长很长的文献要读。但是关于JavaScript模块化的规范都比较简短,大概十分钟就看完了。关于JavaScript模块化规范起源于CommonJS,后来有几个比较著名的实现,比如nodejs,requirejs还是国人实现的seajs。其中requirejs遵从AMD(Asyncronous Module Definition),seajs后来又搞了个标准CMD(Common Module
Definition),关于这些起源历史争论在seajs的官方文档里有详细的介绍,我这边就不重复。
先抛开这些规范不说,如果是我们自己来实现一个模块化开发的基础类库的话,需要解决什么问题呢?
1. 最基本的是脚本异步动态加载,在没有这些类库之前,我想大部分人都写过类似的代码:创建动态script元素,然后导入JavaScript脚本;
2. 模块的依赖识别;
3. 模块的入口声明,就是每个模块定义都需要符合一定的规范,否则模块加载系统无法识别模块啊;
4. 模块加载系统的配置,对于每个具体的前端项目,其模块加载都会有些公共配置;
但是有几个细节需要主要,首先这段代码就等于是把脚本从加载开始一直到执行的控制权都交给了浏览器,这显然不是我们所期待的。有一些实际的问题,比如同一个模块文件被多个其他模块共同依赖,这时就没有比较把共同依赖的模块加载多次,虽然我们可以依靠浏览器的缓存来避免模块文件被多次下载,但是不能避免这个模块文件被重复执行。seajs把一个模块分成6种状态
node就会是这个base node而不是head node。第三个细节就是模块加载完成之后,我们需要把挂载在node上的回调函数置为null,并将该node从head中移掉,以释放内存。稍微解释下,因为onerror,onreadystatechange的回调函数是每次都要创建的临时匿名函数,以后很难有机会把他们占用的存储空间通过removeEventListener释放掉,所以需要将他们的引用断开,以便这部分内存被自动回收掉。
如果把以上几点都实现好了,那我们的模块加载系统就基本完成了。但是seajs在此之外还提供了一些额外的功能。
Definition),关于这些起源历史争论在seajs的官方文档里有详细的介绍,我这边就不重复。
先抛开这些规范不说,如果是我们自己来实现一个模块化开发的基础类库的话,需要解决什么问题呢?
1. 最基本的是脚本异步动态加载,在没有这些类库之前,我想大部分人都写过类似的代码:创建动态script元素,然后导入JavaScript脚本;
2. 模块的依赖识别;
3. 模块的入口声明,就是每个模块定义都需要符合一定的规范,否则模块加载系统无法识别模块啊;
4. 模块加载系统的配置,对于每个具体的前端项目,其模块加载都会有些公共配置;
脚本动态加载
最基本的思路就是利用script元素实现,借鉴mdn上的一段示范代码var importScript = (function (oHead) { function loadError (oError) { throw new URIError("The script " + oError.target.src + " is not accessible."); } return function (sSrc, fOnload) { var oScript = document.createElement("script"); oScript.type = "text\/javascript"; oScript.onerror = loadError; if (fOnload) { oScript.onload = fOnload; } oHead.appendChild(oScript); oScript.src = sSrc; } })(document.getElementsByTagName("head")[0]);
importScript("myScript1.js"); importScript("myScript2.js", /* onload function: */ function () { alert("You read this alert because the script \"myScript2.js\" has been correctly loaded."); });
但是有几个细节需要主要,首先这段代码就等于是把脚本从加载开始一直到执行的控制权都交给了浏览器,这显然不是我们所期待的。有一些实际的问题,比如同一个模块文件被多个其他模块共同依赖,这时就没有比较把共同依赖的模块加载多次,虽然我们可以依靠浏览器的缓存来避免模块文件被多次下载,但是不能避免这个模块文件被重复执行。seajs把一个模块分成6种状态
// 1 - The `module.uri` is being fetched FETCHING: 1, // 2 - The meta data has been saved to cachedMods SAVED: 2, // 3 - The `module.dependencies` are being loaded LOADING: 3, // 4 - The module are ready to execute LOADED: 4, // 5 - The module is being executed EXECUTING: 5, // 6 - The `module.exports` is available EXECUTED: 6当在模块加载的过程中,其状态会不断变化。还有一个细节就是浏览器对onload事件的支持,webkit版本小于535.23或firefox小于9的浏览器都不支持onload事件,对于这种情况seajs是用onreadstatechange事件来代替的。另外,seajs还可以把外部css文件作为模块来加载,关于这也有些浏览器兼容问题,在低版本的webkit和firefox中,需要用setTimeout不断轮询样式加载的状态。第二个细节就是把script node或link node插入到head时,如果head下有base节点,在IE6下不能直接调用head.appendChild,否则,新加node的parent
node就会是这个base node而不是head node。第三个细节就是模块加载完成之后,我们需要把挂载在node上的回调函数置为null,并将该node从head中移掉,以释放内存。稍微解释下,因为onerror,onreadystatechange的回调函数是每次都要创建的临时匿名函数,以后很难有机会把他们占用的存储空间通过removeEventListener释放掉,所以需要将他们的引用断开,以便这部分内存被自动回收掉。
依赖解决
咋一看这个问题,有些头大,是不是要做实现语法分析?这就变成编译器问题了,这个问题可以通过一定的代码规范来控制,我们可以在define模块时指定其依赖的其他模块,虽然这种方式对模块编写者要求比较高,并且不适合将已经编写好的模块导入到系统中,这需要比较的人工投入来列出模块所有的依赖。如果在define时没有指定依赖,那怎么办呢?seajs在处理这个问题上做了简化,总共处理代码不超过10行,但是足以处理绝大部分良好编程习惯下的情况了。var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g var SLASH_RE = /\\\\/g对,就是基于正则表达式在模块代码中查找require关键字。同样这种实现也有几个细节要注意。首先在匹配开始之前要把代码中的\\都删掉,避免在匹配语句"\\";var a = require('./a');时,错误认为\"是一个转义字符,而导致将后面全部认为是字符串的内容。第二个就是要过滤双引号包含的字符串,单引号包含的字符串,/**/的注释,/和/包含的正则表达式,//开头的注释。这部分内容里面的require可以忽略掉。
模块定义
模块定义尽量不要污染global命名空间,所以需要将require,exports,module作为参数传入模块的factory函数中,define(function(require, exports, module) {}),在这点上seajs和requirejs一样的。稍微解释一下,exports是模块的导出公共接头,和module.exports代表同一个对象,module指代当前模块。加载配置
一般就是支持配置模块的base path;然后可以加载外部的模块,这可以通过建立url映射来实现;也可以配置预加载项;对于路径复杂的模块,也可以定义别名;还需要有些配置项是方便模块开发过程中调试方便用的,比如debug。这些实现难度不大,所以不详细说了。如果把以上几点都实现好了,那我们的模块加载系统就基本完成了。但是seajs在此之外还提供了一些额外的功能。
插件支持
seajs认为,一切皆模块,它自身支持动态异步载入js,css文件之外,我们还可以通过插件方式支持将更多不同格式的模块动态载入。它的这种插件机制的实现跟我们传统意义理解的还不太一样,只能通过监听seajs提供的事件来扩展。我们看几个实现的例子,seajs-text,彻底就是改写了fetch方法,只不过用了一种比较猥琐的方式,request事件回调函数可以通过事件参数回传一个emitData.requested=true,这样原来fetch方法里的剩余部分就彻底不执行了。再比如seajs-combo,挂载的是fetch事件,通过事件参数emitData回传了requestUri,这儿就有必要搞清楚在编写模块时,require调用会具体执行什么操作,因为这个问题会直接关系到seajs事件触发的顺序,只有这个问题搞清楚了,才好编写插件啊。要回答这个问题,我们可以先回到模块状态上去,基本上模块加载时执行顺序跟模块状态是对应的,fetch->save->load->execute。我们再开看看seajs在这整个模块加载的过程中触发的事件顺序,resolve->load->fetch->request->exec。这两者比较的话,可能会发现一个现象,在模块状态中load师排在fetch之后,但是在模块加载过程中,事件load师排在fetch之前。nodejs运行环境的支持
如果seajs模块没有依赖window,或者做DOM操作,这些浏览器端特性,那么它们可以成功运行在nodejs上。究其具体实现,就好比是开发了一个seajs的插件挂载事件,然后把这些模块导到nodejs中。相关文章推荐
- SeaJS实现模块化JavaScript开发
- 使用SeaJS实现模块化JavaScript开发
- JavaScript使用自定义事件实现简单的模块化开发
- 使用SeaJS实现模块化JavaScript开发
- 使用SeaJS实现模块化JavaScript开发
- 使用SeaJS实现模块化JavaScript开发
- JavaScript使用自定义事件实现简单的模块化开发
- 使用SeaJS实现模块化JavaScript开发【转】
- 使用SeaJS实现模块化JavaScript开发
- 使用SeaJS实现模块化JavaScript开发
- 使用SeaJS实现模块化JavaScript开发
- 使用SeaJS实现模块化JavaScript开发
- 使用SeaJS实现模块化JavaScript开发
- 使用SeaJS实现模块化JavaScript开发
- 使用SeaJS实现模块化JavaScript开发
- 使用SeaJS实现模块化JavaScript开发2
- 使用SeaJS实现模块化JavaScript开发
- 使用SeaJS实现模块化JavaScript开发
- 使用SeaJS实现模块化JavaScript开发
- 使用SeaJS实现模块化JavaScript开发