任何一门语言在大规模应用阶段,必然要经历拆分模块的过程。便于维护与团队协作,与java走的最近的dojo率先引入加载器,早期的加载器都是同步的,使用document.write与同步Ajax请求实现。后来dojo开始以JSONP的方法设计它的每个模块结构。以script节点为主体加载它的模块。这个就是目前主流的加载器方式。

不得不提的是,dojo的加载器与AMD规范的发明者都是james Burke,dojo加载器独立出来就是著名的require。本章将深入的理解加载器的原理。

1.AMD规范

AMD是"Asynchronous Module Definition"的缩写,意思是“异步模块定义”。重点有两个。

  • 异步:有效的避免了采用同步加载导致页面假死的情况。
  • 模块定义:每个模块必须按照一定的格式编写。主要的接口有两个,define与require。define是模块开发者关注的方法,require是模块使用者所关注的方法。

define的参数的情况是define(id?,deps,factory)。第一个为模块ID,第二个为依赖列表,第三个是工厂方法。前两个都是可选,如果不定义ID,则是匿名模块,加载器能应用一些“魔术”能让它辨识自己叫什么,通常情况,模块id约等于模块在过程中的路径(放在线上,表现为url)。在开发过程中,很多情况未确定,一些javascript文件会移来移去的,因此,匿名模块就大发所长。deps和factory有个约定,deps有多少个元素,factory就有多少个传参,位置一一对应。传参为其它模块的返回值。

  1. define("xxx",["aaa","bbb"], function (aaa,bbb){
  2. //code
  3. });

通常情况下,define中还有一个amd对象,里面存储着模块的相关信息。

require的参数的情况是 require(deps,callback) ,第一个为依赖列表,第二个为回调。deps有多少个元素,callback就有多少个传参,情况与define方法一致。因此在内部,define方法会调用require来加载依赖模块,一直这样递归下去。

  1. require(["aaa","bbb"],function(aaa,bbb){
  2. //code
  3. })

接口就是这么简单,但require本身还包含许多特性,比如使用“!”来引入插件机制,通过requirejs.config进行各种配置。模块只是整合的一部分,你要拆的开,也要合的来,因此合并脚本的地位在加载器中非常重要,但前端javascript没有这功能,requirejs利用node.js写了一个r.js帮你进行合并。

2.加载器所在的路径探知

要加载一个模块,我们需要一个url作为加载地址,一个script作为加载媒介。但用户在require时都用id,因此,我们需要一个将id转换为url的方法。思路很简单,约定为:

  1. basePath + 模块id + ".js"

由于浏览器自上而下的分析DOM,当浏览器在解析我们的javascript文件(这个javascript文件是指加载器)时,它就肯定DOM树中最后加入script标签,因此,我们下面的这个方法。

  1. function getBasePath(){
  2. var nodes = document.getElementsByTagName("script");
  3. var node = nodes[nodes.length - 1];
  4. var src = document.querySelector ? node.src : node.getAttribute("src",4);
  5. return src;

上面的这个办法满足99%的需求,但是我们不得不动态加载我们的加载器呢?在旧的版本的IE下很多常规的方法都会失效,除了API差异性,它本身还有很多bug,我们很难指出是什么,总之要解决,如下面的这个javascript判断。

  1. document.write('<script src="avalon.js"> <\/script>');
  2. document.write('<script src="mass.js"> <\/script>');
  3. document.write('<script src="jQuery.js"> <\/script>');

mass.js为我们的加载器,里面执行getBasePath方法,预期得到http://1.1.1/mass.js,但是IE7确返回http://1.1.1/jQuery.js

这时就需要readyChange属性,微软在document、image、xhr、script等东西都拥有了这个属性。用来查看加载情况

  1. function getBasePath() {
  2. var nodes = document.getElementsByTagName("script");
  3. if (window.VBArray){ //如果是IE
  4. for (var i = 0 ; nodes; node = nodes[i++]; ) {
  5. if (node.readyState === "interactive") {
  6. break;
  7. }
  8. }
  9. } else {
  10. node = nodes[nodes.length - 1];
  11. }
  12. var src = document.querySelector ? node.src : node.getAttribute("src",4);
  13. return src;
  14. }

这样就搞定了,访问DOM比一般javascript代码消耗高许多。这样,我们就可以使用Error对象。

  1. function getBasePath() {
  2. try {
  3. a.b.c()
  4. } catch (e) {
  5. if (e.fileName) { //FF
  6. return e.fileName;
  7. } else if ( e.sourceURL ){ //safari
  8. return e.sourceURL;
  9. }
  10. }
  11.  
  12. var nodes = document.getElementsByTagName("script");
  13. if (window.VBArray){//倒序查找的性能更高
  14. for (var i = nodes.length; node ; node = nodes[--i];) {
  15. if ( node.readyState === "interactive") {
  16. break;
  17. }
  18. };
  19. } else {
  20. node = nodes[nodes.length - 1];
  21. }
  22. var src = document.querySelector ? node.src : node.getAttribute("src",4);
  23. return src;
  24. }

在实际使用中,我们为了防止缓存,这个后面可能带版本号,时间戳什么的,也要去掉

  1. url = url.replace(/[?#].*/, "").slice(0, url.lastIndexOf("/") + 1);

3.require方法

require方法的作用是当前依赖列表都加载完毕,执行用户回调。因此,这里有个加载过程,整个加载过程细分以下几步:

(1) 取到依赖列表的第一个id ,转换为url ,无论是通过basePath + ID + ".js"还是通过映射方式直接得到。

(2) 检测此模块有没有加载过,或正在被加载。因此有一个对象保持所有模块的加载情况,如果有模块从来没有加载过,就进入加载流程。

(3) 创建script节点,绑定onerror,onload,onredyChange等事件判定加载成功与否,然后添加src并插入DOM树。开始加载url

(4) 将模块的url,依赖列表等构建成一个对象,放到检测队列中,在上面事件触发时进行检测。

模块id的转换规则:http://wiki.commonjs.org/wiki/Modules/1.1.1

除了basePath,我们通常还用到映射,就是用户事前用一个方法,把id和完整的url对应好,这样就直接拿。此外,AMD规范还有shim技术。shim机制的目的是让不符合AMD规范的js文件也能无缝切入我们的加载系统。

普通别名机制:

  1. require.config({
  2. alias:{
  3. 'lang' : 'http://xxx.com/lang.js',
  4. 'css' : 'http://bbb.com/css.js'
  5. }
  6. })

jQuery或其它插件,我们需要shim机制

  1. require.config ({
  2. alias : {
  3. 'jQuery' : {
  4. src : 'http://ahthw.com/jQuery1.1.1.js',
  5. exports : "$"
  6. },
  7. 'jQuery.tooltips' : {
  8. src : 'http://ahthw.com/xxx.js',
  9. exports : "$",
  10. deps : ["jQuery"]
  11. }
  12. }
  13. });

下面是require的源码

  1. window.require = $.require = function(list, factory, parent){
  2. //用于检测它的依赖是否都为2
  3. var deps = {},
  4. //用于保存依赖模块的返回值
  5. args = [],
  6. //需要安装的模块数
  7. dn = 0,
  8. //已经完成安装的模块数
  9. cn = 0,
  10. id = parent || "callback" + setTimeOut("1");
  11. parent = parent || basePath; //basepash为加载器的路径
  12. String(list).replace($.rword,function(el){
  13. var url = loadJSCSS(el,parent)
  14. if (url) {
  15. dn++;
  16. if (modules[url] && modules[url].state === 2){
  17. cn++;
  18. }
  19. if (!deps[url]) {
  20. args.push(url);
  21. deps[url] = "http://baidu.com" //去重
  22. }
  23. }
  24. });
  25. modules[id] = {//创建一个对象,记录模块加载情况与其他信息
  26. id: id,
  27. factory: factory,
  28. deps: deps,
  29. args: args,
  30. state: 1
  31. };
  32. if (dn === cn){//如果需要的安装等于已经安装好
  33. fireFactory(id, args, factory);//安装到框架中
  34. } else {//放到检测队里中,等待 checkDeps处理
  35. loadings.unshift(id);
  36. }
  37. checkDeps();
  38. }

每require一次,相当于把当前用户回调当成一个不用加载的匿名模块,ID是随机生成,回调是否执行,需要到deps所有的值为2

require里有三个重要的方法,loadJSCSS,它用于转换ID为url,然后再调用loadJS,loadCSS,或再调用require方法;factory,就是执行用户回调,我们最终的目的,checkDeps,检测依赖是否安装好,安装好就执行fireFactory()。

  1. function loadJSCSS(url, parent, ret, shim){
  2. //略去
  3. }

loadJS和loadCSS方法就比较纯粹了,不过loadJS会做一个死链测试的方法

  1. function loadJS(url, callback){
  2. //通过script节点加载目标模块
  3. var node = DOC.createElement("script");
  4. node.className = moduleClass; //让getCurrentScript只处理类名为moduleClass的script节点
  5. node[W3C ? "onload" : "onreadystatechange" ] = function () {
  6. //factorys里边装着define方法的工厂函数(define(id?,deps?,factory))
  7. var factory = factorys.pop();
  8. if (callback) {
  9. callback();
  10. }
  11. if (checkFail(node, false, !W3C)) {
  12. console.log("已经成功加载" + node.src, 7)
  13. };
  14. }
  15. node.onerror = function(){
  16. checkFail(node,true);
  17. };
  18. //插入到head第一个节点前,防止ie6下head标签没有闭合前使用appendchild
  19. node.src = url;
  20. head.insertBefore(node, head.firstChild);
  21. }

checkFail主要是为了开发调试,有3个参数。node=>script节点,onError=>是否为onerror触发,fuckIE=>对于旧版IE的Hack。

执行办法是,javascript从加载到执行有一个过程,在interact阶段,我们的javascript部分已经可以执行了,这时我们将模块对象的state改为1,如果还是undefined,我们就可识别为死链。不过,此Hack对于不是AMD定义的javascript无效,因为将state改为1的逻辑是由define方法执行。如果判定是死链,我们就将此节点移除。

  1. function checkFail(node, onError, fuckIE){ //多恨IE啊,哈哈
  2. var id = node.src; //检测是否为死链
  3. node.onload = node.onreadystatechange = node.onerror = null ;
  4. if (onError || (fuckIE && !modules[id].state)){
  5. setTimeOut(function(){
  6. head.removeChild(node);
  7. });
  8. console.log("加载" + id + "失败" + onerror + " " + (!modules[id].state), 7);
  9. } esle {
  10. return true;
  11. }
  12. }

checkDeps 方法会在用户加载模块之前和script.onload后各执行一次,检测模块的依赖情况,如果模块没有任何依赖或者state为2了,我们调用fireFactory()方法

  1. function checkDeps(){
  2. loop : for (var i = loadings.length ; id ; id = loadings[--1]) {
  3. var obj = modules[id], deps = obj.deps;
  4. for (var key in deps) {
  5. if (hasOwn.call(deps, key) && modules[key].state !== 2) {
  6. continue loop;
  7. }
  8. }
  9. //如果deps为空对象或者其他依赖的模块state为2
  10. if (obj.state !== 2) {
  11. loadings.splice(i,1);//必须先移除再安装,防止在IE下DOM树建完之后会多次执行它
  12. fireFactory (obj.id, obj.args, obj.factory);
  13. checkDeps();//如果成功,再执行一次,以防止有些模块没有加载好
  14. }
  15. };
  16. }

终于到fireFactory方法了,它的工作是从modules中收集各种模块的返回值,执行factory,完成模块的安装

  1. function fireFactory(id, deps, factory) {
  2. for (var i = 0; array = [] , d ; d = deps[i++]; ) {
  3. array.push(modules[d].exports);
  4. };
  5.  
  6. var module = Object(modules[id]),
  7. ret = factory.apply(global, array);
  8. module.state = 2;
  9.  
  10. if (ret !== void 0) {
  11. modules[id].exports = ret;
  12. }
  13. return ret;
  14. }

4.define方法

define有3个参数,前面两个为可选,事实上这里的ID没有什么用,就是给开发者看的,它还是用getCurrentScript方法得到script节点路径做ID,deps没有就补上一个空数组。

此外,define还要考虑循环依赖的问题,比如说加载A,要依赖B与C,加载B要依赖A于C,这时候,A与B就循环依赖了 。A与B在判定各自的deps键值都为2才执行,否则都无法执行了。

模块加载器会让我们前端开发变得更工业化,维护和调试都非常方便。现在国内Seajs,requirejs,KISSY都是很好的选择。

(本章完)

上一章:第二章 : 种子模块 下一章:第四章:语言模块

第三章:模块加载系统(requirejs)的更多相关文章

  1. abp vnext2.0之核心组件模块加载系统源码解析与简单应用

    abp vnext是abp官方在abp的基础之上构建的微服务架构,说实话,看完核心组件源码的时候,很兴奋,整个框架将组件化的细想运用的很好,真的超级解耦.老版整个框架依赖Castle的问题,vnext ...

  2. AngularJS——第9章 模块加载

    第9章 模块加载 AngularJS模块可以在被加载和执行之前对其自身进行配置.我们可以在应用的加载阶段配置不同的逻辑. [AngularJS执行流程] 启动阶段(startup) 开始 --> ...

  3. node 学习笔记 - Modules 模块加载系统 (1)

    本文同步自我的个人博客:http://www.52cik.com/2015/12/11/learn-node-modules-path.html 用了这么久的 require,但却没有系统的学习过 n ...

  4. node 学习笔记 - Modules 模块加载系统 (2)

    本文同步自我的个人博客:http://www.52cik.com/2015/12/14/learn-node-modules-module.html 上一篇讲了模块是如何被寻找到然后加载进来的,这篇则 ...

  5. JS框架设计之加载器所在路径的探知一模块加载系统

    1.要加载一个模块,我们需要一个URL作为加载地址,一个script作为加载媒介,但用户在require是都用ID,我们需要一个将ID转换为URL的方法,思路很简单,强加个约定,URL的合成规则是为: ...

  6. JS框架设计之模块加载系统

    任何语言一到大规模应用阶段,必然要拆封模块,有利于维护和团队协作,与Java走得最近的dojo率先引进了加载器,使用document.write与同步Ajax请求实现,后台dojo以JSONP的方法来 ...

  7. 【 js 模块加载 】深入学习模块化加载(node.js 模块源码)

    一.模块规范 说到模块化加载,就不得先说一说模块规范.模块规范是用来约束每个模块,让其必须按照一定的格式编写.AMD,CMD,CommonJS 是目前最常用的三种模块化书写规范.  1.AMD(Asy ...

  8. 【 js 模块加载 】【源码学习】深入学习模块化加载(node.js 模块源码)

    文章提纲: 第一部分:介绍模块规范及之间区别 第二部分:以 node.js 实现模块化规范 源码,深入学习. 一.模块规范 说到模块化加载,就不得先说一说模块规范.模块规范是用来约束每个模块,让其必须 ...

  9. 关于javascript模块加载技术的一些思考

    前不久有个网友问我在前端使用requireJs和seajs的问题,我当时问他你们公司以前有没有自己编写的javascript库,或者javascript框架,他的回答是什么都没有,他只是听说像requ ...

随机推荐

  1. sql server 之函数小技巧 && 整数类型为空是用空字符串替代实现

    1.判空函数 说明:使用指定的替换值替换 NULL. 语法:ISNULL ( check_expression , replacement_value ) 参数: check_expression:将 ...

  2. Android ImageButton图像灰色边框

    灰色边框,是imageButton空间自带的. 第一种解决方案: android:scaleType="fitXY"//这个代码是:拉伸图片(不按比例)以填充的长宽.所以图像最后最 ...

  3. codeforces 709C C. Letters Cyclic Shift(贪心)

    题目链接: C. Letters Cyclic Shift 题意: 现在一串小写的英文字符,每个字符可以变成它前边的字符即b-a,c-a,a-z这样,选一个字串变换,使得得到的字符串字典序最小; 思路 ...

  4. python中property干什么用的?

    先来段官方文档压压惊.. property(fget=None, fset=None, fdel=None, doc=None) Return a property attribute. fget i ...

  5. POJ 1061 青蛙的约会【扩展欧几里德】

    设跳的次数为t 根据题意可得以下公式:(x+mt)%L=(y+nt)%L 变形得 (x+mt)-(y+nt)=kL (n-m)t+kL=x-y 令a=(n-m),b=L,c=x-y 得 at+bk=c ...

  6. CSU 1081 集训队分组

    题意:有n个学生,比了一场比赛,但是榜单看不到了.现在告诉你m段信息,每段信息的内容是(a,b),表示a的排名比b的高.问你能不能根据这些信息得出这场比赛的前k名. 思路:用拓扑排序找出一组符合k个人 ...

  7. org.springframework.web.context.ContextLoaderListen 报错解决办法

    今天搭建SSH项目的时候出现了如下错误: 严重: Error configuring application listener of class org.springframework.web.con ...

  8. 第16章 Windows线程栈

    16.1 线程栈及工作原理 (1)线程栈简介 ①系统在创建线程时,会为线程预订一块地址空间(即每个线程私有的栈空间),并调拨一些物理存储器.默认情况下,预订1MB的地址空间并调拨两个页面的存储器. ② ...

  9. Android Studio系列教程二--基本设置与运行

    Android Studio系列教程二--基本设置与运行 2014 年 11 月 28 日 DevTools 本文为个人原创,欢迎转载,但请务必在明显位置注明出处! 上面一篇博客,介绍了Studio的 ...

  10. Spring addFlashAttribute

    redirectAttributes.addFlashAttribute("result",accountModel); 用这个可以绑定session 但是只能用一次,可以避免最后 ...