Saturday, January 21, 2012

Supporting Both Dart and Javascript

‹prev | My Chain | next›

Tonight I would like to get my Dart code running side-by-side with the Javascript equivalent. That is, I would like to serve up Dart to Dart-enabled browsers, but the Javascript equivalent for other browsers. No doubt that, as the language and platform evolves, there will be easy ways to accomplish this. For tonight, I would like to know if it is possible.

Currently, the Dart in my web application is loaded via a <script> tag:
<!DOCTYPE html>
<html>
<head>
  <title>Dart Comics</title>
  <link rel="stylesheet" href="/stylesheets/style.css">

  <!-- Force Dartium to start the script engine -->
  <script>{}</script>
  <script src="/scripts/comics.dart" type="application/dart"></script>
</head>

<body>
<!-- ... -->
</body>
</html>
When I compile this dart into Javascript with the frogc (I still don't get the name), it produces a file named comics.dart.js. I would almost like one of those old timey IE conditional comments to load in that Javascript file:
<!--[if Dart]>
  <script src="/scripts/comics.dart" type="application/dart"></script>
<![endif]-->
<!--[if !Dart]>
  <script src="/scripts/comics.dart.js" type="text/javasript"></script>
<![endif]-->
But I don't really want that. It sucks. Besides, I am duplicating the basename of /scripts/comics.dart in both <script> tags.

What I would like is for the Dart code—and only the Dart code—to execute in Dart browsers. Getting the Dart code is easy enough, but how do I detect that dart is enabled so that I can prevent <script src="/scripts/comics.dart"> from being evaluated?

In other browsers, I would like a simple, unobtrusive fallback to the Javascript.

There are plans to use Isolates to allow Dart to communicate with Javascript code. But, since that is not available today, I need some other way for Dart to tell Javascript that it is present. The only way that I can think to communicate between the two is the DOM:
<script type="application/dart">
  #import('dart:html');
  main() {
    var el = new Element.html('<div id="_is-dart-enabled"/>');
    document.nodes.add(el);
  }
</script>
If Dart is enabled, that bit of code will insert a <div> tag into the DOM with an ID of _is-dart-enabled. I should then be able to to query for the presence of that tag from Javascript:
<script>
  if (document.getElementById('_is-dart-enabled') == null) {
    console.log('Not Dart enabled.');
  }
</script>
Only that does not work. I always see that console.log() output—even in Dartium which does add the _is_dart-enabled tag as expected.

This turns out to be caused by the different ways in which Javascript and Dart are evaluated. Dart only executes once the DOM is ready. Javascript is evaluated immediately (unless wrapped in something like a DOM ready callback). Thus, the Javascript is always evaluated before Dart has a chance to insert the DOM element.

To get around this, at least for now, I add a setTimeout:
<script>
  setTimeout(function() {
    if (document.getElementById('_is-dart-enabled') == null) {
      console.log('Not Dart enabled.');
    }
  }, 50);
</script>
With that, I only see "Not Dart enabled." when the browser really does not support Dart. So far so good. I convert that into a noDart() function that takes a callback and arguments to be invoked when there is no Dart available:
function notDart() {
  var args = Array.prototype.slice.call(arguments)
    , fn_when_no_dart = args.shift();

  setTimeout(function() {
    if (document.getElementById('_is-dart-enabled') == null) {
      console.log('Not Dart enabled.');
      fn_when_no_dart.apply(null, args);
    }
  }, 50);
}
With that, I can say that, in cases when there is no Dart, I should load the Javascript equivalents:
notDart(loadJsEquivalentScripts);

function loadJsEquivalentScripts() {
  var scripts = document.getElementsByTagName('script');
  for (var i=0; i<scripts.length; i++) {
    loadJsEquivalent(scripts[i]);
  }
}

function loadJsEquivalent(script) {
  if (!script.hasAttribute('src')) return;
  if (!script.hasAttribute('type')) return;
  if (script.getAttribute('type') != 'application/dart') return;

  var js_script = document.createElement('script');
  js_script.setAttribute('src', script.getAttribute('src') + '.js');
  document.body.appendChild(js_script);
}
And, with that, I have Firefox loading my Dart application:


And it still works in Dartium (with no console message about Dart not being enabled):

That is a little hackish, but it works. I will clean that up and make it available for others if anyone else would like to use it.

Day #272

4 comments:

  1. Hi Chris,

    Frogc gets its name from the Dart Frog.

    Second, see the recent Code Review for a Dart / JavaScript bootstrap: http://codereview.chromium.org/9226025/

    Hope that helps,
    Seth

    ReplyDelete
    Replies
    1. I think you've told me about the dart frog before. It just doesn't stick for some reason :P

      Aaargh! I'm a day late and that implementation is even better than mine `document.implementation.hasFeature('dart', '1.0')`. Dammit :)

      Delete
  2. Could you instead just give either body or html a class name? Something like what Modernizr does?

    So you end up with <html class="dart"> if Dart is enabled?

    ReplyDelete
    Replies
    1. Yah, thanks for pointing that out. Dunno why that didn't occur to me...

      Delete