Building a Desktop-Quality App on Web Technologies

Article by: notwebsafe

Building a Desktop-Quality App on Web Technologies

Brackets is built almost entirely in HTML, CSS (via LESS), and JavaScript. For the Brackets team, this is our first project building a large, complex app using open web technologies. In this post, we’d like to describe the overall architecture of Brackets as it stands today, and share some of our early experiences with large-scale JavaScript development.

Why build Brackets in HTML/CSS/JS?

When we started prototyping Brackets last year, we weren’t really thinking too hard about what language to implement it in. We were focused on experimenting with a couple of UI ideas for web development, and just wanted to find the fastest way to prototype them. After looking around and seeing that there were several mature open-source JavaScript-based text editors out there, it seemed worth trying to build the prototype quickly in JS, purely because it’s an easy and flexible language to hack in.

Of course, there are many more important advantages to building it on the web stack:

  • Multi-platform, multi-device. Brackets is currently packaged as a desktop application, largely because we believe many developers still prefer to work with local files. But because it’s written almost entirely as a web application, we can easily deploy it to tablets and other devices, and integrate easily with cloud hosting.
  • If you can use it, you can help build it. Because Brackets is built in the same languages that it’s focused on editing, it’s easy for users to become extension developers and code contributors.
  • No compiling! This is old hat for web developers, of course, but being able to restart your app by just refreshing is really, er, refreshing. #SWIDT

From prototype to product

It’s one thing to hack together a prototype in JavaScript. It’s another thing to try to build a real, robust application.

It’s true that you lose some safety nets by programming in JS or similar languages-strong typing in particular. And without strong typing, you also lose a lot of tooling niceties like compile errors and refactoring. There have certainly been enough times when one of us has facepalmed over making a mistake with this.

But strong typing is only one kind of safety net anyway; there are all kinds of bugs that strong typing and classical OO aren’t going to prevent. The important thing we’ve found is to follow good architectural principles that apply no matter what language you’re programming in, and take advantage of the wide array of tools that have been built around JavaScript:

  • Modularity and encapsulation. Although modules aren’t explicitly built into JavaScript, closure-based module patterns provide a standard and reasonably convenient way to break apart functionality. And reducing API surfaces and cross-module dependencies is just as applicable in JavaScript as in any other language. More on this in the section on architectural patterns below.
  • Unit testing. The lack of refactoring tools forces us to be even more conscientious about building a robust set of unit tests in order to catch breakages. Building good unit tests for UI is tricky, but the declarative nature of HTML actually helps here, because you can treat the DOM structure as a UI model and write tests against it.
  • Static analysis tools. Currently we’re using JSLint, but might move to JSHint in the future. These tools won’t do everything for you that your favorite OO language tools did, but they catch a lot of obvious errors.
  • Browser-based debugging tools. As you might expect, we’re making heavy use of the Web Inspector inside our Chromium-based shell for debugging. (See below for more on our native shell application.)

Building on solid ground

When building Brackets, we wanted to take advantage of existing JavaScript projects as much as possible rather than reinventing the wheel. At the same time, we didn’t want to buy into a large-scale application framework; we wanted the freedom to try out different approaches and switch out different pieces over time if necessary.

There is a wide-and rather bewildering-array of choices in the JavaScript library/framework ecosystem, and there are often many good options for a particular bit of functionality (though partisan flamewars on stackoverflow might try to convince you otherwise). Rather than vacillate endlessly trying to figure out the absolute best choice, we decided to timebox our research and go with what seemed best, with the understanding that we might decide to switch later if we found something wasn’t meeting our needs.

We ended up starting with the following:

  • jQuery, primarily for DOM manipulation, event dispatch, and deferreds/promises (see below). One might argue that jQuery is itself a fairly large framework, but it’s so widespread at this point that we think of it more like a standard library, and it doesn’t dictate the overall structure of your application. We considered using Zepto as a lighter-weight alternative, but we felt that many extension developers would likely be familiar with jQuery and want to use more of its features, and so we might as well include it in the core application. In general, though, because we only intend to target modern browsers, we’ve tried not to use jQuery functions where there’s an ES5 equivalent (e.g. we use [].forEach() instead of $.each()).
  • Bootstrap and LESS for styling. We rely on Bootstrap primarily for our in-app menus and dialog UI, and are using LESS because it’s much more convenient than raw CSS for writing maintainable stylesheets.
  • require.js for module loading (and eventually minification, though we aren’t optimizing our scripts yet).
  • CodeMirror 2 for our core text editor. CodeMirror is a mature and compact editor, and the codebase is flexible enough that it was relatively straightforward for us to add our Quick Edit functionality. We’re working with the project’s maintainer, Marijn Haverbeke, to integrate our changes back into the main project; he’s already accepted a reworking of the virtual scrolling code to eliminate flickering when scrolling rapidly in large documents.
  • Jasmine for unit testing.
  • JSLint for code cleanliness, as mentioned above.
  • We’re also using a few off-the-shelf UI components, such as jsTree and Smart Autocomplete. Again, we didn’t want to buy into a particular UI framework, preferring to keep our dependencies small and relatively fungible, and to do as much layout as possible in CSS rather than relying on JS-based layout (though there are certainly challenges with this, as we’ll discuss below).

We considered a few other kinds of frameworks, but haven’t yet included them in the core app:

  • We considered including a lightweight MVC framework like Backbone. However, so far it’s been relatively easy for us to just manually dispatch and handle fairly coarse-grained events from our models; we haven’t really felt the need for a formal framework. That might change once we start building more fine-grained UI like preference dialogs.
  • We also haven’t included a templating library yet; we’re often creating HTML manually, generally using jQuery to create DOM nodes. We definitely plan to make use of templating in the future-there are already parts of the code (e.g. dialogs) that are cruftier than they need to be because we’re hardcoding HTML rather than generating it from a template. We haven’t done a deep investigation of the alternatives, but Handlebars and Hogan look like potential choices.

Architectural patterns

As mentioned above, we’ve tried to follow good architectural principles in order to keep our codebase sane. We’re also trying not to over-architect up front; we’re trying to structure our code as simply as possible until we find we have a need for a more complicated pattern. Here are a few of the patterns that we’ve been using-some that we deliberately started with, and others that we’ve discovered over time as we gained more experience with JavaScript.

  • Model/view separation. While we aren’t using a formal MVC framework, we’re generally trying to keep models and views separate, and we manually dispatch custom events (using jQuery’s $.triggerHandler()) from models in order to notify views and other modules when underlying data changes.
  • Command pattern. We don’t have a formal application controller, but the Command pattern, which is common in desktop app architectures, serve some of the functions of a global controller. Commands package high-level application operations that can be invoked by various user actions, such as menu items and keyboard shortcuts. Commands also help decouple modules by making it so a given module can simply invoke a global command rather than having to know which module it should talk to for a given piece of functionality.
  • Handling asynchronicity through Deferreds/Promises. In Brackets, as with other complex client-side applications, we need to deal with many kinds of asynchronous operations, largely around file access and connecting to the browser for live development. JavaScript lends itself very well to asynchronous programming via closures, but dealing with callbacks can be a mess, as they can end up having to be passed down as arguments through many levels of function calls. We’ve adopted the jQuery $.Deferred pattern, in which an asynchronous function returns a promise object to which callbacks can be attached in a chained fashion. We’ve also built a set of async utilities on top of $.Deferred that make it easier to manage parallel/sequential asynchronous operations.
  • Modules, constructors, and classes. In general, despite our OO background, we’ve tried to avoid creating deep inheritance hierarchies. The dynamic, untyped nature of JavaScript makes it less necessary to define formal interfaces and superclasses. Instead, we tend to use simple singleton modules where possible, and basic prototype-based constructors where we need multiple instances. We do have a couple of places where we use inheritance, most notably in inline editors, but we’re trying to keep those to a minimum. We’re hooking up prototype chains manually, again because we’re not relying on a particular application framework that would provide inheritance for us.

Since it’s still early days, there are a number of pieces of the Brackets codebase that aren’t as clean as they could be. For example, the ProjectManager module currently mixes model and view code, and will be refactored eventually. Also, the Document and Editor classes aren’t cleanly separated because they both rely on an underlying CodeMirror instance, and CodeMirror itself is not publicly factored into model and view modules (though it does have a model/view separation internally). We’d like to work on improving this separation in CodeMirror so that we can fully decouple these classes in Brackets, and have Document really be a purely model-only class.

Extensibility

Extensibility has been a key goal of Brackets since day one, but here as elsewhere we’ve been trying not to over-architect things-and one of the nice things about building in HTML/CSS/JS is that we don’t necessarily have to. On a platform like Eclipse that’s built in a standard OO language, every single extension point (especially in the UI) has to be explicitly thought of up front and architected into the system. In HTML, it’s almost the opposite; an extension that we load into Brackets can inject or modify anything it wants in the DOM. This potentially gives extension authors great flexibility, but of course also means that over time we might inadvertently break extensions that make too many assumptions about the structure of the DOM.

Our approach has been to start by defining formal APIs in a few key areas that we know extensions will need to augment, such as menus and keyboard shortcuts, as well as adding extensibility APIs for specific features like Quick Edit and Quick Open, so extensions can define their own inline editors and language-specific logic. (There are still a few key areas missing, notably the ability to add code editor themes and new language-specific modes-though we get a lot of modes for free from CodeMirror itself.)

We’ve also been thinking ahead to other kinds of UI that extension authors will want to create. Our goal with Brackets is to keep the UI simple and clean, so we’d want to encourage extensions to keep within our existing UI patterns as much as possible. To that end, we’ve started a document describing extension UI guidelines that describes where we’d like to go with the UI. Not all of these are implemented yet, so this is a good time to send feedback about extension use cases you think would be valuable to consider that don’t fit into these areas.

In addition to the formal APIs we define, though, we’d like to encourage extension authors to try out other kinds of UI as well. If you find that you want to create a kind of UI that we don’t formally support, and want to prototype it by hacking into the DOM, go ahead and give it a try-and start a conversation with the community on the brackets-dev forum. If it makes sense to generalize what you’re doing, we’ll definitely look into adding formal support in the core.

Aside from the UI, another key area for extensibility is in file access-right now we only access the local filesystem, but over time we’ll need to make it possible to access files hosted elsewhere, in the cloud or in source control systems. We haven’t built this out yet, but will definitely need to do so in order to bring Brackets into the browser, and will have to think about things like local caching and synchronization for offline work.

Running on the desktop

As we mentioned at the beginning, Brackets is currently packaged as a desktop application, in order to support a traditional, local-file-based development workflow. Because we also want to deploy Brackets through the browser, though, we’ve written as much of Brackets in HTML as possible. In general, our guiding principle has been to implement all functionality in HTML except where absolutely necessary.

The main pieces that we’ve implemented (or expect to implement) natively are:

  • Local (unsandboxed) fileystem access.
  • Native file and folder open/save dialogs.
  • Native OS menus. Currently, the menus in Brackets are actually implemented in HTML, and will continue to be implemented that way for the in-browser version, but for the desktop application, we think it’s important to provide native menus, especially on the Mac, in order to make it feel like a first-class application rather than an awkward web/desktop hybrid.
  • Explorer/Finder integration, such as file associations (so double-clicking a text file can open Brackets) and dragging files onto the Brackets app icon.

One difficult call we made was to implement dialogs (other than file and folder open/save dialogs) in HTML only, rather than using native dialogs. This does run the risk of having the application feel less native. However, it’s much more difficult to build an API that abstracts across HTML and native dialog layouts (unlike menus, where it’s fairly easy to abstract across the two).

Our current implementation is based on the first version of the Chromium Embedded Framework (CEF), which is an API for embedding the Chromium browser engine in a standalone application. We’ve currently implemented low-level local filesystem access as V8 extensions to within the CEF shell, with an API based on the Node.js filesystem APIs. From the main Brackets code, we use a JS-based wrapper that mimics the HTML5 NativeFileSystem APIs (though they’re not fully implemented in our shell yet).

We’re in the process of migrating to CEF3, the latest version of CEF, which is based on the Chromium Content API and should provide a more maintainable base going forward. CEF3 is more heavily asynchronous internally-CEF1 was single-process, whereas CEF3 has separate browser and render processes; this is obviously better for performance in general, but makes the native extension implementation more complicated, and exposes some bugs in the core Brackets code where it’s not properly handling asynchronicity. We’ll be cleaning this up over the next few sprints.

One interesting area that we’re currently investigating is migrating some of our native functionality to an embedded Node.js server. In addition to providing us with a robust off-the-shelf set of filesystem APIs (rather than our current homegrown ones), leveraging Node would enable us to take advantage of the wide variety of existing developer tools that are built in Node (such as preprocessors, project templating systems, and so on), as well as greatly expanding the extensibility of the native portions of the app. For example, we could offload things like project-wide search/replace to Node rather than running it in client-side JavaScript. Longer-term, this could also give us an analogous code path when we host Brackets online; we could run Node on the server and use it in the same way we would be using it within the native shell. We’ve captured some of our research on using Node in a document on the Brackets wiki.

It’s not all unicorns and rainbows

We’ve had a great time building Brackets in HTML/CSS/JS so far, but as with any technology stack, there are things we’ve struggled with along the way. Here are a few:

  • this‘Nuff said. It’s still pretty easy for us to accidentally use this in a closure without getting it bound properly. For closures nested inside a prototype method, we use the var self = this trick in the outer scope, so we can use self inside the nested closure. When the handler is a separate method, we bind() and reassign the handler in the constructor.
  • Building application layouts. CSS is great in all sorts of ways-not least of which is its declarative nature and easy contextual overridability. But the fact is that the classic CSS layout mechanisms are built for document-oriented layout, not for application layout. Conversely, JavaScript-based layouts are both less performant (especially on overall window resize, which is kind of janky in WebKit-based browsers to begin with) and difficult to write properly (because individual DOM elements don’t dispatch events when they’re resized). To alleviate this, we’re using the flex-box layout that’s available in WebKit, but the implementation is not optimal, and it’s still using the old flex-box model rather than the current standard. This will get better over time as CSS standards for grid and flex-box layouts become finalized and properly implemented.
  • Lack of weak references/maps. This isn’t usually a big problem in ordinary web development. In our case, though, we need to maintain a central cache of Documents, so there’s only ever one Document object for a given file. At the same time, we’d like the Document for a file to go away if no one is referencing it. We can’t get this behavior from the standard garbage collection mechanism without the ability for our central cache to use weak references. As a result, we’ve had to resort to manual reference-counting, which feels yucky in a GCed language. The ECMAScript Harmony group is considering a weak map proposal for the next version of JS.
  • Lack of style scoping. Again, this isn’t usually a problem in web development, but because we’re trying to build an extensible/pluggable architecture, we need better ways to isolate the styles associated with an extension. We also have problems with the fact that we have nested editors within editors; contextual styles (e.g. classes indicating the focused state) that get set on the outer editor can leak into the inner editor. We might be able to solve this with <style scoped>, now that that’s starting to be implemented in browsers, and ultimately a solution like Shadow DOM is probably the right way to go.
  • Performance profiling. We’ve started to instrument the code to measure script performance, but we’ve found that it’s hard to get much insight into the rendering side, even using the Web Inspector profiling tools. In some cases, like typing performance, we’ve resorted to taking high-frame-rate videos in order to figure out exactly how much time there is between pressing a key and seeing it on the screen, and we’ve found that our JS performance measurements are only accounting for part of that time.
  • …and the occasional browser bug. We haven’t run into a huge number of these, but there are a few, such as extra blank lines on paste in WebKit.

Moving forward

Even at this early date in the evolution of Brackets, we’ve learned a lot about building complex HTML/CSS/JS applications, but we know there’s much more to learn, and many ways we could be improving our code. If you’re a web development expert, please check out the Brackets github repo and let us know what you think, good or bad. You can file architectural and feature suggestions as well as bugs in our issue tracker. Even better, please fork us, submit some pull requests or build some extensions, and help us make Brackets even better!

Narciso Jaramillo (@notwebsafe)
Brackets team

Leave a Reply

Your email address will not be published. Required fields are marked *