Replace DOM with javascript and run new scripts

后端 未结 1 688
忘掉有多难
忘掉有多难 2021-01-27 02:55

I am trying to replace the whole DOM on page load to do a no-js fallback for a user created knockout page.

I have it replacing the DOM, but when I do the scripts include

1条回答
  •  孤城傲影
    2021-01-27 03:17

    As you've discovered, the code in the script tags in the text you assign to innerHTML is not executed. Interestingly, though, on every browser I've tried, the script elements are created and placed in the DOM.

    This means it's easy to write a function to run them, in order, and without using eval and its weird effect on scope:

    function runScripts(element) {
      var scripts;
    
      // Get the scripts
      scripts = element.getElementsByTagName("script");
    
      // Run them in sequence (remember NodeLists are live)
      continueLoading();
    
      function continueLoading() {
        var script, newscript;
    
        // While we have a script to load...
        while (scripts.length) {
          // Get it and remove it from the DOM
          script = scripts[0];
          script.parentNode.removeChild(script);
    
          // Create a replacement for it
          newscript = document.createElement('script');
    
          // External?
          if (script.src) {
            // Yes, we'll have to wait until it's loaded before continuing
            newscript.onerror = continueLoadingOnError;
            newscript.onload = continueLoadingOnLoad;
            newscript.onreadystatechange = continueLoadingOnReady;
            newscript.src = script.src;
          }
          else {
            // No, we can do it right away
            newscript.text = script.text;
          }
    
          // Start the script
          document.documentElement.appendChild(newscript);
    
          // If it's external, wait for callback
          if (script.src) {
            return;
          }
        }
    
        // All scripts loaded
        newscript = undefined;
    
        // Callback on most browsers when a script is loaded
        function continueLoadingOnLoad() {
          // Defend against duplicate calls
          if (this === newscript) {
            continueLoading();
          }
        }
    
        // Callback on most browsers when a script fails to load
        function continueLoadingOnError() {
          // Defend against duplicate calls
          if (this === newscript) {
            continueLoading();
          }
        }
    
        // Callback on IE when a script's loading status changes
        function continueLoadingOnReady() {
    
          // Defend against duplicate calls and check whether the
          // script is complete (complete = loaded or error)
          if (this === newscript && this.readyState === "complete") {
            continueLoading();
          }
        }
      }
    }
    

    Naturally the scripts can't use document.write.

    Note how we have to create a new script element. Just moving the existing one elsewhere in the document doesn't work, it's been marked by the browser as having been run (even though it wasn't).

    The above will work for most people using innerHTML on an element somewhere in the body of the document, but it won't work for you, because you're actually doing this on the document.documentElement. That means the NodeList we get back from this line:

    // Get the scripts
    scripts = element.getElementsByTagName("script");
    

    ...will keep expanding as we add further scripts to the document.documentElement. So in your particular case, you have to turn it into an array first:

    var list, scripts, index;
    
    // Get the scripts
    list = element.getElementsByTagName("script");
    scripts = [];
    for (index = 0; index < list.length; ++index) {
        scripts[index] = list[index];
    }
    list = undefined;
    

    ...and later in continueLoading, you have to manually remove entries from the array:

    // Get it and remove it from the DOM
    script = scripts[0];
    script.parentNode.removeChild(script);
    scripts.splice(0, 1); // <== The new line
    

    Here's a complete example for most people (not you), including the scripts doing things like function declarations (which would be messed up if we used eval): Live Copy | Live Source

    
    
    
    
    Run Scripts
    
    
      
    Click me

    And here's your fiddle updated to use the above where we turn the NodeList into an array:

    HTML:

    
        Hello world22
    
    

    Script:

    var model = {
        'template': '\t\u003chtml\u003e\r\n\t\t\u003chead\u003e\r\n\t\t\t\u003ctitle\u003eaaa\u003c/title\u003e\r\n\t\t\t\u003cscript src=\"http://cdnjs.cloudflare.com/ajax/libs/jquery/1.10.1/jquery.min.js\"\u003e\u003c/script\u003e\r\n\t\t\t\u003cscript type=\u0027text/javascript\u0027\u003ealert($(\u0027body\u0027).html());\u003c/script\u003e\r\n\t\t\u003c/head\u003e\r\n\t\t\u003cbody\u003e\r\n\t\t\tHello world\r\n\t\t\u003c/body\u003e\r\n\t\u003c/html\u003e'
    };
    document.documentElement.innerHTML = model.template;
    
    function runScripts(element) {
        var list, scripts, index;
    
        // Get the scripts
        list = element.getElementsByTagName("script");
        scripts = [];
        for (index = 0; index < list.length; ++index) {
            scripts[index] = list[index];
        }
        list = undefined;
    
        // Run them in sequence
        continueLoading();
    
        function continueLoading() {
            var script, newscript;
    
            // While we have a script to load...
            while (scripts.length) {
                // Get it and remove it from the DOM
                script = scripts[0];
                script.parentNode.removeChild(script);
                scripts.splice(0, 1);
    
                // Create a replacement for it
                newscript = document.createElement('script');
    
                // External?
                if (script.src) {
                    // Yes, we'll have to wait until it's loaded before continuing
                    newscript.onerror = continueLoadingOnError;
                    newscript.onload = continueLoadingOnLoad;
                    newscript.onreadystatechange = continueLoadingOnReady;
                    newscript.src = script.src;
                } else {
                    // No, we can do it right away
                    newscript.text = script.text;
                }
    
                // Start the script
                document.documentElement.appendChild(newscript);
    
                // If it's external, wait
                if (script.src) {
                    return;
                }
            }
    
            // All scripts loaded
            newscript = undefined;
    
            // Callback on most browsers when a script is loaded
    
            function continueLoadingOnLoad() {
                // Defend against duplicate calls
                if (this === newscript) {
                    continueLoading();
                }
            }
    
            // Callback on most browsers when a script fails to load
    
            function continueLoadingOnError() {
                // Defend against duplicate calls
                if (this === newscript) {
                    continueLoading();
                }
            }
    
            // Callback on IE when a script's loading status changes
    
            function continueLoadingOnReady() {
    
                // Defend against duplicate calls and check whether the
                // script is complete (complete = loaded or error)
                if (this === newscript && this.readyState === "complete") {
                    continueLoading();
                }
            }
        }
    }
    runScripts(document.documentElement);
    

    This approach just occurred to me today when reading your question. I've never seen it used before, but it works in IE6, IE8, Chrome 26, Firefox 20, and Opera 12.15.

    0 讨论(0)
提交回复
热议问题