Flight is a new JavaScript framework conjured up by the folks at Twitter (big shout out to @danwrong and the rest of the @flight team). At TweetDeck we have had the opportunity to work with a pre-release version of Flight over the past few months and have now shifted all new development to Flight.
When I first heard about Flight I thought it was a brilliant idea. A simple framework done right. The agnostic nature of modules really appealed, as did the event-driven architecture. Having now used it on a large application, I can say with certainty that it did not disappoint and I’d thoroughly recommend taking a good look.
What I want to talk about here is our experience with Flight at TweetDeck, mainly around our approach in converting TweetDeck to Flight and also how we’ve organised our data and UI components.
TweetDeck: before Flight
TweetDeck’s been around for nearly two years as a JavaScript application (the largest in the Chrome App Store, I believe) and has already undergone two architectural changes. At first, modules were built as straightforward namespaced javascript objects. Understanding that this was perhaps not the best long-term approach the team switched to a klass-based approach and ran the two side by side.
For small applications either one of these techniques can be quite effective in the short term but even at that scale, over time they become hard to maintain and harder to refactor. For large applications it requires serious rigour to stop things getting rapidly out-of-control.
Deep-linking between modules is a big factor in this. TD.vo.Column.get() might make sense in the context of an instance of TD.vo.Column but you just try finding every single reference to a method called ‘get’ in your codebase and you’ll quickly understand why you don’t want to mess around with it, especially when the instances start getting passed around as parameters between modules, creating references like column.get, col.get, etc.
TweetDeck’s modules had become interdependent to the point that we recently found it impossible to create a dependency tree. The spaghetti monster was rearing its ugly head.
TweetDeck: after Flight
To be honest, the codebase is much the same as it was. There certainly isn’t any sense trying to rebuild tens of thousands of lines of code for the sake of it.
We’ve created a new directory in our script folder in which all flight components, tests and libraries sit. The hope is that at some point our flight/app directory will drive the entire app, but that’s a long way off.
Luckily, Flight makes it very easy to bolt new components in on top of an existing structure.
Whenever a method would have referenced another namespaced method, it now fires an event, instantly allowing us to stop worrying about refactoring deeply-linked methods. It’s a lovely feeling, removing all those old namespaced references like
var metadata = TD.controller.columnManager.getColumnByKey(columnKey).getMetadata();
this.doSomething(metadata);
where we make so many assumptions about which objects have been instantiated and which methods they expose. We then replace them with event triggers and listeners that assume nothing about the rest of the application:
this.on('dataColumnMetadata', this.handleColumnMetadata);
this.trigger('uiNeedsColumnMetadata', columnKey);
In fact, our main worry when designing Flight components is sensibly named events.
Event names
I’m not sure we’ve got our event-naming nailed as yet. In fact, our naming conventions seem to be widely disagreed upon within our small team. Despite that, we seem to be managing pretty well.
We followed the basic naming conventions used on Twitter.com and added a few of our own. We have four core types of event:
- ui data request
- A request from the UI for data. E.g.: uiNeedsTwitterProfile, uiNeedsRelationship
- ui user action
- An action performed by the user, probably listened to by a data component. E.g.: uiFollowAction, uiBlockAction
- ui request
- A request for the ui to do something. E.g.: uiShowColumnOptions, uiRemoveColumn, uiCloseModal
- ui moment
- An interesting moment which is the result of a ui request. Some requests have multiple interesting moments: the start and end of an animation, or even the steps within it. “ui” is followed by the name of the component, then the action E.g.: uiShowingColumnOptions, uiColumnOptionsShown, uiColumnOptionsHidden
- data
- An event containing data. “data” is usually followed by the name of the component triggering the event. Generally data components only trigger a single data event. E.g.: dataTwitterProfile, dataRelationship
You can see this in action by adding a listener for these events to the document in Chrome’s JavaScript console. E.g.:
$(document).on('dataTwitterUser', function(e, data) {console.log(e, data);});
Then go and look at someone’s profile, or try this:
$(document).trigger('uiShowProfile', {id: 'tbrd'});
Our conventions are still in flux and as a result we have a number of events which don’t fit this model. Luckily, renaming them is an easy process.
Mentioning that, I’m reminded again of one of the great things about Flight: ease of refactoring. One piece of advice I can offer is that not to worry too much about getting everything right first time around.
We spent quite some time at the outset considering how data components would behave, how ui components would talk to them, how big components should be (or how small) and where we should use mixins instead of components.
When it came down to it, we realised we needn’t have bothered. It’s desperately easy to alter a component to be a mixin, or the other way around. It’s simple to change the way a component works internally because nothing else cares. It’s dead simple to break up a data component in to lots of little components because nothing is talking to it directly.
Another reason not to worry about it is that Flight actually seems to promote good code. It seems to be quite hard to write a huge, meandering component – it’s much easier (and much more pleasing) to create a host of tiny little components, each of them performing their own job perfectly.
Event ownership
In the absence of a call stack, we need to make sure that a particular event is one we actually want to listen to. There could be lots of data events being fired, all with the same name but with data we’re not interested in.
We’ve tried to manage this by attaching identifying data with each request which is then attached to the data response.
For example, we currently have two components, search and compose, which use Twiter’s typeahead search. Our typeahead data module sends out events like this:
this.trigger('dataTypeaheadSuggestions', {
query: queryString,
suggestions: suggestions,
datasources: datasources,
dropdownId: dropdownId
});
The dropdown that send the request for suggestions will need to check that the query, datasources and dropdownId all match its request before doing anything with the suggestions provided.
Testing
We’ve built a test framework for Flight with Jasmine. You can see some example Jasmine tests in the library itself, but that’s not the whole story. There are a few things you’ll definitely need to implement if you want to make your life easier.
First, we (Twitter) extended Jasmine to provide two additional define methods, defineComponent and defineMixin, which set-up the component/mixin for you and provide access to the component and its prototype within tests, allowing you to interrogate attributes and invoke methods directly.
Second, we utilised Jasmine-jQuery, another extension for Jasmine which provides the ability to test jQuery objects and, more importantly DOM events.
We then patched the Jasmine-jQuery extension to provide us with better events features.
Whatever test framework you use, it’s essential it is able to test which events were fired, what data was passed with them and which DOM element they were fired upon. It’s also useful to check how many times the event was fired – we’ve seen some annoying bugs as a result of events being fired twice, or in a circular event chain.
Without this, testing Flight components is largely pointless as the events are the interface between components, ie: the thing that needs testing most.
I’m working on a fork of Flight including our Jasmine wrappers and extensions – no idea when it might be ready to open up on github. I’ll update this as and when.
The future of Flight in TweetDeck
We have got no doubts that we’ve picked the right framework for the job. However, we still have a lot of big challenges to overcome. Our core data layer is still firmly entrenched in old-style code and is going to continue to be for some time to come. Producing a plan that allows us to keep that going side-by-side with Flight without duplicating code or confusing responsibilities is not a simple task.
In the meantime, every new feature we build is built with Flight. We refactor old modules only when required, instead creating Flight wrappers around old data components to provide additional functionality.
Once we a significant proportion of an old module’s functionality exists in Flight wrapper components, that would be an appropriate time to move the entire functionality of the component in to Flight.
We’re trying to ensure that any deep-linked references to old components exist solely in our data modules. The UI should be blissfully unaware that the TD namespace ever existed.
The future of Flight
I see Flight components as providing great plug-and-play modules. jQuery’s plugin library was one of the big reasons for its success and it’d be awesome to see something similar happening with Flight. Although most data components will be very unique to your application, ui components could easily be shared between apps. I’m hoping to make a few (including a keyboard shortcut manager and focus manager) available to the community soon.
Flight is massively extensible – ideas such as data-binding would seem to fit it well, suggesting the possibility of a growing library of extensions. I’d love to see a big, community-led extensions & plugins library.
Why not go and get involved.