使用远程src注入脚本标签并等待其执行


70

如何将<script src="https://remote.com/"></script>元素注入到页面中,等待其执行,然后使用其定义的功能?

仅供参考:就我而言,该脚本在极少数情况下会进行一些信用卡处理,因此我不想总是包含它。当用户打开“更改信用卡选项”对话框,然后向其发送新的信用卡选项时,我想快速添加它。

编辑以获取更多详细信息:我无权访问远程脚本。


您可以控制远程脚本吗?如果这样的话,让脚本本身在完成时调用您的代码可能会更容易,a-la JSONP
hugomg 2011年

不幸的是,我无法控制远程脚本。编辑的问题,以反映〜
莱利云雀

Answers:


139

您可以使用Google Analytics(分析)Facebook的方法:

(function(d, script) {
    script = d.createElement('script');
    script.type = 'text/javascript';
    script.async = true;
    script.onload = function(){
        // remote script has loaded
    };
    script.src = 'http://www.google-analytics.com/ga.js';
    d.getElementsByTagName('head')[0].appendChild(script);
}(document));

更新:

下面是新的Facebook方法;它依赖于现有的脚本标签,而不是<head>

(function(d, s, id){
    var js, fjs = d.getElementsByTagName(s)[0];
    if (d.getElementById(id)){ return; }
    js = d.createElement(s); js.id = id;
    js.onload = function(){
        // remote script has loaded
    };
    js.src = "//connect.facebook.net/en_US/sdk.js";
    fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
  • 替换facebook-jssdk为唯一的脚本标识符,以避免多次附加。
  • 用您自己的脚本替换URL。

这似乎很完美,但是互联网上有一些chat不休的说法,并不是所有浏览器都调用onload。您知道它将与哪些浏览器一起使用吗?
莱利云雀

为什么不将其附加到身体上?
Ben Taliadoros 2014年

1
而不是使用script.onloadjs.onload,我将向事件添加事件侦听器loadstackoverflow.com/questions/15564029/...
ha404

附加到另一个script与附加到另一个有好处head吗?我认为附加到scripts的不利之处在于,如果没有附加内容,它会中断。
ha404 2015年

6
请注意,如果脚本已经存在,则d.getElementsByTagName(s)[0]可以将第一个查询移到if检查的下方,以为自己保存DOM查询。
mAadhaTTah

43

使用事件侦听器和ES2015构造的相同方法:

function injectScript(src) {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = src;
        script.addEventListener('load', resolve);
        script.addEventListener('error', e => reject(e.error));
        document.head.appendChild(script);
    });
}

injectScript('https://example.com/script.js')
    .then(() => {
        console.log('Script loaded!');
    }).catch(error => {
        console.error(error);
    });

真好!这将是一个好的NPM软件包或ES6模块。+1以获取示例用法。
A-Diddy '18

您知道为什么在then()主体中我不能使用已加载脚本中定义的变量/函数吗?
dar0x

11

这是一种动态加载和同步执行脚本列表的方法。您需要将每个脚本标签插入DOM,将其async属性显式设置为false:

script.async = false;

默认情况下,注入到DOM中的脚本是异步执行的,因此您必须手动将async属性设置为false来解决此问题。

<script>
(function() {
  var scriptNames = [
    "https://code.jquery.com/jquery.min.js",
    "example.js"
  ];
  for (var i = 0; i < scriptNames.length; i++) {
    var script = document.createElement('script');
    script.src = scriptNames[i];
    script.async = false; // This is required for synchronous execution
    document.head.appendChild(script);
  }
  // jquery.min.js and example.js will be run in order and synchronously
})();
</script>

<!-- Gotcha: these two script tags may still be run before `jquery.min.js`
     and `example.js` -->
<script src="example2.js"></script>
<script>/* ... */<script>

参考文献


现在正在运行,已被批准。顺便说一句,最后一个关闭脚本缺少反斜杠。
华盛顿瓜迪斯

6

这样的事情应该可以解决问题:

(function() {
    // Create a new script node
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.onload = function() {
        // Cleanup onload handler
        script.onload = null;

        // do stuff with the loaded script!

    }

    // Add the script to the DOM
    (document.getElementsByTagName( "head" )[ 0 ]).appendChild( script );

    // Set the `src` to begin transport
    script.src = "https://remote.com/";
})();

希望有帮助!干杯。


您是否有理由在回调中将脚本onload手动设置为null?触发不止一次是否有问题,或者这仅仅是强制某种垃圾回收?
James Tomasino 2013年

是的,这是为了避免旧IE中的内存泄漏,因为脚本元素将函数引用为onload处理程序。
keeganwatkins

5

动态 import()

使用动态导入,您现在可以加载模块并等待它们执行,就像这样简单:

import("http://example.com/module.js").then(function(module) {
  alert("module ready");
});

如果模块已经被加载和执行,则不会再次加载和执行模块,但是返回的promiseimport仍然可以解决。

请注意,文件是作为模块而不是脚本加载的。模块以严格模式执行,并且在模块范围内加载,这意味着不会像在正常加载的脚本中那样自动将变量全局化。export在模块中使用关键字可以与其他模块或脚本共享变量。

参考文献:


1

创建一个加载器

您可以有序地在加载程序中注入脚本。

请注意,动态加载脚本的执行通常是在静态加载脚本(即<script src="My_script.js"></script>)之后执行的(DOM中的注入顺序并不能保证相反的情况):

例如,loader.js:

function appendScript(url){
   let script = document.createElement("script");
   script.src = url;
   script.async = false //IMPORTANT
   /*Node Insertion Point*/.appendChild(script);
}
appendScript("my_script1.js");
appendScript("my_script2.js");

my_script1.js将有效之前执行my_script2.js,(有益的,如果相关性my_script2.jsmy_script1.js

请注意,这很重要,script.async = false因为async = true默认情况下动态加载的脚本async不会确保加载顺序。


0

这是我根据弗兰克的回答改编的版本:

static async importScript(src, expressionToEvaluateAndReturn){

        return new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.async = true;
            script.src = src;
            script.addEventListener('load', (event)=>{
                if(expressionToEvaluateAndReturn){
                    try{
                        let result = eval(expressionToEvaluateAndReturn);
                        resolve(result);
                    } catch(error){
                        reject(error);
                    }

                } else {
                    resolve();
                }
            });
            script.addEventListener('error', () => reject('Error loading script "' + src + '"'));
            script.addEventListener('abort', () => reject('Script loading aborted for "' + src + '"'));
            document.head.appendChild(script);
        });    

    }   

用法示例:

let d3 = await importScript('/bower_components/d3/d3.min.js','d3')
                    .catch(error => {
                        console.log(error);
                        throw error;
                    });
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.