Why build Brackets in HTML/CSS/JS?
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 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
- 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
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
- 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.
- 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.
$.Deferredpattern, 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.
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 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.
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
thisin a closure without getting it bound properly. For closures nested inside a prototype method, we use the
var self = thistrick in the outer scope, so we can use
selfinside the nested closure. When the handler is a separate method, we
bind()and reassign the handler in the constructor.
- 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.
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)