目录

思路

发现问题

解决等待加载seajs的问题

解决脚本按依赖加载的问题

实现autoload

加载嵌入页面的脚本

完整的entry.js



项目第二版本开始开发的时候,我觉得应该对脚本干点什么。因为之前的脚本引用太多,太杂,且不说引用的第三方脚本,也不说每个页面自己的业务脚本,就项目组自己写的,项目的公共脚本都有十余个,不好好管理真是不行。那么很自然的,是RequireJS,SeaJS之类的加载工具。本文不纠结如何选择,反正最终选择了SeaJS。


其实引入seajs,除了想把脚本模块化之外,还有一个很重要的目的,就是希望在每个页面都只需要引入很少,甚至1个脚本,就能根据需要引入其它的脚本,不用再在一个页面文件中写大量的 <script ...></script> 标签了。


记得之前seajs可以在引入的时候自动加载config,然后再根据标签中的一个 data-xxx 配置项加载页面的入口脚本,不过我在 seajs 2 的文档中没发现有这样的用法,可能是由于某些原因取消了吧。所以我准备自己写一个脚本,这个脚本要干这么些事情:


思路

  • 这个脚本命名为 entry.js

  • 首先加载sea.js,并调用 seasj.config 进行基本的配置

  • 然后加载所有(或绝大部分)页面都需要加载的公共脚本,比如jquery、jquery-easyui等

  • 最后根据当前页面的地址加载对应的页面脚本 

    1. 如果页面不需要自己的业务脚本,不加载

    2. 如果页面需要自己的的业务脚本,且脚本是独立文件,则按页面路径放置在 js 目录中相应位置,自动加载

    3. 如果页面有业务脚本不是嵌入式脚本,需要在所有公共脚本加载完成之后调用。


根据这个思路,entry.js 大概应该这样写


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// @(#) entry.js
( function () {
     // 通过document.write加载sea.js
     document.write( '<script type="text/javascript" src="/js/modules/sea.js"></script>' );
     // seajs.config({...})
     seajs.config({
         alias: {
             "jquery" "jquery-1.11.0.min" ,
             "easyui" "/js/easyui/jquery.easyui.min" ,
             "easyui-zh" "/js/easyui/locale/easyui-lang-zh_CN" ,
             "loading" "jquery.loading.js"
         },
         preload: [
             "jquery"
         ]
     });
     // 使用seajs加载共享模块
     var  loadCommons =  function () {
         seasj.use( "jquery" );
         seasj.use( "easyui" );
         seasj.use( "easyui-zh" );
         seasj.use( "loading" );
     };
     loadCommons();
     // 加载页面对应的脚本
     var  autoload =  function () {
         // TODO 加载页面对应的脚本
     };
     autoload();
)();

发现问题

现在问题出现了

  1. seajs.config会报错 seasj is not defined

  2. 绕过第1个问题之后,loadCommons 也会出问题,会报告 jQuery is not defined

  3. autoload的实现得想个方便又易于实现的办法

  4. 嵌入页面的脚本该如何加载

分析原因,在document.write之后,浏览器开始加载sea.js,但是脚本没等浏览器加载sea.js完成,就开始执行 seajs.config(),所以出现 seajs 未定义的错误。同理,seajs.use 同时加载几个模块,压根儿没顾及它们之间的依赖关系(本来也没法定义依赖关系)


解决等待加载seajs的问题

对于第1个问题,上面说到绕过,其实就是通过 setTimeout 来延时加载绕过的。修改后的代码结构如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// @(#) entry.js
( function () {
     var  config = {
         // 这里是seajs.config中的定义
         // 为了方便修改配置,把这个配置对象定义提前
         // 内容略
     };
     // 通过document.write加载sea.js
     document.write( '<script type="text/javascript" src="/js/modules/sea.js"></script>' );
     // 使用seajs加载共享模块
     var  loadCommons =  function () {
         // 略
     };
     // 加载页面对应的脚本
     var  autoload =  function () {
         // TODO 加载页面对应的脚本
     };
     // 定义 main 函数等待 seajs 加载完成,
     // 简单的通过判断是否存在 seajs 对象来判断其是否加载完成
     var  main =  function  () {
         // 如果没有加载完成,等待50毫秒再试
         if  ( typeof  (seajs) ===  "undefined" ) {
             setTimeout(main, 50);
             return ;
         }
         seajs.config(config);
         loadCommons();     // 现在这个函数内部还有问题
         autoload()         // 现在这里加载autoload也有问题
     };
     main();
)();



解决脚本按依赖加载的问题

下面来解决脚本依赖的问题,分析依赖关系

  • easyui-zh依赖easyui

  • easyui和loading依赖jquery

  • autuload()依赖loadCommons()

一开始想通过seajs的define来解决这个问题,但尝试了下面两种办法都不行,

 
  
1
2
3
4
5
6
7
8
// 方法一,不行,因为几个require会同步加载,一样无序
define( function (require) {
     require( "jquery" );
     require( "easyui" );
     require( "easyui-zh" );
     require( "loading" );
     autuload();
});


1
2
// 方法二,也不行,因为依赖的几个模块也是同步加载,无序
define( "autoload" , [ "jquery" "easyui" "easyui-zh" "loading" ], autload);


最后想到用seajs.use来解决这个问题,虽然代码看起来有点难过


1
2
3
4
5
seajs.use( "jquery" function () {
     seajs.use( "easyui" function () {
         seajs.use([ "easyui-zh" "loading" ], autoload);
     });
});


现在这都算好的,如果依赖树过长的话,这个代码看来肯定会想死的心都有了。如果能把这个调用平面化就好了……于是继续想办法,终于想到了用队列方式来处理,于是定义了这么个东西


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 通过链式调用按顺序加载依赖的js库(使用seajs.use)
    var  useQueue =  function  () {
        return  ( function  () {
            var  queue = [];
            return  {
                add:  function  (p) {
                    // 按顺序添加一个模块(名称)
                    queue.push(p);
                    return  this ;
                },
                addRange:  function  (a) {
                    // 按顺序并入(追加)一个模块(名称)列表
                    queue = queue.concat(a);
                    return  this ;
                },
                run:  function  (callback) {
                    // 使用递归的方式,从队列中依次加载模块
                    // 幸好javascript的function是对象
                    var  go =  function  () {
                        if  (queue.length == 0) {
                            callback();
                        else  {
                            var  p = queue.shift();
                            seajs.use(p, go);
                        }
                    };
                    go();
                }
            };
        })();
    };


这个方法的关键在 run() 方法里,这里定义了一个加载调用函数 go(),它每次从队列中取出一个元素给 seajs.use() 加载,而 seajs.use() 加载完成之后会递归调用 go() 继续处理队列后面的元素。


代码整体搞复杂了,但是加载这里简单了(这算不算强迫症)


1
2
3
4
5
6
7
useQueue().add( "jquery" )
         .add( "easyui" )
         .add( "easyui-zh" )
         .add( "loading" )
         .run( function  () {
     autoload();
});


考虑了詥队列可能会经常修改,把依赖队列配置到 config 对象中,


1
2
3
4
5
6
7
8
9
10
11
12
13
var  config {
     // 上面是seajs.config所需要的配置
     dependQueue: [
         "jquery" ,
         "easyui" ,
         "easyui-zh" ,
         [
             "loading"
             // 这里用数组是为了后面可能添加与loading平级的其它脚本
             // 反正 seajs.use 的第1个参数可以是单个模块名,也可以是一个模块名的数组
         ]
     ]
}


这是后面的加载部分


1
2
3
useQueue().addRange(config.dependQueue).run( function () {
     autoLoad();
});



 
  

实现autoload

autoload() 中需要检查当前页面的配置,是否需要加载页面对应的脚本文件,如果要加载,是指定路径还是自动计算路径?


考虑到页面在加载entry.js的时候就需要做好这些决定,而这个配置只能是在页面中,不能写在 entry.js 中,还要考虑不给页面增加负担,一般能想到的办法是在<body> 标签中加个属性来配置,而更好的办法,是加在引入 entry.js 的 <script> 标签中。


1
< script  type = "text/javascript"  src = "/js/modules/entry.js"  id = "js-entry"  data-autoload = "" ></ script >


从这个 <script> 标签我们很容易得到需要的信息——只需要解析 data-autoload 属性就行了,这件事用jquery来干就是一句话的事情。为了更快的定位到这个 <script>,可以约定为它加一个ID:js-entry


分析 data-autoload 的逻辑是这样:

  1. 如果有定义data-autoload,但没有属性值,或属性值为空,表示自动计算脚本路径

  2. 如果有定义data-autoload,且有值,则其值是需要加载的页面业务脚本路径

  3. 如果没有定义data-autoload,表示不加载页面业务脚本

代码实现


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var  autoLoad =  function  () {
     var  script = $( "#js-entry" );
     if  (script.length == 0) {
         // 这里简单处理了,没有定义js-entry,就不自动加载
         // 如果不怕麻烦,这种情况下还可以去分析得到当前script标签
         return ;
     }
     var  url = script.data( "autoload" );
     if  ( typeof  url !==  "string" ) {
         // 没有定义data-autoload
         return ;
     }
     if  (!url) {
         // data-autoload没有属性值,或属性值为空字符串
         // 根据当前页面URL分析计算得到脚本的URL
         var  subPath = window.location.href.replace(/^.*\/\/.+\ //, "/");
         subPath = subPath.replace(/\.[^.]+$/,  "" );
         url =  "/js/pages"  + subPath;
     }
     seajs.use(url);
};




加载嵌入页面的脚本

页面需要的脚本很少的时候,完全没有必要为页面去创建一个脚本文件,这时候需要在页面内嵌入业务脚本代码。加载嵌入脚本大概应该是这样


1
2
3
4
< script  type = "text/javascript"  src = "/js/modules/entry.js"  id = "js-entry"  data-autoload = "" ></ script >
< script  type = "text/javascript" >
     // TODO 这里是页面嵌入脚本
</ script >


在执行到页面嵌入脚本的时候,entry.js 有可能还在加载依赖库,甚至有可能还在加载 sea.js。那么这个时候嵌入脚本就不能很好的运行。怎么办?


执行嵌入脚本的时候唯一明确的是 entry.js 中 setTimeout 之外的代码已经执行完了。那么这里可以定义一个函数,通过回调的方式来执行页面嵌入脚本。就像这样


1
2
3
4
5
6
7
8
9
10
// entry.js
$( function () {
     // .....
     var  callback;
     var  page =  function (fun) {
         callback = fun;
     };
     // .....
     window.page = page;
});


1
2
3
4
5
6
7
<!-- page.html -->
< script  type = "text/javascript"  src = "/js/modules/entry.js"  id = "js-entry"  data-autoload = "" ></ script >
< script  type = "text/javascript" >
     page(function() {
         // TODO 这里写页面嵌入代码
     }
</ script >


剩下的问题就是,如何保证 callback() 在所有依赖脚本加载完成之后调用。也许这样可以这样


1
2
3
4
5
6
useQueue().addRange(config.dependQueue).run( function () {
     autoLoad();
     if  ( typeof  callback ==  "function" ) {
         callback();
     }
});


但实际上这样并不保险……因为,万一,entry.js加载脚本的速度嗖嗖的,就有可能出现调用 callback 的时候,页面上的 page() 还没执行,也就是说,callback 还是 undefined


如果能用 jQuery.Deferred 来处理,这个事情就好办了,问题是有页面上调用 page() 的时候 jQuery 有可能还没加载出来……只好简单的自己实现一个了


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var  page = ( function  () {
     var  isReady =  false ;
     var  func;
     return  {
         define:  function  (callback) {
             func = callback;
             if  (isReady) {
                 func.call( this );
             }
         },
         run:  function  () {
             isReady =  true ;
             if  ( typeof  func ===  "function" ) {
                 func.call( this );
             }
         }
     };
})();
// ......
var  main =  function  () {
     // .....
     useQueue().addRange(config.dependQueue).run( function  () {
         autoLoad();
         page.run();
     });
};


相应的页面代码要改改


1
2
3
4
5
< script  type = "text/javascript" >
     page.define(function() {
         // TODO 这里写页面脚本
     });
</ script >






完整的entry.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
  * Entry javascript for pages
  * http://jamesfanc.blog.51cto.com/
  *
  * @requires [seajs](http://seajs.org/)
  * @requires [jquery](http://jquery.com/)
  * @author [James Fancy](mailto:jamesfancy@126.com)
  *
  * Copyright 2014 James Fancy
  */
( function  () {
     var  config = {
         alias: {
             "jquery" "jquery-1.11.0.min" ,
             "easyui" "/js/easyui/jquery.easyui.min" ,
             "easyui-zh" "/js/easyui/locale/easyui-lang-zh_CN" ,
             "loading" "jquery.loading.js"
         },
         preload: [
             "jquery"
         ],
         dependQueue: [
             "jquery" ,
             "easyui" ,
             "easyui-zh" ,
             [
                 "loading"
             ]
         ]
     };
     if  ( typeof  (seajs) ===  "undefined" ) {
         document.write( '<script type="text/javascript" src="/js/modules/sea.js"></script>\n' );
     }
     // 定义page对象,
     // 可以使用page.define(callback)来定义页面脚本
     // callback会在依赖项加载完成之后调用
     var  page = ( function  () {
         var  isReady =  false ;
         var  func;
         return  {
             define:  function  (callback) {
                 func = callback;
                 if  (isReady) {
                     func.call( this );
                 }
             },
             run:  function  () {
                 isReady =  true ;
                 if  ( typeof  func ===  "function" ) {
                     func.call( this );
                 }
             }
         };
     })();
     var  autoLoad =  function  () {
         var  script = $( "#js-entry" );
         if  (script.length == 0) {
             return ;
         }
         var  url = script.data( "autoload" );
         if  ( typeof  url !==  "string" ) {
             return ;
         }
         if  (!url) {
             var  subPath = window.location.href.replace(/^.*\/\/.+\ //, "/");
             subPath = subPath.replace(/\.[^.]+$/,  "" );
             url =  "/js/pages"  + subPath;
         }
         seajs.use(url);
     };
     // 通过链式调用按顺序加载依赖的js库(使用seajs.use)
     var  useQueue =  function  () {
         return  ( function  () {
             var  queue = [];
             return  {
                 addRange:  function  (a) {
                     queue = queue.concat(a);
                     return  this ;
                 },
                 add:  function  (p) {
                     queue.push(p);
                     return  this ;
                 },
                 run:  function  (callback) {
                     var  go =  function  () {
                         if  (queue.length == 0) {
                             callback();
                         else  {
                             var  p = queue.shift();
                             seajs.use(p, go);
                         }
                     };
                     go();
                 }
             };
         })();
     };
     var  main =  function  () {
         if  ( typeof  (seajs) ===  "undefined" ) {
             setTimeout(main, 50);
             return ;
         }
         seajs.config(config);
         useQueue().addRange(config.dependQueue).run( function  () {
             autoLoad();
             page.run();
         });
     };
     main();
     window.page = page;
})();

本文转自边城__ 51CTO博客,原文链接:http://blog.51cto.com/jamesfancy/1381765,如需转载请自行联系原作者