Thursday, May 9, 2013

Dynamic Script Load Order

‹prev | My Chain | next›

In last night's #pairwithme session, Kate Bladow and I spent more time debugging JavaScript than we did writing Dart for ICE Code Editor.

The goal of the session was to add Emacs keybindings in the code editor portion of ICE. Since the code editor is done in ACE code editor, that meant that the JavaScript source file providing Emacs keybindings needed to be added to the document. In ICE, JavaScript source files are dynamically created so that the developer need not see any of the usual nastiness of endless <script> tags.

We added the keybinding after the main ace.js source file to mimic how it had been done in JavaScript:
    var script_paths = [
      "packages/ice_code_editor/js/ace/ace.js",
      "packages/ice_code_editor/js/ace/keybinding-emacs.js",
      "packages/ice_code_editor/js/deflate/rawdeflate.js",
      "packages/ice_code_editor/js/deflate/rawinflate.js"
    ];

    var scripts = script_paths.
      map((path) {
        var script = new ScriptElement()
          ..src = path;
        document.head.nodes.add(script);
        return script;
      }).
      toList();
Unfortunately, we found the following error upon loading the example page:
Uncaught ReferenceError: ace is not defined keybinding-emacs.js:31
(anonymous function)
We eventually took a look at the old JavaScript version of the editor to see how it worked. The HTML for the page starts:
<!DOCTYPE html>
<html>
  <head>
    <title>code editor</title>
    <meta charset="utf-8">
    <link rel="stylesheet" href="editor.css">
    <script src="js/ace/ace.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/ace/keybinding-emacs.js"></script>
    <script src="js/rawinflate.js"></script>
    <script src="js/rawdeflate.js"></script>   
At a cursory glance, this seems very similar to the code that dynamically creates the script elements. So why does this work, but adding the scripts dynamically does not?

We found the answer by checking out the Timeline tab in Chrome's developer tools:



Interestingly, the keybinding JavaScript file finishes loading before the main ace.js file, but the ace.js file is evaluated first. When we dynamically added the <script> elements, this did not happen. Whichever source file finished loading first was the first to be evaluated. In our case, the keybindings were loaded first (by virtue of being a much smaller file) and then evaluated before the main ace.js. This threw everything into confusion.

So I ended up learning something new and seemingly very important during the #pairwithme session: the first script listed in the <head> section of a web page is evaluated first, regardless of receive order.

Now that I think about, this was probably something that I have taken advantage of before, without really understanding it. It turns out that the first script in the <head> section of the web page, ace.js, is evaluated before the HTML itself. And, looking at the evaluation of the web page, the remaining scripts in the <head> of the document are then evaluated in order:



It is illuminating actually seeing that in action.

Still, this is not what happens when scripts with src attributes are dynamically added to the document. I need some way to ensure that they are loaded in order—that they are loaded synchronously. Well it turns out that, in modern web browsers, the async attribute for <script> tags does just that. Since I am writing in Dart, I need only concern myself with modern web browsers. If async is false, then scripts are evaluated in the order in which they are added.

So I add the async attribute to the dynamically created script tags:
    var script_paths = [
      "packages/ice_code_editor/js/ace/ace.js",
      "packages/ice_code_editor/js/ace/keybinding-emacs.js",
      "packages/ice_code_editor/js/deflate/rawdeflate.js",
      "packages/ice_code_editor/js/deflate/rawinflate.js"
    ];

    var scripts = script_paths.
      map((path) {
        var script = new ScriptElement()
          ..async = false
          ..src = path;
        document.head.nodes.add(script);
        return script;
      }).
      toList();
With that, I am assured that ace.js will load before keybinding-emacs.js. From there it is fairly straight forward to get Emacs keybindings working—as an easter egg, of course, this is primarily intended for kids after all (in 3D Game Programming for Kids).


Day #746

1 comment:

  1. Great article about this here: http://www.html5rocks.com/en/tutorials/speed/script-loading/

    ReplyDelete