关于前端JS模块加载器实现的一些细节

 

最近工做须要,实现一个特定环境的模块加载方案,实现过程当中有一些技术细节不解,便参考 了一些项目的api设计约定与实现,记录下来备忘。javascript

本文不探讨为何实现模块化,以及模块化相关的规范,直接考虑一些技术实现原理。html

1.简单实现模块化

一开始我想若是个人代码只有一个文件,那几行不就实现了吗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>

2.拆多个文件

后来业务需求的增加,个人一个代码文件逐渐膨胀到了接近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>

后来了解到,我能够用构件工具gulpconcatwatch模块,能够监听文件改动,自动生成大文件,以便在开发的时候能够按模块拆成多个文件,运行的时候倒是在一个文件。详细能够了解相关资料。

3.按模块加载文件

上面提到用构件工具来实现打包成一个文件,这样作有个缺点,代码若是有错误,报错的行数没法与相应文件模块的行数相对应,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 了。

4.控制文件加载顺序

script在加载过程当中会有一些状态,支持设立回调函数好比 onloadonreadysteadychange 这样咱们能够在当一个模块加载完成后加载另外一个模块来控制文件加载顺序。

咱们经常使用的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)
    })
})

这样的坏处即是,代码中要写层层回调,模块的加载顺序须要写代码的人本身来管理。

5.XHR加载代码

script标签能够设置src加载远程代码,还能够直接把代码写在标签内。

<script>
    define("A","i am A")
</script>

因而咱们能够经过XHR对象,加载远程代码文本,而后动态的插入进去,好比innerHTML 甚至,XHR有同步的加载方法,来让咱们串行的加载代码,避免写重重回调。固然,同步的XHR请求性能很低

XHR有个硬伤就是受浏览器同源策略影响,不能方便的跨域。

6.实现高级API

有了上面的一些基础,咱们就能够来封装一些高级的API了。

通常来讲,咱们只须要这样一个define(id,deps,factory),实现了模块的定义和加载就基本够用了。

define("moduleC",["moduleA","moduleB"],function(moduleA,moduleB){
    console.log(moduleA,moduleB)
})

这样的define作了这么一些事情

  • 将id 和 factory关联
  • 用loadscript的方案,去递归的加载deps,保证该模块被依赖时,模块自己依赖的模块都加载完毕。
  • 收集完毕后按照deps顺序将相关模块经过apply传递给factory

7.自动收集依赖

咱们以为每次去写一堆依赖,而后还要保证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变量压缩。

8.定义匿名模块

有时候咱们以为文件名已经可以表明模块名字了,咱们连定义模块名字都不想要了。

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。

这里关于浏览器兼容性有一些细节:

  • document.currentScript只有现代浏览器才支持。
  • IE6-10会有一些黑魔法,利用浏览器单线程执行的特性,获取页面上全部的script标签,判断其readystate为interactive时,该script即是document.currentScript
  • 利用Error.stack,获得文件调用栈,来分析获得currentScript

写匿名模块方即是方便,可是会带来一些麻烦。

好比,不能直接打包成一个文件了,由于依赖于模块的文件名,这个很好理解了。

define(function(){
    return 100
})
define(function(){
    return 200
})

9.加载文本资源

甚是怀念在孢子工做时的那套代码结构与模块化方案,开发不须要依赖构建工具,模板直接写html文件,不用包装amd。 等等。。,模板直接写html文件是怎么作到的,尝试去看源码,基本看不懂,孢子源码太难读了。后来抓包才知道,原来是后端配合,在特定的目录名称下返回的html文件会自动包上define,黑魔法。。

固然也有其余方法,通常状况下就是用XHR,加载相应地文本,而后用eval设定执行上下文环境为global,来包装define。

10.参考:

  • http://requirejs.org/docs/why.html
  • https://github.com/seajs/seajs/issues/259
  • http://www.cnblogs.com/rubylouvre/archive/2013/01/23/2872618.html
相关文章
相关标签/搜索