When I first heard about Flight, I thought it was a brilliant idea. A simple framework done right. The agonistic nature of modules really appealed to me, 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, as well as how we've organised our data and UI components.
TweetDeck: before Flight
For small applications, either one of these techniques can be quite effective in the short term, but over time they become hard to maintain and harder to refactor, even at scale. For large applications it requires serious rigour to stop things from 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 try finding every single reference to a method called 'get' in your codebase and you'll quickly understand why refactoring it could be a serious challenge. This is especially true when the instances get[a] assigned to other identifiers, 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 code monster had reared 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. For example:
In fact, our main worry when designing Flight components is sensibly named events.
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 five core types of event:
Then go and look at someone's profile, or try this:
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 to not worry too much about getting everything right the 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 or small components should be, 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 into lots of little components because nothing is talking to it directly.
Another reason you shouldn’t worry about it is because Flight actually seems to promote good code. It’s 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.
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 Twitter's typeahead search. Our typeahead data module sends out events like this:
The dropdown that sends 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.
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. This allows 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 that 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, i.e. the thing that needs testing most.
I'm working on a fork of Flight including our Jasmine wrappers and extensions which I’ll open up as and when it’s available.
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 identify that 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 into 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 would be awesome to see something similar happen with Flight. Although most data components will be unique to your application, UI components could easily be shared between apps.
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 and plugins library.
Why not go and get involved?
Posted by Tom Hamshere (@tbrd)
Front-end Engineer, TweetDeck