Embed Google Web Toolkit (GWT) apps on a React website

Posted on August 19, 2020 in programming

Google Web Toolkit, also known as GWT, is a technology by Google that allows you to create sophisticated web applications using the Java programming language on the frontend. While I don't feel it makes sense to start a new project using GWT for situations like this, you may find yourself in a circumstance where embedding an old GWT application on your new React website makes sense.

In my case, I created numerous games with the framework LibGDX years ago during high school, and I wanted to have a playable version of them on my website. The web-exported versions of the game use GWT to create JS bundles. While writing web games using a web game creation framework would make the most sense for a new project, for cross-platform games using a framework like this may still make sense today.

Changes to the GWT Application

GWT Applications can be large and have generally different requirements to React applications. Due to this, it may make sense to store the GWT application separately to the React application. For this to work, a few things need to change within the GWT Application's code itself.

It would help if you first decided how you wish to host these. For this article, I'm going to use the example of hosting in an S3 bucket attached to a cdn.hostname.com domain. The instructions will be nearly identical for most other setups, however.

CrossOrigin Images

If your GWT Application makes use of images, any image elements created in code must have the crossOrigin attribute set to anonymous. Doing this allows fetching images from another domain through the use of CORS.

This is done by setting image.setAttribute("crossOrigin", "anonymous"); on your ImageElement instance before setting the source.

Use the correct URLs

GWT includes a few different methods of getting the URL of the application. The two most common are GWT.getHostPageBaseURL and GWT.getModuleBaseURL. When hosting an application at a different location to where it is displayed, the distinction between these two method calls is crucial.

When referencing assets or files that are inside the GWT application, the module base URL is the one to use. When referencing the page that the user is using the application from, the host page base URL is the one to use. Using the wrong URL here will mean the application can't find files.

Usage with LibGDX

If you want to use this with LibGDX, I've already contributed changes to a GWT LibGDX backend to support this. Click here to check it out.

Hosting the Application

To host the GWT application from an S3 bucket, first, create a new bucket using the hostname. For example, if you wanted to host it from cdn.website.com, you'd create a bucket called cdn.website.com. From here, upload the GWT application into a directory and make a note of the file path.

You want to find the <moduleName>/<moduleName>.nocache.js file. In the case of LibGDX applications, the module name is generally html, so the file path would be html/html.nocache.js.

After uploading the files, you want to make sure to enable CORS in your hosting service. For AWS S3 you can use a simple CORS configuration such as the following while testing, however, it is strongly recommended to limit it down to specific origins.

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
</CORSRule>
</CORSConfiguration>

Setting up the React application

This article assumes you already have a React application setup, and want to embed a GWT application into it.

A simple hook to add a GWT application is as follows,

const useScript = (url) => {
  useEffect(() => {
    const script = document.createElement('script');

    script.src = url;
    script.async = false;

    document.body.appendChild(script);

    return () => {
      document.body.removeChild(script);
    };
  }, [url]);
};

This hook adds a script to the page and removes it from the page when navigating away. It can add the <moduleName>.nocache.js file to start the application.

useScript('https://cdn.website.com/SomeApp/html/html.nocache.js');

Cleaning up after the application

While the above will work, it does not correctly clean up after the application. As different applications will create different HTML tags, this exact process will differ per application. However, there are a few items that should almost always need cleaning up.

Most GWT applications create an iframe to run the application inside. If you do not clean this up, the application will keep running after navigating away. The <moduleName>.nocache.js file also adds another file, following a <hash>.cache.js pattern.

To also clean this up, I've created a second hook.

const useGWTScript = (url, badScriptPattern, moduleName) => {
  useEffect(() => {
    const script = document.createElement('script');

    script.src = url;
    script.async = false;

    document.body.appendChild(script);

    return () => {
      document.body.removeChild(script);

      const iframe = document.querySelector(`iframe#${moduleName}`);
      if (iframe) {
        document.body.removeChild(iframe);
      }

      const badScripts = [];

      for (let i = 0; i < document.scripts.length; i++) {
        const scriptElement = document.scripts.item(i);
        if (scriptElement && badScriptPattern.test(scriptElement.src)) {
          badScripts.push(scriptElement);
        }
      }

      badScripts.forEach((scriptElem) =>
        scriptElem.parentNode?.removeChild?.(scriptElem)
      );
    };
  }, [url]);
};

This hook is used instead of the previous useScript hook and is called like this.

useGWTScript(
    'https://cdn.website.com/SomeApp/html/html.nocache.js',
    /https\:\/\/cdn.website.com\/SomeApp\/html\/(.*).cache.js/,
    'html'
  );

Preventing keyboard navigation

If you're trying to embed an application that makes use of the keyboard, it may be a good idea to prevent keyboard interaction with the main web page. I've created the following hook to do this,

const useKeyJail = () => {
  useEffect(() => {
    const listener = (event) => {
        console.log(event.keyCode);
        if ([32, 33, 34, 35, 36, 37, 38, 39, 40].includes(event.keyCode)) {
            event.preventDefault();
            return false;
        }
    };

    document.addEventListener('keydown', listener);

    return () => {
      document.removeEventListener('keydown', listener);
    };
  }, []);
};

This hook prevents the primary page navigation keys from working. This list includes the space bar, page up and down keys, end, home, and the arrow keys. You may need to tweak this for your application.

Conclusion

While not a great idea for a new project, hosting a GWT application on a React website can help preserve old applications for future use. It's worth noting that while this setup has worked for me, all applications are different and you may need to alter some or all of these steps to have it work for you. For games created with LibGDX, this setup appears to work fine. I have this exact setup in use currently on a few of my older Ludum Dare games, such as Triage or DropDown.

If you find something that could be useful to know when embedding GWT applications on a React site, please let me know, and I'll look at including it in this post.

Share this with others!
Post Tags

Other Posts

Demystifying a TypeScript quirk

Posted on February 01, 2020

I recently read Asana's blog post on TypeScript quirks and took particular interest in the first TypeScript quirk they mention. While it may seem like an inconsistency, the way the type system behaves here is entirely logical.

Exceptions as Flow Control in Java

Posted on December 27, 2016

Exceptions are commonly used for flow control in Java, but how well do they perform compared to return values?

Optimizing data-heavy JavaScript code

Posted on March 01, 2020

JavaScript may not seem to be the ideal language to manipulate large amounts of data. This post goes over a few key problem areas and how you can avoid them.