最近工做须要,实现一个特定环境的模块加载方案,实现过程当中有一些技术细节不解,便参考 了一些项目的api设计约定与实现,记录下来备忘。javascript
本文不探讨为何实现模块化,以及模块化相关的规范,直接考虑一些技术实现原理。html
一开始我想若是个人代码只有一个文件,那几行不就实现了吗java
main.jsgit
var modules = {} var define = function(id,factory){ moudles[id] = factory } var require = function(id){ return modules[id] } define("moduleA",{text:"I am text"}) var moduleA = require("moduleA"); console.log(moduleA)
main.htmlgithub
<script src="main.js"></script>
后来业务需求的增加,个人一个代码文件逐渐膨胀到了接近2w多行。这个时候每次改动文件找函数找半天啊,俺的编辑器也时不时的开始崩溃了,传到服务端的时候,也要等很久很久了。。。json
因而我把文件拆成了3个:gulp
1.module.js后端
var modules = {} var define = function(id,factory){ moudles[id] = factory } var require = function(id){ return modules[id] }
2.moduleA.jsapi
define("moduleA",{text:"I am text"})
3.main.js跨域
var A = require("moduleA") console.log(A)
因而我在html中得这么写了
<script src="module.js"><script> <script src="moduleA.js"><script> <script src="main.js"><script>
后来了解到,我能够用构件工具gulp
的concat
和watch
模块,能够监听文件改动,自动生成大文件,以便在开发的时候能够按模块拆成多个文件,运行的时候倒是在一个文件。详细能够了解相关资料。
上面提到用构件工具来实现打包成一个文件,这样作有个缺点,代码若是有错误,报错的行数没法与相应文件模块的行数相对应,debug困难。
这个时候貌似只有不依赖于构件工具,咱们在代码中实现加载其余模块。 貌似也挺简单的。
咱们得知道,script标签是能够用JS动态建立和加载的
var loadScript = function(src){ var script = document.createElement("script") script.src = src document.head.appendChild(script) }
因而咱们能够在main.js中这样去加载
loadScript("module.js") loadScript("moduleA.js")
这样就能够在页面中只引入一个主文件,而后在主文件中引入其余模块文件了。
多了解一些咱们会知道 loadScript
这样的代码加载方法,是并行加载非顺序执行的,有可能moduleA的代码执行的时候module尚未执行,这是就会报错 variable define is not defined
了。
script在加载过程当中会有一些状态,支持设立回调函数好比 onload
、 onreadysteadychange
这样咱们能够在当一个模块加载完成后加载另外一个模块来控制文件加载顺序。
咱们经常使用的jsonp技术便也大概是这样一个原理。
var loadScript = function(src,callback){ var script = document.createElement("script") script.src = src script.onload = callback document.head.appendChild(script) } loadScript("module.js",function(){ loadScript("moduleA.js",function(){ var A = require("moduleA") console.log(A) }) })
这样的坏处即是,代码中要写层层回调,模块的加载顺序须要写代码的人本身来管理。
script标签能够设置src加载远程代码,还能够直接把代码写在标签内。
<script> define("A","i am A") </script>
因而咱们能够经过XHR对象,加载远程代码文本,而后动态的插入进去,好比innerHTML 甚至,XHR有同步的加载方法,来让咱们串行的加载代码,避免写重重回调。固然,同步的XHR请求性能很低
XHR有个硬伤就是受浏览器同源策略影响,不能方便的跨域。
有了上面的一些基础,咱们就能够来封装一些高级的API了。
通常来讲,咱们只须要这样一个define(id,deps,factory)
,实现了模块的定义和加载就基本够用了。
define("moduleC",["moduleA","moduleB"],function(moduleA,moduleB){ console.log(moduleA,moduleB) })
这样的define作了这么一些事情
咱们以为每次去写一堆依赖,而后还要保证deps顺序和factory的变量顺序一致,一一对应着实有些蛋疼,这时候咱们会想要把deps去掉,改为在factory里面写依赖。
moduleC.js
define("moduleC",function(require){ var moduleA = require("moduleA") var moduleB = require("moduleB") })
这时候须要用到JS的一个神奇的特性,function的toString方法能够拿到函数的源代码。 这样咱们能够经过一些手段分析出 require 了哪些模块。能够看这里 https://github.com/seajs/seajs/issues/478
固然为了可以分析出require了哪些模块,咱们要对require作一些约定,就是但愿require有一些特定的标志,以便于咱们可以经过代码文本静态的分析出require项。
好比说 不可以这样,详细见 https://github.com/seajs/seajs/issues/259
var req = require req("moduleA")
而后呢,也不能用通用的压缩工具压缩,由于压缩工具会把require变量压缩。
有时候咱们以为文件名已经可以表明模块名字了,咱们连定义模块名字都不想要了。
moduleC.js
define(function(){ var moduleA = require("moduleA") var moduleB = require("moduleB") return { A : moduleA, B : moduleB }; })
当初看到这样的api用法时都震惊了,由于以前实现define的时候都会把id和factory相关联,这没ID怎么办?后来冷静下来,以为ID必定是有的,只是有办法不经过函数参数传递。
果真,有一个document有一个对象叫作currentScript,能够得到当前正在执行的script的对象,因而moduleC.js在执行的时候,define是能够经过document.currentScript拿到src为moduleC.js的script对象的,进而能够提取出ID。
这里关于浏览器兼容性有一些细节:
interactive
时,该script即是document.currentScript写匿名模块方即是方便,可是会带来一些麻烦。
好比,不能直接打包成一个文件了,由于依赖于模块的文件名,这个很好理解了。
define(function(){ return 100 }) define(function(){ return 200 })
甚是怀念在孢子工做时的那套代码结构与模块化方案,开发不须要依赖构建工具,模板直接写html文件,不用包装amd。 等等。。,模板直接写html文件是怎么作到的,尝试去看源码,基本看不懂,孢子源码太难读了。后来抓包才知道,原来是后端配合,在特定的目录名称下返回的html文件会自动包上define,黑魔法。。
固然也有其余方法,通常状况下就是用XHR,加载相应地文本,而后用eval设定执行上下文环境为global,来包装define。