The need for Flex modules
When applications grow to be bigger, there are several reasons to split them up into modules. Here we focus on some reasons specifically applied to our Flex applications. This makes it clear what features our modularity system provides and why.
- For developers: Code manageability. If you’d develop a large Flex application into one big project, it would quickly grow to be unmanageable. Any class can depend on any other one. By splitting into separately compiled modules, you are forced to properly separate concerns, giving better maintainable code. Although they are compiled separately, modules can depend on each other, but dependency cycles must be impossible.
- For users: Performance. Flex applications can be used over the internet, over possibly slow connections. Someone who only uses part of the application should not have to download everything before he can use it. The module system automatically takes care of loading modules on demand; only when they are really needed.
- Pluggability. Not every user is interested in all functionality, some might only need a limited set of modules. Developers cannot foresee all possible combinations of modules that can be shipped together. Some client-specific modules can be added too. The main application and modules can not know in advance (at development/compilation time) which other modules will be available. Available modules must automatically plug their features into the appropriate locations (e.g. in the user interface), even before they have been loaded.
Technical basis
In the Flex/Flash world, there are several kinds of module artifacts. None of these provide the separately compiled, inter-dependent, on-demand-loaded modules we want; but they are the building blocks of our solution.
- Swf modules are loaded on demand at runtime by the Flash Player. But you cannot use a swf as a dependency when building another artifact.
- Swcs can be used as a dependency when building another artifact. They are compile-time-only: helpful during development, but they don’t exist at runtime. The Flash Player cannot load them.
- Rsls (runtime shared libraries) can be used as dependencies and are loaded at runtime. But they are always loaded immediately when the application or module that uses them is loaded. They are not on-demand.
The common way of splitting a Flex application into swf modules, is to have the application and all its module in the same project. In other words, dropping our “separately compiled” requirement. You tell the Flex compiler what the main classes of the application and the modules are, and it will automagically figure out which classes are (directly or indirectly) used by which part, and include them in the right swf that it generates. This big project that contains or depends on everything and generates all swf artifacts may work for small applications, but for bigger ones it has several problems. It is slow to compile, hard to manage and predict (what codes ends up in what module), and does not fit into the maven world. And there’s no way to make this pluggable; no way to add a module to this without touching the central project.
Our solution is to have sources of modules separately (in different maven modules / FB projects), and for each of them generate both a swf and a swc from the same sources. The swf is deployed in the web application, and the swc is only used for other modules that depend on it. This takes care of code manageablility and performance. Next, we’ll make modules pluggable.
Extensions and extension points
With the standard approach of loading Flex modules (using the ModuleLoader or ModuleManager), the name of the swf to load is hard-coded in the main application. We want any module to be able to plug itself in, so the application can’t have an explicit reference to any module, not even the name. To accomplish this, when some code needs a feature that may be in another module, instead of asking for a particular module, it must ask for the feature it needs. Modules describe which features they provide, so at runtime the system can figure out which module needs to be loaded.
For this we heavily base ourselves on the ideas of Eclipse extension points and extensions, but apply them in Flex. An extension point is a place in an application or module, where another module’s extension can be plugged in to add the needed functionality. Just like in Eclipse, modules describe their extension points and extensions in a plugin.xml file. For example, we have a main application with a menu bar, where entries in the menu can open a view that fills most of the screen. We call these kinds of views tasks. One such task in the Flex scheduler module is the TriggersTask, in which the user can configure a scheduler running on the server. The main application defines the task extension point in its plugin.xml and provides the xml schema for this point. In the scheduler module’s plugin.xml, there is the TriggersTask extension. Concretely, it looks like this:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
<?xml version="1.0" encoding="UTF-8"?> <plugin xmlns="http://www.bsb.com/sf/flex/plugins/1" xmlns:ext="http://www.bsb.com/sf/flex/points/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.bsb.com/sf/flex/plugins/1 http://www.bsb.com/schemas/sf/flex/plugins-1.0.xsd http://www.bsb.com/sf/flex/points/1 http://www.bsb.com/schemas/sf/flex/points-1.3.xsd"> <extensions /> <points> <point id="com.bsb.sf.Task" qname="ext:Task" /> ... </points> </plugin> |
|
1 2 3 4 5 6 7 8 9 10 11 |
<?xml version="1.0" encoding="UTF-8"?> <plugin xmlns="http://www.bsb.com/sf/flex/plugins/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sf="http://www.bsb.com/sf/flex/points/1" xsi:schemaLocation="http://www.bsb.com/sf/flex/plugins/1 http://www.bsb.com/schemas/sf/flex/plugins-1.0.xsd http://www.bsb.com/sf/flex/points/1 http://www.bsb.com/schemas/sf/flex/points-1.2.xsd"> <extensions> <sf:Task taskId="triggers" task="com.bsb.sf.scheduler.console.triggers.TriggersTask" /> ... </extensions> </plugin> |
The plugin.xml of course can’t be embedded in the swf, because it is needed before the swf is loaded. Instead, for every module, in addition to the swc and swf, we package the plugin.xml separately. All plugin.xmls of available modules are combined on the server-side into one plugins.xml file which is downloaded by the flex client at startup. That way it knows which tasks it can show in the menu without loading any of their module. Using this extension point mechanism makes it easy to abstract away the details involved in dealing with modules from the code that uses it (e.g. the code behind the menu). It simply asks for a list of all task extensions, and when the user selects a menu entry, it asks for the extension’s implementation. It doesn’t have to care if the task is in the same or in a different module that is already loaded or not. Loading a module is an asynchronous process, so to get an extension, you have to pass a callback that will be called when it is available (which can be immediately).
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public function showTask(taskId:String):void { // match the extension that provides the task we need const filter:Function = function(extension:ExtensionDescriptor):Boolean { return extension.document.@taskId == taskId; }; const point:ExtensionPoint = PluginManager.getInstance().getExtensionPoint("com.bsb.sf.Task"); // Callback function because getting an extension is an asynchronous process point.doWithFilteredExtensions(extensionCallback, filter); } private function myExtensionCallback(extensions:Array):void { if (extensions.length != 1) { log.error("Found " + extensions.length + " extensions providing the tasks instead of one."); // insert error handling here } const extension:Extension = extensions[0]; const task:ITask = ITask(extension.createInstance(extension.document.@task)); // now show the task (which is a UIComponent) ... } |
Conclusion
The Flex framework provides the basic foundations for a module mechanism. To build truly modular, performant, pluggable applications we made a layer on top of it using a double swf-swc compilation mechanism and extensions inspired by Eclipse. This turns out to be a nice fit: it cleanly abstracts the lazy asynchronous loading, and making many parts of application pluggable becomes easy.
We covered the basics here, but this opens up more opportunities: we take advantage of this mechanism to handle asynchronous initialization steps of modules, automatic loading of dependent modules and more.
[...] This post was mentioned on Twitter by Wouter Coekaerts and lejeunen, BSB labs. BSB labs said: [blog] Modularity in Flex enterprise applications. http://bit.ly/hyBgFo [...]
[...] http://labs.bsb.com/2011/02/modularity-in-flex-enterprise-applications/ [...]
I just skimmed the cemmonts, but it sounds like there is no definite answer to the question of whether classes can be excluded from a swf by using an *_exclude.xml file when published by Flash CS3. Since I used exclude files extensively in other projects, I have been dissappointed to find they aren’t working, or at least things are working as expected, in Flash CS3. I have ResourceLoader.fla, Resource1.fla, and Resource2.fla. Resource1 and Resource2 have a class named BasicButton compiled into them. ResourceLoader references the BasicButton, but ResourceLoader_exclude.xml contains the following:In Flash 7 and 8, BasicButton would NOT be compiled into the ResourceLoader.swf; however, in Flash 9 (CS3) BasicButton is compiled into the ResourceLoader.swf . What is the solution to this? I cannot switch production to using Flex. And, the getDefinitionByName method seems like it would be a horrible solution, with the loss of type checking or clunky, risky usage of casting.
I don’t have experience with *_exclude.xml files or depending on .fla files. What we do to avoid duplicating the classes from the dependencies in the swf that depends on it is by using the swcs as “external” dependencies. In Flex Builder, you can set that in the dialog where dependencies are configured. With the command line compiler that’s the
-compiler.external-library-pathoption, with Flexmojos it’s done by settingscopetoexternal.You set applicationDomain in the loaedrContext for the Loader that loads the SWF up. This way, it’ll partition the class definitions in the loaded SWF in a separate ApplicationDomain. Read up on ApplicationDomain, there are several options.Also, you can use -link-report=Classes.xml as a compiler option (not sure about FCS3) to generate a list of classes in a SWF, and then use -link-external=Classes.xml when compiling the other SWF to have it exclude any classes in the first SWF.This XML format is much better than _exclude.xml, it shows you how big each class is, what it extends, and what it depends on.That way, common classes can stay in the base SWF, or, if your modules need to be a little more portable, you can exclude the common classes from the base SWF.
One solution is to use getDefinitionByName() to avoid exliicpt references to the shared class. This prevents the shared class from being compiled into the module swf: code import flash.utils.*;var ClassReference:Class = getDefinitionByName( com.domain.SharedClassName ) as Class;var instance:Object = new ClassReference(); code However this is ugly. Excluding the class with a compiler setting would be nicer.
that’s what i do:just set the loaded swf to a new apicolatpindomain,and keep the shell class ref in module using Object or other flash global class, you can call the method as usual, but that’s no compiler type check when you are coding.