I have seen some posts where people asked about "dynamic module
loading" and did not find any possibility doing this. I am not talking
about lazy UI instantiation like it is done in Kitchensink example, but
about the loading of a script file of another module on demand. If you
still have an interest I will explain how can it be done.
For example we have two gwt-modules, which are compiled separately. One
is called MainModule.gwt.xml and another one TestModule.gwt.xml. I want
MainModule to load
TestModule after method MainModule.onModuleLoad() is called. To show
that TestModule is loaded, it will add certain text to special
placeholder of MainModule.
As we already known gwt.js script reads <meta> tags to identify which
modules need to be loaded. Than it loads "selector"-file for every
module and "selector"-file loads the script itself for certain local
and browser.
My idea was to force gwt.js to do the same work but with newely defined
custom meta tags. For that I removed original meta tags from document,
added new meta tags, which point to the model I want to be loaded and
started bootstraping again. As a result old modules, which are already
loaded, work as before and new module in loaded.
Below you will find source code that you can try by yourself.
Best regards,
Sergej
File com/sp/client/MainModule.gwt.xml
<module>
<inherits name="com.google.gwt.user.User"/>
<entry-point class="com.sp.client.MainModule"/>
</module>
File com/sp/client/public/MainModule.html
<html>
<head>
<meta name='gwt:module' content='com.sp.MainModule'>
<title>MainModule</title>
</head>
<body bgcolor="white">
<script language="javascript" src="gwt.js"></script>
</body>
</html>
File MainModule.java
/** ... main module, with place for injection ;-) */
package com.sp.client;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.RootPanel;
public class MainModule implements EntryPoint {
public void onModuleLoad() {
RootPanel.get().add(new HTML(" * <div id='injection_place'> *
"));
DynamicModuleLoader.loadModule("com.sp.TestModule");
}
}
File com/sp/client/TestModule.gwt.xml
<module>
<inherits name="com.google.gwt.user.User"/>
<entry-point class="com.sp.client.TestModule"/>
</module>
File TestModule.java
/** ... test module, to be loaded and injected */
package com.sp.client;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.RootPanel;
public class TestModule implements EntryPoint {
public void onModuleLoad() {
RootPanel.get("injection_place").add(new HTML("This text is
inserted by TestModule"));
}
}
File DynamicModuleLoader
/** ... module loader class, which performs the loading */
package com.sp.client;
public class DynamicModuleLoader {
/** ... tested with GWT 1.1.10, Firefox */
public static native void loadModule(String moduleName)
/*-{
var __top = $wnd.top;
var __doc = __top.document;
// remove already loaded meta data
var metas = __doc.getElementsByTagName("meta");
for (var i = 0, n = metas.length; i < n; i++) {
var meta = metas[i];
var name = meta.getAttribute("name");
if (name == "gwt:module") {
meta.maskedName = name;
meta.name = "dummy"
}
}
// add new meta tag for module to be loaded
var meta = __doc.createElement("meta");
meta.name = "gwt:module";
var index = __top.location.href.indexOf($moduleName);
var base = __top.location.href.substring(0, index);
meta.content = base + moduleName + "/" + moduleName;
__doc.getElementsByTagName("head")[0].appendChild(meta);
// load module
__top.__gwt_moduleControlBlocks = new
__top.ModuleControlBlocks();
__top.__gwt_bootstrap();
}-*/;
}
I wonder if we can get some attention from the GWT team to validate
this approach or point out the risks.
1) I notice you are removing existing metadata tags. I suspect that
this has no negative impact since the specified modules are already
loaded by then.
2) I notice as well that you are creating a new instance of
ModuleControlBlocks(); From looking at the gwt.js code, this looks like
it would wipe out the dynamicResources, which are marked at static (in
a comment). We might presume that such resource would also have been
loaded before this code executes.
3) The bootstrap code hooks an onunload handler to cleanup. I think the
way it's done is safe since it chains existing calls.
4) The big question for me is can we force reclamation (unloading) by
running window.onunload, any registered WindowCloseListener and
removing the iframe hosting a particular module?
Thanks for a clever post. I'd love to hear optinions from other readers
about the viability/risks of this approach. As far as I can tell, it
looks relatively safe.
Thank you for this... I was very happy to see this, as it was just what
I needed for my project.
Unfortunatly after testing I noticed it's not working as intended.
If you compile and run this project you will see that the text "This
text is
inserted by TestModule" is *not* inserted at the div between the * *
but after.
This is because the RootPanel in the MainModule and the TestModule are
not of the same instance.
RootPanel.get("injection_place") in the TestModule does therefor not
return the correct panel... you might as well just do RootPanel.get(),
because the RootPanel in TestModule doesn't know of "injection_place"
So it seems it doesn't work to share data between the modules.
What I wanted was a ModuleManager singleton that I could access from
both the MainModule and the other Modules, but atm this does not work
as each module creates its own instance of the class.
Is there a way to make this work? A way to communicate between the
MainModule and Modules? I don't truly understand how everything works
together in GWT, so it would be very nice if anyone could comment on
this.
>inserted by TestModule" is *not* inserted at the div between the * * but after.
To fix this problem you just need to add a closing div-tag (like this
one "<div id='injection_place'></div>") in MainModule.
>So it seems it doesn't work to share data between the modules.
Hmm... Yes and no. You surely can share the objects of the data-types
which are native for JavaScript. The idea here is to create a shared
instance of, for example, JavaScript Array and to access it from both
modules. For instance you can add two methods like these two below to
DynamicModuleLoader class.
public static native Object get(String name)
/*-{
var sharedArray = $wnd.top.__sergej_sharedArray;
return sharedArray == null ? null : sharedArray[name];
}-*/;
public static native void put(String name, Object value)
/*-{
var __top = $wnd.top;
// lazy create shared array
if (__top.__sergej_sharedArray == null)
__top.__sergej_sharedArray = new Array();
// store value
__top.__sergej_sharedArray[name] = value;
}-*/;
Now you are able to force MainModule to put some parameters into shared
array and TestModule to read them. You could write something like this:
MainModule:
public void onModuleLoad() {
RootPanel.get().add(new HTML(" *<div
id='injection_place'></div>*"));
// preconfigure TestModule
DynamicModuleLoader.put("com.sp.TestModule.InjectionPlace",
"injection_place");
DynamicModuleLoader.put("com.sp.TestModule.ButtonText", "Click
me");
// load test module
DynamicModuleLoader.loadModule("com.sp.TestModule");
}
TestModule:
public void onModuleLoad() {
final String text =
DynamicModuleLoader.get("com.sp.TestModule.ButtonText") + " ";
final Button button = new Button(text);
button.addClickListener(new ClickListener() {
private int m_counter;
public void onClick(Widget sender) {
button.setText(text + ++m_counter);
}}
);
String id = (String)
DynamicModuleLoader.get("com.sp.TestModule.InjectionPlace");
RootPanel.get(id).add(button);
}
After you compile both modules you can run them and the modules will
use shared strings. Of course it also works in the opposite direction
(TestModule puts and MainModule reads)
The problems start after you try to share the instances of an user
defined class. You will be able to put data into the shared place, but
it will not be possible to use it within another module. It was
possible in my tests to read data by another module but the class
casting has always failed. IMHO there are at least two problems:
1. Every GWT module contains a "type casting table" which defines the
casting rules for all classes, which are compiled in this module. The
classe types in this table are represented by integer values (type_id),
which are assigned during compilation. Thus the same classes can have
different type IDs in two different modules. As a result type casting
will fail.
2. Code obfuscation might assign two different "obfuscated" names to
the same classes (class fields) within two different modules. As a
result second module will not be able to retrieve data from a user
defined class.
Perhaps some time the google guys will think about how to load common
classes (like java.util implementation etc.) only once and to share
this loaded scripts between other modules. But this is different story
with own problems ;)
Summarizing if you don't find yourself familier with all this I would
suggest you to use this kind of dynamic loading for study purposes only
to avoid problems with your project.
br
s.