动态添加的脚本标签的加载顺序

Load ordering of dynamically added script tags

我有一个 typescript 应用程序,它动态添加指向 JS 文件的脚本标签。由于一些限制,我不能在 html 文件中静态定义这些脚本标签,所以我通过打字稿动态添加它们,如下所示:

for (let src of jsFiles) {
  let sTag = document.createElement('script');
  sTag.type = 'text/javascript';
  sTag.src = src;
  sTag.defer = true;
  document.body.appendChild(script);
}

现在,我注意到,当我动态添加脚本标签时,它们似乎不能保证它们的加载顺序。不幸的是,jsFiles 数组具有相互依赖的脚本。因此,数组中的第二个脚本只能在第一个完全加载后才能加载。第二个脚本引用第一个脚本中定义的函数。有没有一种方法可以指定动态添加脚本时脚本的排序和执行顺序(类似于在 html 文件中静态定义脚本标签时的排序方式)?

P.S。我想避免使用 onload 回调来解决这个问题,因为我注意到我的应用程序性能下降。我的第二个脚本文件非常大,我假设它导致了性能下降。

我可以提及一些替代方案来克服该要求:

  1. 使用库注入依赖项(AMD 或 CommonJS 模块)
    只需使用modules。 ES2015:import/export,或 CommonJS:require().
  2. 以编程方式创建 script 标记并设置回调 onload 以便在异步加载脚本时做出反应。默认设置 async = true 属性。
  3. 如果允许修改要注入的脚本,则在脚本末尾添加一行 objectarray 以跟踪脚本已加载。
  4. 您可以将脚本作为文本 (XMLHttpRequest) 获取,然后按要求的顺序使用脚本构建 string,最后通过 [=38= 执行文本脚本]
  5. 和不太推荐但经常使用的选项,设置一个setInterval来检查脚本是否已经执行。

我建议选择第一个选项。但出于学术目的,我将说明第二个选项:

Create the script tag programmatically and set the callback onload in order to react when the script has been loaded asynchronously.

我想推荐一本关于脚本加载器的读物:Deep dive into the murky waters of script loading,值得花半个小时!

下面的例子是一个管理脚本注入的小模块,这就是它背后的基本思想:

let _scriptsToLoad = [
  'path/to/script1.js',
  'path/to/script2.js',
  'path/to/script3.js'
];

function createScriptElement() {
  // gets the first script in the list
  let script = _scriptsToLoad.shift();
  // all scripts were loaded
  if (!script) return;
  let js = document.createElement('script');
  js.type = 'text/javascript';
  js.src = script;
  js.onload = onScriptLoaded;
  let s = document.getElementsByTagName('script')[0];
  s.parentNode.insertBefore(js, s);
}

function onScriptLoaded(event) {
  // loads the next script
  createScriptElement();
};

在此 plunker 中,您可以按特定顺序异步测试脚本注入:

主要想法是创建一个 API,允许您通过公开以下方法与要注入的脚本进行交互:

  • addScript:为每个要加载的脚本接收一个 URL 或 URL 列表。
  • load: 运行 以指定顺序加载脚本的任务。
  • reset: 清除脚本数组,或取消脚本加载。
  • afterLoad: 每个脚本加载完成后执行的回调。
  • onComplete: 所有脚本加载完成后回调

我喜欢 Fluent Interface 方法链接 技术,所以我以这种方式构建了模块:

  scriptsLoader
    .reset()
    .addScript("script1.js")
    .addScript(["script2.js", "script3.js"])
    .afterLoad((src) => console.warn("> loaded from jsuLoader:", src))
    .onComplete(() => console.info("* ALL SCRIPTS LOADED *"))
    .load();

在上面的代码中,我们首先加载 "script1.js" 文件,并执行 afterLoad() 回调,接下来,对 "script2.js""script3.js" 执行相同的操作,之后已加载所有脚本,执行 onComplete() 回调。

I would like to avoid using the onload callback to solve this issue since I noticed a performance degradation with my application.

如果您想要订购文件...您需要等待每个文件的加载。没有办法解决它。

这是我曾经写过的一个效用函数:

/**
 * Utility : Resolves when a script has been loaded
 */
function include(url: string) {
  return new Promise<{ script: HTMLScriptElement }>((resolve, reject) => {
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = url;

    script.onload = function() {
      resolve({ script });
    };

    document.getElementsByTagName('head')[0].appendChild(script);
  });
}

对于正在使用 jQuery 的任何人,我改进了@adeneo 脚本,因此它将按指定顺序加载所有脚本。它不进行链式加载,因此非常快,但如果您想要更快,请更改 50 毫秒等待时间。

$.getMultiScripts = function(arr, path) {

    function executeInOrder(scr, code, resolve) {
        // if its the first script that should be executed
        if (scr == arr[0]) {
            arr.shift();
            eval(code);
            resolve();
            console.log('executed', scr);
        } else {
            // waiting
            setTimeout(function(){
                executeInOrder(scr, code, resolve);
            }, 50);
        }
    }

    var _arr = $.map(arr, function(scr) {

        return new Promise((resolve) => {
            jQuery.ajax({
                type: "GET",
                url: (path || '') + scr,
                dataType: "text",
                success: function(code) {
                    console.log('loaded  ', scr);
                    executeInOrder(scr, code, resolve);
                },
                cache: true
            });
        });

    });
        
    _arr.push($.Deferred(function( deferred ){
        $( deferred.resolve );
    }));
        
    return $.when.apply($, _arr);
}

使用方法:

var script_arr = [
    'myscript1.js', 
    'myscript2.js', 
    'myscript3.js'
];

$.getMultiScripts(script_arr, '/mypath/').done(function() {
    // all scripts loaded
});