Search This Blog

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.

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.

Original version

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

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):

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.)

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.

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 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.

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:

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:

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 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:

Original version

The original code also shimmed requestAnimationFrame in bootstrap.js:

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 version

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”.

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:

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:

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.

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:

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.

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:

Read the file into an array buffer.

When the loadEnd event is done, then parse the array buffer.

When the array buffer is done, then read the duration.

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

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.

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.

Popular posts from this blog

Now, this has to have a built-in somewhere in Scala , because it just seems too common. So, how to convert an Array to a List in Scala? Why do I need this? I needed to drop to Java for some functionality, which in this case returns an Array. I wanted to get that Array into a List to practice my functional programming skillz. **Update**: I figured out how to convert Arrays to Lists the Scala way. Turns out it's a piece of cake. val myList = List.fromArray(Array("one", "two", "three")) or val myList = Array("one","two","three").elements.toList The call to elements returns an Iterator , and from there you can convert to a List via toList . Nice. Because my first version wasn't actually tail recursive, what follows is a true tail recursive solution, if I were to implement this by hand. The above, built in mechanism is much better, though. object ArrayUtil { def toList[a](array: Array[a]): List[a] = { d

In which the virtues of automated mechanical arboreal pruning are extolled over quaint manual labor, as applied to web development build processes. The setup Ever notice how the primary bit of marketing for many traditional web programming libraries is their download size? Why is that? Check this out: Why does size matter so much for these libraries? Your first instinct is probably, "because the more bytes you shuttle across the wire, the slower the app starts up." Yes, this is true. I'd also say you're wrong. The primary reason that size matters for these libraries is because traditional web development has no intelligent or automated way to prune unused code so you can ship only the code that is used over the wire. The web is full of links, yet web dev has no linker The web development workflow is missing a linking step. A linker's job is to combine distinct project files into a single executable. A smart linker will only incl