Friday, May 9, 2014

I ported a JavaScript app to Dart. Here's what I learned.

In which I port a snazzy little JavaScript audio web app to Dart, discover a bug, and high-five type annotations. Here's what I learned.

[As it says in the header of this blog, I'm a seasoned Dart developer. However, I certainly don't write Dart every day (I wish!). Don't interpret this post as "Hi, I'm new to Dart". Instead, interpret this post as "I'm applying what I've been documenting."]


This post analyzes two versions of the same app, both the original (JavaScript) version and the Dart version. The original version is a proxy for any small JavaScript app, there's nothing particularly special about the original version, which is why it made for a good example.


This post discusses the differences between the two implementations: file organization, dependencies and modules, shims, classes, type annotations, event handling, calling multiple methods, asynchronous programming, animation, and interop with JavaScript libraries. Finally, I detail the lessons learned.

Background



Mr. Paul Lewis wrote a snazzy little web app for audio processing. His Music DNA app uses JavaScript to load up an audio file, play it, and visualize the sounds in a browser. It’s a cool little demo of what’s possible on the web platform.


Screen Shot 2014-04-25 at 4.10.53 PM.png


I’m always impressed with Paul’s designs and code, so I asked myself: Could I easily port this app to Dart? Was Dart up to the challenge of a web audio app? Is there benefit to using Dart for little apps like this?


Spoiler: yes, yes (mostly), and yes. Read on!

Fine print



I ported the original JavaScript version of Music DNA app as of this revision. The code for the  Dart version is also available. It’s possible that by the time you read this, the original JavaScript version will have changed. I also attempted to minimize the differences between the two code bases. I tried to keep the names the same (when appropriate), and the number of files the same (again, when appropriate).

Organization



In which I examine file layout and navigation of both the original project and my Dart version.

Original version



The original version collected all files into a single directory.


Screen Shot 2014-04-25 at 9.48.35 PM.png
Original version


To discover the “main” JavaScript file, I opened index.html and I found six script files:


Screen Shot 2014-04-25 at 9.49.50 PM.png
Original version


Hm, where does the application start? I wasn’t sure. I opened each file, in order. It appeared that bootstrap.js contained the “entry point” of the application.


The entry point was a “immediately invoked function expression” (IIFE):


music-dna_bootstrap_js_at_master_·_paullewis_music-dna.png
Original version

Dart version



Dart’s package manager, pub, defines a layout convention for packages and apps. The Dart version has the HTML, and Dart files for the app, inside a web directory. The library files (files that contain libraries that might be useful for any app) are inside a lib directory. (More on this soon.)


Screen Shot 2014-04-25 at 10.34.11 PM.png
Dart version


To find the “entry point”, I opened the main HTML file (music_dna.html). I found a single Dart file and two supporting JavaScript files.


Screen Shot 2014-04-25 at 9.59.01 PM.png
Dart version


I did not port the id3.js file to Dart for two reasons: it was minified, and I wanted to test Dart-JavaScript interop. The dart.js file is boilerplate: it helps the app run in both JavaScript engines and Dart VM.


Dart_Editor.png
Dart version


The application starts from the main() function in bootstrap.dart. All Dart programs start at the main() function.

Dependencies and modules



In which I examine how both versions handle linking to libraries or modules.

Original version



JavaScript doesn’t have a native library or module system. The original version listed each “module” as a separate script tag from the main HTML file (screenshot above). The order of the scripts is important, as scripts that load later depend on scripts that were loaded earlier. The code does not explicitly declare its dependencies.


For example, the bootstrap script creates a new instance of MusicDNA. The bootstrap.js file does not include a reference to the location of the definition of the MusicDNA function.


music-dna_bootstrap_js_at_master_·_paullewis_music-dna.png
Original version


The MusicDNA function comes from the music-dna.js file, which is loaded before the bootstrap.js file via script tags.

Dart version



The Dart language natively supports libraries. A library can import other libraries. For example, the bootstrap.dart file imports its dependencies:


Screen Shot 2014-04-25 at 10.31.14 PM.png
Dart version


The Dart SDK has some standard libraries like dart:html for DOM access, dart:js for interop with JavaScript (more on this below), and more. The code also imports music_dna.dart which explicitly provides the MusicDNA class. Using the show keyword with import makes it easy to glance at what names are provided by what library.


The music_dna.dart file is its own library, which also declares its own dependencies:


Screen Shot 2014-04-25 at 11.10.20 PM.png
Dart version


Notice the two types of URIs used in imports: dart:____ and package:_____. Imports from dart: come from the Dart SDK, and imports from package: refer to libraries that live in packages. A package is a bundle of Dart code, conforming to some layout conventions, and declared by a pubspec.yaml file.


Both libraries and applications can live inside of a package. The music_dna application is itself a package. The music_dna library imports the audio library from its own package. This is a handy way to avoid using relative (and brittle) paths that would have to reach up and out of the file’s directory.


Back to libraries: A library can be split into multiple parts. The audio.dart file declares the library, its imports, and the two other files that contain the library’s classes: audio_parser.dart and audio_renderer.dart.


Dart_Editor.png
Dart version


An abbreviated way to look at the libraries and dependencies of the Dart version:


Dart version

Shims



In which I explore how both versions deal will shimming browser APIs, such as Web Audio and requestAnimationFrame. A shim is a thin layer of code that compensates for browser differences in function names or APIs.

Original version



The original version shimmed the APIs using a common JavaScript language pattern. For example, here is the shim for AudioContext (from Web Audio) in audio_parser.js:


music-dna_audio-parser_js_at_master_·_paullewis_music-dna.png
Original version


The original code also shimmed requestAnimationFrame in bootstrap.js:


Screen Shot 2014-04-25 at 10.44.26 PM.png
Original version

Dart version



The dart:html library hides the shims, so I don’t need to worry about it. For example, I just needed to import the dart:web_audio library and then use the AudioContext name.


Dart_Editor.png
Dart version


Dart_Editor.png
Dart version


When I compile the Dart app to JavaScript, the shims are included in the generated JavaScript. This ensures the generated code works in all modern browsers.

Classes



In which I examine how both versions model and construct concepts.

Original version



JavaScript is not a class-based object oriented language. The original version used standard JavaScript patterns for “classes”.


music-dna_audio-parser_js_at_master_·_paullewis_music-dna.png
Original version


The JavaScript version initializes fields by constructing new objects as well calling methods. The object isn’t initialized until the entire body of the AudioParser function is run.

Dart version



Dart is a class-based object oriented language. I created a formal class for AudioParser.


Dart version


The Dart version initializes most fields inside of the constructor body. By the time the constructor body is run, the object is initialized.

Type annotations (aka discovering a bug)



In which I use type annotations and discover a bug in the original code.

Original version



JavaScript doesn’t have a way to explicitly state that a variable can hold a specific type of object. Fields are simply marked as var, as in this example:


Screen Shot 2014-04-26 at 12.26.34 AM.png
Original version


It’s unclear what kind of function can be assigned to audioDecodedCallback. I can assume what sourceNode or audioRenderer can be, because I know this is a Web Audio app and there’s another file called audio-renderer.js.


Method parameters also do not have a type annotation. Here is one example:


Original version


Can you spot the bug in the above example? I didn’t, until I converted the code to Dart.

Dart version



Type annotations are optional in Dart, but I love ‘em. I use type annotations as inline and explicit docs for my fellow programmers and my productive tools.


We’ve seen it before, but here’s the recap of how I use type annotations for fields in AudioParser:


Screen Shot 2014-04-26 at 12.36.44 AM.png
Dart version


So, gainNode is of type GainNode. I could probably have guessed that, if I knew there was a Web Audio type called GainNode. Without the type annotation, editors and analyzers can’t infer that gainNode is a GainNode. And that’s important, because…


Dart version


Dart Editor and the Dart analyzer knows that disconnect() takes an int and not a GainNode. A warning is displayed, and bug identified!


Turns out, the code didn’t need the call to disconnect() anyway. But, I feel more confident knowing that tools can identify potential errors.

Event handlers, and calling multiple methods on the same object



In which I encounter a Dart-y DOM, use a Dart language feature, and reduce the amount of code I have to type.

Original version



The traditional way to listen for DOM events (like drop or dragover) is to use addEventListener(). This method takes a string as the first parameter. If you misspell the name of the event, you will not get a warning, and the program will keep on trucking. There is no code completion for the values of the strings, because the language can’t express all the valid values for the first argument.


Here is an example, from the original code, of listening to multiple events on a single fileDropArea object.


Screen Shot 2014-04-26 at 12.47.25 AM.png
Original version

Dart version



The dart:html library offers DOM APIs that are more toolable. There are specific methods for specific events, for example onDrop. If you misspell the method name, the tools can warn you about an unknown method. Also, you can use code completion to help discover other events to listen for.


Dart supports method cascades, which help reduce repetitive code. If you need to call multiple methods on a single object, cascade the calls and avoid repeating the variable.


Here is the Dart code for setting up the drag and drop area:


Screen Shot 2014-04-26 at 1.12.45 AM.png
Dart version


Notice how fileDropArea is stated only once. The double dot (..) syntax is the cascade. Compare this version to the original JavaScript version above.

Asynchronous callbacks



In which I explore how both versions handle asynchronicity.

Original version



When something needs to run “later”, JavaScript calls a callback function. The MusicDNA app uses asynchronous callbacks when decoding audio data.


  1. MusicDNA has a onAudioDataParsed function.
  2. MusicDNA creates a new AudioParser, and passes onAudioDataParsed to it.
  3. MusicDNA calls audioParser.parseArrayBuffer(), which calls an asynchronous decodeAudioData method.


Control is returned to the browser, and events are processed as normal. The audio is decoded in the background. Sometime later...


  1. When decodeAudioData is finished and is successful, it calls audioParser.onDecodeData.
  2. When onDecodeData is finished, it calls onAudioDataParsed.
  3. Now we’re back in MusicDNA!


As you can see, a function can get passed around, with no clear line of execution.


Here’s the code, see if you can follow the callback:
Original version


In AudioParser:
Original version


parseArrayBuffer() calls decodeAudioData() which asynchronously calls onDecodeData.
Original version


The onDecodeData function calls the callback given to AudioParser from the constructor:


Original version

Dart version



Dart’s core SDK has a Future class, which represents values available sometime in the future. An asynchronous function or method returns a Future instead of accepting a callback as a parameter. The Future completes when the asynchronous function is finished, and a value is available.


In the Dart version, parseArrayBuffer returns a Future, as seen below:
Dart version


Now that the async methods can return futures, the code can get simplified. For example, Futures are chainable, so music_dna.dart looks like this:


Dart version


The above code reads:


  1. Read the file into an array buffer.
  2. When the loadEnd event is done, then parse the array buffer.
  3. When the array buffer is done, then read the duration.
  4. Return a future to caller of parse knows when parsing is finished.

Animation



Modern web apps animate with requestAnimationFrame, which is an API that lets your app get notified when the browser is ready for your app to draw a frame.

Original version



Again, because JavaScript uses callbacks for async notifications, the requestAnimFrame (a shimmed version of requestAnimationFrame) uses callbacks.


Original version

Dart version



Dart uses Futures in many places, including in the HTML libraries. Specifically, requestAnimationFrame has been rewritten to return a Future that completes when the frame is ready.


Dart version

Code size


There are two ways to look at code size: the original code that the developer works with, and the compiled, minified, gzipped code size. It's important to stress that neither the original version or the Dart version made any attempts to create the most terse code possible. Line counts include blank lines and comments. I believe this app is too small to make any reasonable claim about lines of code correlation to language.

Original version


The important JavaScript files, their sizes, and number of lines:
  • audio-parser.js: 1823 bytes, 78 lines
  • audio-renderer.js: 2751 bytes, 120 lines
  • bootstrap.js: 1803 bytes, 69 lines
  • music-dna.js: 3608 bytes, 130 lines
Total bytes for the scripts (not counting id3.js): 9985 bytes.

Total lines for the scripts: 397 lines.

The original app did not concat the JS files or minify them, however most developers should perform those steps with "real" apps. Most web servers will gzip on the fly, so it's worth noting that the total number of gzipped bytes from scripts (gzipping each file individually) results in 3825 bytes.

Dart version


The important Dart files, their sizes, and number of lines:
  • lib/audio.dart: 427 bytes, 12 lines
  • lib/src/audio_parser.dart: 1786 bytes, 68 lines
  • lib/src/audio_renderer.dart: 2273 bytes, 99 lines
  • web/bootstrap.dart: 1763 bytes, 71 lines
  • web/music_dna.dart: 1623 bytes, 59 lines
Total bytes for the scripts (not counting id3.js): 7872 bytes.

Total lines for the scripts: 309 lines.

Compiling and minifying the Dart scripts to JavaScript generates 143kb bytes. Gzipping the generated JavaScript (as most web servers and browsers compress/decompress on the fly) results in 41kb bytes. (For comparison, jQuery itself is 32kb minified and gzipped. However, the original Music DNA app does not use jQuery.)

Interop with JavaScript libraries



The Music DNA app uses a JavaScript file called id3.js to read the ID3 tags and extract metadata like song title and artist.

Original version



The id3.js file is included as a script tag, just like any other JavaScript file.


Original version


Using the ID3 functionality is easy, as it’s just JavaScript in JavaScript. Here is how the ID3 library is used inside of bootstrap.js:


Original version

Dart version



Using JavaScript files in Dart requires the dart:js library, which provides Dart-JavaScript interop. I had to import the dart:js library, explicitly get a reference to the ID3 object (which originally came from the id3.js file), and use the callMethod method to call JavaScript methods.


I was able to use id3.js in the Dart app, but the integration isn’t as trivial as it is with JavaScript-JavaScript interop.


Here is the Dart code:


Dart version

Lessons learned



Porting the Music DNA app from JavaScript to Dart was a lot of fun. Here’s what I learned:


  • Futures are great! They really simplify API design.
  • Type annotations save the day. I found a bug in the original code after I ported it to Dart, thanks to type annotations and code analysis.
  • You can interoperate with JavaScript from Dart, it’s just not a “cut and paste” job.
  • It’s easy to tell where Dart programs start.
  • Compiled, minified, and gzipped apps about this size end up a little bigger than jQuery.
  • Dart’s library mechanism makes it easy to determine where names come from.
  • Dart’s HTML library shims APIs like requestAnimationFrame for you.
  • Method cascades are sweet.
  • Code completion. Love it. Especially helpful when exploring new APIs.

There are other language and library features of Dart that I didn't get a chance to use. If you're interested, there is a Dart language tour that might be interesting to you.

Of course, most of the techniques used by Dart in this article aren't impossible with JavaScript. In fact, future versions of JavaScript should get features like modules and promises. They just aren't built-in, obvious, or easy "out of the box" with JavaScript as we know it today. And this is where I begin to see some of the real power with Dart: the out of the box experience is really good. Shims? yup. Futures? yup. Layout convention? yup. Classes? yup. Type annotations (instead of comment)? yup. Dart seems to have a broader and more functional base on which we can build. I like it.


Many thanks to Paul Lewis for the inspiration and sharing his code.

Disclaimer

I'm probably required to say that the views expressed in this blog are my own, and do not necessarily reflect those of my employer. Also, except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 3.0 License, and code samples are licensed under the BSD License.