目录
思路
发现问题
解决等待加载seajs的问题
解决脚本按依赖加载的问题
实现autoload
加载嵌入页面的脚本
完整的entry.js
项目第二版本开始开发的时候,我觉得应该对脚本干点什么。因为之前的脚本引用太多,太杂,且不说引用的第三方脚本,也不说每个页面自己的业务脚本,就项目组自己写的,项目的公共脚本都有十余个,不好好管理真是不行。那么很自然的,是RequireJS,SeaJS之类的加载工具。本文不纠结如何选择,反正最终选择了SeaJS。
其实引入seajs,除了想把脚本模块化之外,还有一个很重要的目的,就是希望在每个页面都只需要引入很少,甚至1个脚本,就能根据需要引入其它的脚本,不用再在一个页面文件中写大量的 <script ...></script>
标签了。
记得之前seajs可以在引入的时候自动加载config,然后再根据标签中的一个 data-xxx
配置项加载页面的入口脚本,不过我在 seajs 2 的文档中没发现有这样的用法,可能是由于某些原因取消了吧。所以我准备自己写一个脚本,这个脚本要干这么些事情:
思路
根据这个思路,entry.js
大概应该这样写
// @(#) 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(); )();
发现问题
现在问题出现了
seajs.config会报错
seasj is not defined
绕过第1个问题之后,
loadCommons
也会出问题,会报告jQuery is not defined
autoload的实现得想个方便又易于实现的办法
嵌入页面的脚本该如何加载
分析原因,在document.write之后,浏览器开始加载sea.js,但是脚本没等浏览器加载sea.js完成,就开始执行 seajs.config(),所以出现 seajs 未定义的错误。同理,seajs.use 同时加载几个模块,压根儿没顾及它们之间的依赖关系(本来也没法定义依赖关系)
解决等待加载seajs的问题
对于第1个问题,上面说到绕过,其实就是通过 setTimeout
来延时加载绕过的。修改后的代码结构如下:
// @(#) 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来解决这个问题,但尝试了下面两种办法都不行,
// 方法一,不行,因为几个require会同步加载,一样无序 define(function(require) { require("jquery"); require("easyui"); require("easyui-zh"); require("loading"); autuload(); });
// 方法二,也不行,因为依赖的几个模块也是同步加载,无序 define("autoload",["jquery","easyui","easyui-zh","loading"],autload);
最后想到用seajs.use来解决这个问题,虽然代码看起来有点难过
seajs.use("jquery",function() { seajs.use("easyui",function() { seajs.use(["easyui-zh",autoload); }); });
现在这都算好的,如果依赖树过长的话,这个代码看来肯定会想死的心都有了。如果能把这个调用平面化就好了……于是继续想办法,终于想到了用队列方式来处理,于是定义了这么个东西
// 通过链式调用按顺序加载依赖的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()
继续处理队列后面的元素。
代码整体搞复杂了,但是加载这里简单了(这算不算强迫症)
useQueue().add("jquery") .add("easyui") .add("easyui-zh") .add("loading") .run(function () { autoload(); });
考虑了�队列可能会经常修改,把依赖队列配置到 config
对象中,
var config { // 上面是seajs.config所需要的配置 dependQueue: [ "jquery",[ "loading" // 这里用数组是为了后面可能添加与loading平级的其它脚本 // 反正 seajs.use 的第1个参数可以是单个模块名,也可以是一个模块名的数组 ] ] }
这是后面的加载部分
useQueue().addRange(config.dependQueue).run(function() { autoLoad(); });
实现autoload
autoload()
中需要检查当前页面的配置,是否需要加载页面对应的脚本文件,如果要加载,是指定路径还是自动计算路径?
考虑到页面在加载entry.js的时候就需要做好这些决定,而这个配置只能是在页面中,不能写在 entry.js
中,还要考虑不给页面增加负担,一般能想到的办法是在<body>
标签中加个属性来配置,而更好的办法,是加在引入 entry.js
的 <script>
标签中。
<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
的逻辑是这样:
代码实现
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); };
加载嵌入页面的脚本
页面需要的脚本很少的时候,完全没有必要为页面去创建一个脚本文件,这时候需要在页面内嵌入业务脚本代码。加载嵌入脚本大概应该是这样
<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
之外的代码已经执行完了。那么这里可以定义一个函数,通过回调的方式来执行页面嵌入脚本。就像这样
// entry.js $(function() { // ..... var callback; var page = function(fun) { callback = fun; }; // ..... window.page = page; });
<!-- 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()
在所有依赖脚本加载完成之后调用。也许这样可以这样
useQueue().addRange(config.dependQueue).run(function() { autoLoad(); if (typeof callback == "function") { callback(); } });
但实际上这样并不保险……因为,万一,entry.js加载脚本的速度嗖嗖的,就有可能出现调用 callback
的时候,页面上的 page()
还没执行,也就是说,callback
还是 undefined
。
如果能用 jQuery.Deferred
来处理,这个事情就好办了,问题是有页面上调用 page()
的时候 jQuery 有可能还没加载出来……只好简单的自己实现一个了
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(); }); };
<script type="text/javascript"> page.define(function() { // TODO 这里写页面脚本 }); </script>
完整的entry.js
/** * 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",preload: [ "jquery" ],dependQueue: [ "jquery",[ "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; })();