how-to-load-dynamic-script

by letiantian

The right way to load javascript files dynamically.

209 Stars 28 Forks Last release: Not found 13 Commits 0 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

动态加载js文件的正确姿势

说明:

这个repository的结构:
*

img/
:图片 *
LABjs-source/
LABjs的源码,带注释,文中部分代码参考了该项目。 *
lazyload-source
lazyload的源码,带注释,文中部分代码参考了该项目。 *
src/
:本文档中涉及的代码,在Firefox 42中测试,使用Firebug观察和调试。 *
README.md
:本文档。

本文中给出了多种解决方式,

方式1
对应的代码是
src/js/loader01.js
src/index01.js
,其他方式对应的代码位置类似。

Gif图片使用LICEcap生成。

目录:

硬编码在html源码中的script是如何加载的 || 从一个例子出发 || 方式1:一个错误的加载方式 || 方式2 || 方式3 || 方式4 || 方式5 || 方式6 Promise 串行 || 方式7 Promise 并行 || 方式8 Generator Promise || 现有哪些工具可以实现动态加载 || 其他 || 资料


最近在做一个为网页生成目录的工具awesome-toc,该工具提供了以jquery插件的形式使用的代码,也提供了一个基于Bookmarklet(小书签)的浏览器插件。

小书签需要向网页中注入多个js文件,也就相当于动态加载js文件。在编写这部分代码时候遇到坑了,于是深究了一段时间。

我在这里整理了动态加载js文件的若干思路,这对于理解异步编程很有用处,而且也适用于Nodejs

硬编码在html源码中的script是如何加载的

如果html中有:

html


那么,浏览器解析到
html

会停止渲染页面,去拉取
1.js
(IO操作),等到
1.js
的内容获取到后执行。 1.js执行完毕后,浏览器解析到
html

进行和
1.js
类似的操作。

不过现在部分浏览器支持async属性和defer属性,这个可以参考:

async vs defer attributes
script的defer和async

script -MDN指出:async对内联脚本(inline script)没有影响,defer的话因浏览器以及版本不同而影响不同。

从一个例子出发

举个实际的例子:

<div id="container">
    <div id="header"></div>
    <div id="body">
        <button id="only-button"> hello world</button>
    </div>
    <div id="footer"></div>
</div>

<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js" type="text/javascript"></script>
<script src="./your.js" type="text/javascript"></script>
<script src="./my.js" type="text/javascript"></script>

js/your.js: ```js console.log('your.js: time='+Date.parse(new Date()));

function myAlert(msg) { console.log('alert at ' + Date.parse(new Date())); alert(msg); }

function myLog(msg) { console.log(msg); } ```

js/my.js:

js
myLog('my.js: time='+Date.parse(new Date()));
$('#only-button').click(function() {
    myAlert("hello world");
});

可以看出

jquery
js/your.js
js/my.js
三者的关系如下:
  • js/my.js
    依赖于
    jquery
    js/your.js
  • jquery
    js/your.js
    之间没有依赖关系。

浏览器打开

index00.html
,等待js加载完毕,点击按钮
hello world
将会触发
alert("hello world");

firbug控制台输出:

下面开始探索如何动态加载js文件。

方式1:一个错误的加载方式

文件js/loader01.js内容如下: ```js Loader = (function() {

var loadScript = function(url) { var script = document.createElement( 'script' ); script.setAttribute( 'src', url+'?'+'time='+Date.parse(new Date())); // 不用缓存 document.body.appendChild( script ); };

var loadMultiScript = function(urlarray) { for (var idx=0; idx < urlarray.length; idx++) { loadScript(url_array[idx]); } }

return { load: loadMultiScript, };

})(); // end Loader ```

index01.html内容如下: ```html

hello world


<p>浏览器打开</p>
<pre>index01.html</pre>,点击按钮<pre>hello world</pre>,会发现什么都没发生。打开firebug,进入控制台,可以看到这样的错误:

<p><img src="https://github.com/letiantian/how-to-load-dynamic-script/raw/master/./img/method01.gif" alt=""></p>

<p>很明显,</p>
<pre>my.js</pre>没等jquery就先执行了。又由于存在依赖关系,脚本的执行出现了错误。这不是我想要的。

<p>在网上可以找到关于动态加载的一些说明,例如:</p>

<blockquote>
<p>Opera/Firefox(老版本)下:脚本执行的顺序与节点被插入页面的顺序一致</p>

<p>IE/Safari/Chrome下:执行顺序无法得到保证</p>

<p>注意:</p>

<p>新版本的Firefox下,脚本执行的顺序与插入页面的顺序不一定一致,但可通过将script标签的async属性设置为false来保证顺序执行
  老版本的Chrome下,脚本执行的顺序与插入页面的顺序不一定一致,但可通过将script标签的async属性设置为false来保证顺序执行</p>
</blockquote>

<p>真够乱的!!(这段描述来自:<a href="http://www.cnblogs.com/chyingp/archive/2012/10/17/2726898.html">LABJS源码浅析</a>。)</p>

<p>为了解决我们遇到的问题,我们可以在loadScript函数中修改script对象async的值:
</p>
<pre>js
var loadScript = function(url) {
  var script = document.createElement('script');
  script.async = false;  // 这里
  script.setAttribute('src', url+'?'+'time='+Date.parse(new Date())); 
  document.body.appendChild(script);
};
</pre>

<p>浏览器打开,发现可以正常执行!可惜该方法只在某些浏览器的某些版本中有效,没有通用性。<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#Browser_compatibility">script browser compatibility</a>给出了下面的兼容性列表:</p>

<p><img src="https://github.com/letiantian/how-to-load-dynamic-script/raw/master/./img/async-support.png" alt=""></p>

<p>下面探索的方法都可以正确的加载和执行多个脚本,不过有些同样有兼容性问题(例如Pormise方式)。</p>

<h2>方式2</h2>

<p>可以认为绝大部分浏览器动态加载脚本的方式如下:</p>

<ol>
<li>动态加载多个脚本时,这些脚本的加载(IO操作)可能并行,可能串行。<br>
</li>
<li>一个脚本一旦加载完毕(IO结束),该脚本放入“待执行队列”,等待出队供js引擎去执行。 </li>
</ol>

<p>所以我们的示例中的三个js脚本的加载和执行顺序可以是下面的情况之一:</p>

<ol>
<li>
<pre>jquery</pre>加载并执行,<pre>js/your.js</pre>加载并执行,<pre>js/my.js</pre>加载并执行。</li>
<li>和情况1类似,不过<pre>js/your.js</pre>在前,<pre>jquery</pre>在后。</li>
<li>
<pre>jquery</pre>和<pre>js/your.js</pre>并行加载,按照加载完毕的顺序来执行;等<pre>jquery</pre>和<pre>js/your.js</pre>都执行完毕后,加载并执行<pre>js/my.js</pre>。</li>
</ol>

<p>其中,“加载完毕”这是一个事件,浏览器的支持监测这个事件。这个事件在IE下是</p>
<pre>onreadystatechange</pre>,其他浏览器下是<pre>onload</pre>。

<p>据此,<a href="https://www.nczonline.net/blog/2009/06/23/loading-javascript-without-blocking/">Loading JavaScript without blocking</a>给出了下面的代码:</p>
<pre class="language-js">function loadScript(url, callback){

    var script = document.createElement("script")
    script.type = "text/javascript";

    if (script.readyState){  //IE
        script.onreadystatechange = function(){
            if (script.readyState == "loaded" ||
                    script.readyState == "complete"){
                script.onreadystatechange = null;
                callback();
            }
        };
    } else {  //Others
        script.onload = function(){
            callback();
        };
    }

    script.src = url;
    document.body.appendChild(script);
}
</pre>
<p>callback函数可以是去加载另外一个js,不过如果要加载的js文件较多,就成了“回调地狱”(callback hell)。</p>

<p>回调地狱式可以通过一些模式来解决,例如下面给出的方式2:</p>
<pre class="language-js">Loader = (function() {

  var load_cursor = 0;
  var load_queue;

  var loadFinished = function() {
    load_cursor ++;
    if (load_cursor &lt; load_queue.length) {
      loadScript();
    }
  }

  function loadError (oError) {
    console.error("The script " + oError.target.src + " is not accessible.");
  }


  var loadScript = function() {
    var url = load_queue[load_cursor];
    var script = document.createElement('script');
    script.type = "text/javascript";

    if (script.readyState){  //IE
        script.onreadystatechange = function(){
            if (script.readyState == "loaded" ||
                    script.readyState == "complete"){
                script.onreadystatechange = null;
                loadFinished();
            }
        };
    } else {  //Others
        script.onload = function(){
            loadFinished();
        };
    }

    script.onerror = loadError;

    script.src = url+'?'+'time='+Date.parse(new Date());
    document.body.appendChild(script);
  };

  var loadMultiScript = function(url_array) {
    load_cursor = 0;
    load_queue = url_array;
    loadScript();
  }

  return {
    load: loadMultiScript,
  };

})();  // end Loader

//loading ...
Loader.load([
            'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js', 
            './js/your.js',
            './js/my.js'
             ]);
</pre>
<p></p>
<pre>load_queue</pre>是一个队列,保存需要依次加载的js的url。当一个js加载完毕后,<pre>load_cursor++</pre>用来模拟出队操作,然后加载下一个脚本。

<p>onerror事件也添加了回调,用来处理无法加载的js文件。当遇到无法加载的js文件时停止加载,剩下的文件也不会加载了。</p>

<p>效果如下:</p>

<p><img src="https://github.com/letiantian/how-to-load-dynamic-script/raw/master/./img/method02.gif" alt=""></p>

<h2>方式3</h2>

<p>方式2是串行的去加载,我们稍加改进,让可以并行加载的js脚本尽可能地并行加载。</p>
<pre class="language-js">Loader = (function() {

  var group_queue;      // group list
  var group_cursor = 0; // current group cursor
  var current_group_finished = 0;  


  var loadFinished = function() {
    current_group_finished ++;
    if (current_group_finished == group_queue[group_cursor].length) {
      next_group();
      loadGroup();
    }
  };

  var next_group = function() {
    current_group_finished = 0;
    group_cursor ++;
  };

  var loadError = function(oError) {
    console.error("The script " + oError.target.src + " is not accessible.");
  };

  var loadScript = function(url) {
    console.log("load "+url);
    var script = document.createElement('script');
    script.type = "text/javascript";

    if (script.readyState){  //IE
        script.onreadystatechange = function() {
            if (script.readyState == "loaded" ||
                    script.readyState == "complete") {
                script.onreadystatechange = null;
                loadFinished();
            }
        };
    } else {  //Others
        script.onload = function(){
            loadFinished();
        };
    }

    script.onerror = loadError;

    script.src = url+'?'+'time='+Date.parse(new Date());
    document.body.appendChild(script);
  };

  var loadGroup = function() {
    if (group_cursor &gt;= group_queue.length) 
      return;
    current_group_finished = 0;
    for (var idx=0; idx &lt; group_queue[group_cursor].length; idx++) {
      loadScript(group_queue[group_cursor][idx]);
    }
  };

  var loadMultiGroup = function(url_groups) {
    group_cursor = 0;
    group_queue = url_groups;
    loadGroup();
  }

  return {
    load: loadMultiGroup,
  };

})();  // end Loader


//loading
var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
    your   = './js/your.js',
    my     = './js/my.js'
;
Loader.load([ [jquery, your], [my] ]);
</pre>
<p></p>
<pre>Loader.load([ [jquery, your], [my] ]);</pre>代表着<pre>jquery</pre>和<pre>js/your.js</pre>先尽可能快地加载和执行,等它们执行结束后,加载并执行<pre>./js/my.js</pre>。

<p>这里将每个子数组里的所有url看成一个group,group内部的脚本尽可能并行加载并执行,group之间则为串行。</p>

<p>这段代码里使用了一个计数器</p>
<pre>current_group_finished</pre>记录当前group中完成的url的数量,在这个数量和url的总数一致时,进入下一个group。

<p>效果如下:<br>
<img src="https://github.com/letiantian/how-to-load-dynamic-script/raw/master/./img/method03.gif" alt=""></p>

<h2>方式4</h2>

<p>该方式是对方式3中代码的重构。</p>
<pre class="language-js">Loader = (function() {

  var group_queue = [];      // group list
  var current_group_finished = 0;  
  var finish_callback;
  var finish_context;

  var loadFinished = function() {
    current_group_finished ++;
    if (current_group_finished == group_queue[0].length) {
      next_group();
      loadGroup();
    }
  };

  var next_group = function() {
    group_queue.shift();
  };

  var loadError = function(oError) {
    console.error("The script " + oError.target.src + " is not accessible.");
  };

  var loadScript = function(url) {
    console.log("load "+url);
    var script = document.createElement('script');
    script.type = "text/javascript";

    if (script.readyState){  //IE
        script.onreadystatechange = function() {
            if (script.readyState == "loaded" ||
                    script.readyState == "complete") {
                script.onreadystatechange = null;
                loadFinished();
            }
        };
    } else {  //Others
        script.onload = function(){
            loadFinished();
        };
    }

    script.onerror = loadError;

    script.src = url+'?'+'time='+Date.parse(new Date());
    document.body.appendChild(script);
  };

  var loadGroup = function() {
    if (group_queue.length == 0) {
      finish_callback.call(finish_context);
      return;
    }
    current_group_finished = 0; 
    for (var idx=0; idx &lt; group_queue[0].length; idx++) {
      loadScript(group_queue[0][idx]);
    }
  };

  var addGroup = function(url_array) {
    if (url_array.length &gt; 0) {
      group_queue.push(url_array);
    }
  };

  var fire = function(callback, context) {
    finish_callback = callback || function() {};
    finish_context = context || {};
    loadGroup();
  };

  var instanceAPI = {
    load : function() {
      addGroup([].slice.call(arguments));
      return instanceAPI;
    },

    done : fire,
  };

  return instanceAPI;

})();  // end Loader


//loading
var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
    your   = './js/your.js',
    my     = './js/my.js'
;
// Loader.load(jquery, your).load(my).done();
Loader.load(jquery, your)
      .load(my)
      .done(function(){console.log(this.msg)}, {msg: 'finished'});
</pre>
<p>在调用多次load()函数后,必须调用done()函数。done()函数用来触发所有脚本的load。</p>

<h2>方式5</h2>

<p>这个方式是对方式4的重写。改进为调用load()时候尽可能去触发实际的load操作。</p>
<pre class="language-js">// 这里调试用的代码我没有删除

Loader = (function() {

    var group_queue  = [];      // group list

    //// url_item = {url:str, start: false, finished:false}

    // 用于调试
    var log = function(msg) {
        return;
        console.log(msg);
    }

    var isFunc = function(obj) { 
        return Object.prototype.toString.call(obj) == "[object Function]"; 
    }

    var isArray = function(obj) { 
        return Object.prototype.toString.call(obj) == "[object Array]"; 
    }

    var isAllStart = function(url_items) {
        for (var idx=0; idx<url_items.length if false return true var isanystart="function(url_items)" for idx="0;" isallfinished="function(url_items)" isanyfinished="function(url_items)" loadfinished="function()" nextgroup showgroupinfo="function()" group="group_queue[idx];" log i="0;" isallstart while> 0) {
            showGroupInfo();
            // is Func
            if (isFunc(group_queue[0])) {
                log('## nextGroup: exec func');
                group_queue[0]();  // exec
                group_queue.shift();
                continue;
            // is Array
            } else if (isAllFinished(group_queue[0])) {   
                log('## current group all finished');
                group_queue.shift();
                continue;
            } else if (!isAnyStart(group_queue[0])) {
                log('## current group no one start!');
                loadGroup();
                break;
            } else {
                break;
            }
        }
    };

    var loadError = function(oError) {
        console.error("The script " + oError.target.src + " is not accessible.");
    };

    var loadScript = function(url_item) {
        log("load "+url_item.url);
        url = url_item.url;
        url_item.start = true;
        var script = document.createElement('script');
        script.type = "text/javascript";

        if (script.readyState){  //IE
            script.onreadystatechange = function() {
                if (script.readyState == "loaded" ||
                        script.readyState == "complete") {
                    script.onreadystatechange = null;
                    url_item.finished = true;
                    loadFinished();
                }
            };
        } else {  //Others
            script.onload = function(){
                url_item.finished = true;
                loadFinished();
            };
        }

        script.onerror = loadError;

        script.src = url+'?'+'time='+Date.parse(new Date());
        document.body.appendChild(script);
    };

    var loadGroup = function() {
        for (var idx=0; idx &lt; group_queue[0].length; idx++) {
            loadScript(group_queue[0][idx]);
        }
    };

    var addGroup = function(url_array) {
        log('add :' + url_array);
        if (url_array.length &gt; 0) {
            group = [];
            for (var idx=0; idx<url_array.length idx url_item="{" url: url_array start: false finished: group.push group_queue.push nextgroup var addfunc="function(callback)" callback isfunc log instanceapi="{" load : function addgroup return wait end loader loading jquery="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js" your="./js/your.js" my="./js/my.js" loader.load .wait and your.js were loaded .load my.js was>
<p>上面的调用中,每次load时候会尝试马上加载和执行这些脚本,而不是像方式4那样要等done()被调用。</p>

<p>另外出现了新的函数wait,当wait之前的load和wait执行结束后,该wait中的匿名函数会被调用。</p>

<p>效果如下:<br>
<img src="https://github.com/letiantian/how-to-load-dynamic-script/raw/master/./img/method05.gif" alt=""></p>

<h2>方式6 Promise+串行</h2>

<p>Promise是一种设计模式。关于Promise,下面的几篇文章值得一看:</p>

<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise - MDN</a></li>
<li><a href="http://www.html5rocks.com/zh/tutorials/es6/promises/">JavaScript Promises</a></li>
<li><a href="http://liubin.github.io/promises-book/">JavaScript Promise迷你书(中文版)</a></li>
<li><a href="https://www.promisejs.org/implementing/">An Implemention of Promise</a></li>
</ul>

<p>当前浏览器对Promise的支持情况如下:</p>

<p><img src="https://github.com/letiantian/how-to-load-dynamic-script/raw/master/./img/promise-support.png" alt=""></p>

<p>使用Promise解决脚本动态加载问题的方案如下:</p>
<pre class="language-js">function getJS(url) {
    return new Promise(function(resolve, reject) {
        var script = document.createElement('script');
        script.type = "text/javascript";

        if (script.readyState){  //IE
            script.onreadystatechange = function() {
                if (script.readyState == "loaded" ||
                        script.readyState == "complete") {
                    script.onreadystatechange = null;
                    resolve('success: '+url);
                }
            };
        } else {  //Others
            script.onload = function(){
                resolve('success: '+url);
            };
        }

        script.onerror = function() {
            reject(Error(url + 'load error!'));
        };

        script.src = url+'?'+'time='+Date.parse(new Date());
        document.body.appendChild(script);

    });
}

//loading
var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
    your   = './js/your.js',
    my     = './js/my.js'
;

getJS(jquery).then(function(msg){
    return getJS(your);
}).then(function(msg){
    return getJS(my);
}).then(function(msg){
    console.log(msg);
});
</pre>
<p>这个实现中js是串行加载的。</p>

<p>效果如下:<br>
<img src="https://github.com/letiantian/how-to-load-dynamic-script/raw/master/./img/method06.gif" alt=""></p>

<h2>方式7 Promise+并行</h2>

<p>可以使用<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all">Promise.all</a>使</p>
<pre>jquery</pre>和<pre>js/your.js</pre>并行加载。
<pre class="language-js">Promise.all([getJS(jquery), getJS(your)]).then(function(results){
    return getJS(my);
}).then(function(msg){
    console.log(msg);
});
</pre>
<p><img src="https://github.com/letiantian/how-to-load-dynamic-script/raw/master/./img/method07.gif" alt=""></p>

<h2>方式8 Generator+Promise</h2>

<p>Promise配合生成器(Generator)可以让js程序按照串行的思维编写。</p>

<p>关于生成器,下面的几篇文章值得一看:<br>
* <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/function*">function* - MDN</a>
* <a href="https://davidwalsh.name/es6-generators">The Basics Of ES6 Generators</a>  </p>

<p>浏览器的支持情况如下:<br>
<img src="https://github.com/letiantian/how-to-load-dynamic-script/raw/master/./img/generator-support.png" alt=""></p>

<p>来两个典型的生成器示例:  </p>

<p>示例1:<br>

function *addGenerator() { var i = 0; while (true) { i += yield i; } }

var adder = addGenerator(); console.log( adder.next().value ); // yield i时候暂停 (循环1) console.log( adder.next(5).value ); // 循环1中yield i的结果为5,i+=5,进入下一个循环(循环2),循环2中yield i 暂停,返回5 console.log( adder.next(5).value ); // 循环2中yield i的结果为5 console.log( adder.next(5).value ); // 循环3中yield i的结果为5 console.log( adder.next(50).value ); //循环4中yield i的结果为50,i+=50,进入循环6 ```

输出:

plain
0
5
10
15
65

示例2: ```plain function* idMaker(){ var index = 0; while(index < 3) yield index++; }

var gen = idMaker();

while ( result = gen.next() ) { if (!result.done) { console.log(result.done + ':' + result.value); } else{ console.log(result.done + ':' + result.value); break; } }

输出:
false:0 false:1 false:2 true:undefined ```

下面的文章介绍了如何搭配Promise和Generator:
* JavaScript Promises 的最后一节 * Generators with Promise

Generator+Promise实现js脚本动态加载的方式如下:

function getJS(url) {
    return new Promise(function(resolve, reject) {
        var script = document.createElement('script');
        script.type = "text/javascript";

    if (script.readyState){  //IE
        script.onreadystatechange = function() {
            if (script.readyState == "loaded" ||
                    script.readyState == "complete") {
                script.onreadystatechange = null;
                resolve('success: '+url);
            }
        };
    } else {  //Others
        script.onload = function() {
            resolve('success: '+url);
        };
    }

    script.onerror = function() {
        reject(Error(url + 'load error!'));
    };

    script.src = url+'?'+'time='+Date.parse(new Date());
    document.body.appendChild(script);

});

}

function spawn(generatorFunc) { function continuer(verb, arg) { var result; try { result = generatorverb; // 这个result是生成器的返回值,有value和done两个属性 } catch (err) { return Promise.reject(err); } if (result.done) { return result.value; } else { return Promise.resolve(result.value).then(onFulfilled, onRejected); // result.value是promise对象 } } var generator = generatorFunc(); var onFulfilled = continuer.bind(continuer, "next"); var onRejected = continuer.bind(continuer, "throw"); return onFulfilled(); }

//// loading

var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js', your = './js/your.js', my = './js/my.js' ;

// “串行”代码在这里 spawn(function*() { try { yield getJS(jquery); console.log('jquery has loaded'); yield getJS(your); console.log('your.js has loaded'); yield getJS(my); console.log('my.js has loaded'); } catch (err) { console.log(err); } });

效果如下:

现有哪些工具可以实现动态加载

For Your Script Loading Needs列出了许多工具,例如lazyloadLABjsRequireJS等。

有些工具也提供了新的思路,例如LABjs中可以使用ajax获取同域下的js文件。

其他

async

q

co

资料

Script Execution Control

LABJS源码浅析

Dynamic Script Execution Order

script节点的onload,onreadystatechange事件

readystatechange - MDN

readyState property - MSDN

Loading JavaScript without blocking

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.