New Odnoklassniki frontend: launching React in Java. Part II

We continue the story of how inside Odnoklassniki using GraalVM we managed to make friends with Java and JavaScript and start migrating to a huge system with a lot of legacy code.

In the second part of the article, we will talk in detail about the launch, assembly and integration of applications on the new stack, dive into the specifics of their work both on the client and on the server, as well as discuss the difficulties encountered on our way and describe the solutions that help them overcome .

If you have not read the first part, I highly recommend doing this. From it you will learn about the history of the frontend in Odnoklassniki and get acquainted with its historical features, go through the path of finding a solution to the problems that have accumulated in us over 11 years of the project, and at the very end you will plunge into the technical features of the server implementation of the decision we made.

UI configuration

To write the UI code, we chose the most advanced tools: React along with MobX, CSS Modules, ESLint, TypeScript, Lerna. All this is collected using Webpack.

Application architecture

As was written in the previous part of this article, in order to implement gradual migration, we will insert new components on the site in DOM elements with custom names that will work inside the new UI stack, while for the rest of the site it will look like a DOM element with its API. The contents of these elements can be rendered on the server.

What is it? Inside there is a cool, trendy, modern MVC application running on React and providing the standard DOM API outward: attributes, methods on this DOM element, and events.

To run such components, we have developed a special mechanism. What is he doing? Firstly, it initializes the application according to its description. Secondly, it binds the component to the specific DOM node in which it starts. There are also two engines (for the client and for the server) that can find and render these components.

Why is this needed? The fact is that when the whole site is made on React, then usually the site component is rendered into the root element of the page, and this component does not matter what is outside, but only what is inside is interesting.

In our case, everything is more complicated: a number of applications need the opportunity to tell our page on the site “I am, and something is changing in me.” For example, the calendar needs to throw an event that the user clicked on the button, and the date has changed, or outside you need the ability so that inside the calendar you can change the date. For this, the application engine implements facades in the basic functionality of the application.

When delivering a component to a client, it is necessary that the engine of the old site can launch this component. To do this, during the build, the information necessary for its launch is collected.

{
    "events-calendar": {
        "bundleName": "events-calendar",
        "js": "events-calendar-h4h5m.js",
        "css": "events-calendar-h4h5m.css"
    }
}

Special markers are added to the attributes of the component tag, which say that this application is of a new type, its code can be taken from a specific JS file. At the same time, it has its own attributes that are needed to initialize this component: they form the initial state of the component in the store.


For rehydration, not a cast of the application state is used, but attributes, which allows saving on traffic. They come in a normalized form, and, as a rule, are smaller than the store that the application creates. At the same time, the time to recreate the store from the attributes on the client is short, so they can usually be neglected.

For example, for the calendar, the attributes only have a highlighted date, and the store already has a matrix with full information for the month. Obviously, it is pointless to transfer it from the server.

How to run the code?

The concept was tested on simple functions that either give a line for the server or write innerHTML for the client. But in real code there are modules and TypeScript.

There are standard solutions for the client, for example, collecting code using Webpack, which itself grinds everything and gives it to the client in the form of a bundle of bundles. And what to do for the server when using GraalVM?

Let’s consider two options. The first is to type TypeScript in JavaScript, as they do for Node.js. This option, unfortunately, does not work in our configuration when JavaScript is a guest language in GraalVM. In this case, JavaScript does not have a modular system, or even asynchrony. Because modularity and work with asynchrony provides a specific runtime: NodeJS or a browser. And in our case, the server has JavaScript that can only execute code synchronously.

The second option – you can simply run on the server code from the same files that were collected for the client. And this option works. But there is a problem that the server needs other implementations for a number of methods. For example, the renderToString () function will be called on the server to render the component, and ReactDOM.render () on the client. Or another example from the previous article: to obtain texts and settings on the server, the function that Java provides will be called, and on the client it will be an implementation in JS.

As a solution to this problem, you can use aliases from Webpack. They allow you to create two implementations of the class we need: for the client and server. Then, in the configuration files for the client and server, specify the appropriate implementation.

But two config files are two assemblies. Each time, collecting everything separately for the server and for the client is long and difficult in support.

You need to come up with such a configuration so that everything is collected in one go.

Webpack configuration to run JS on server and client

To find a solution to this problem, let’s see what parts the project consists of:

Firstly, the project has a third-party runtime (vendors), the same for the client and for the server. It almost never changes. Rantime can be given to the user, and he will be cached on the client until we update the version of the third-party library.

Secondly, there is our runtime (core), which ensures the launch of the application. It has methods with different implementations for the client and server. For example, getting localization texts, settings, and so on. This runtime also changes infrequently.

Thirdly, there is a component code. It is the same for the client and for the server, which allows you to debug the application code in the browser without starting the server at all. If something went wrong on the client, you can see the errors in the browser console, bring everything to mind and be sure that there will be no errors when starting on the server.

In total, three parts are obtained that need to be assembled. We want:

  • Separately configure the assembly of each of the parts.
  • Put dependencies between them so that each part does not fall into what is in the other.
  • Collect everything in one pass.

How to describe separately the parts of which the assembly will consist? There is a multiconfiguration in webpack: you just give away an array of exports of the modules included in each part.

module.exports = [{
  entry: './vendors.js',
}, {
  entry: './core.js'
}, {
 entry: './app.js'
}];

Everything would be fine, but in each of these parts the code of those modules on which this part depends will be duplicated:

Fortunately, the basic set of webpack plugins has Dllplugin, which allows for each assembled part to get a list of the modules included in it. For example, for vendor, you can find out which specific modules are included in this part.

When building another part, for example, core libraries, we can say that they depend on the vendor part.

Then, during the webpack assembly, DllPlugin will see the dependence of core on some library that is already in vendor, and will not add it to core, but simply put a link to it.

As a result, three pieces are assembled at a time and depend on each other. When the first application is downloaded to the client, the runtime and core libraries will be saved in the browser cache. And since Odnoklassniki is a site, the tab with which the user can open “forever”, crowding out will occur quite rarely. In most cases, with releases of new versions of the site, only the application code will be updated.

Resource Delivery

Consider the problem by the example of working with localized texts that are stored in a separate database.

If earlier somewhere on the server you needed text in the component, you could call the function to get the text.

const pkg = l10n('smiles');

 
    Текст: { pkg.getText('title') }

Getting text on the server is not difficult, because the server application can make a quick request to the database or even cache all texts in memory.

How to get texts in components on a react that are rendered on a server in GraalVM?

As discussed in the first part of the article, you can add methods to the global object in the JS object that you want to access from JavaScript. It was decided to make a class with all methods available for JavaScript.

public class ServerMethods {
    …
    
    /**
     * Получаем текст в виде строки
     */
    public String getText(String pkg, String key) {
        …
    }
    
    …
}

Then put an instance of this class in the global JavaScript context:

// добавляем объект с методами Java в поле контекста
js.putMember("serverMethods", serverMethods);

As a result, from JavaScript in the server implementation, we simply call the function:

function getText(pkg: string, key: string): string {
    return global.serverMethods.getText(pkg, key);
}

In fact, this will be a function call in Java that will return the requested text. Direct synchronous interaction and no HTTP calls.

On the client, unfortunately, it takes a very long time to go over HTTP and receive texts for each call to the text insertion function in the components. You can pre-download all the texts to the client, but the texts alone weigh tens of megabytes, and there are other types of resources.

The user will get tired of waiting until everything is downloaded from him before starting the application. Therefore, this method is not suitable.

I would like to receive only those texts that are needed in a particular application. Our texts are broken into packages. Therefore, you can collect the packages needed for the application and download them along with the bundle. When the application starts, all texts will already be in the client cache.

How to find out which texts an application needs?

We entered into an agreement that packages of texts in the code are obtained by calling the l10n () function, into which the package name is transmitted ONLY in the form of a string literal:

const pkg = l10n('smiles');

 
    { pkg.getLMsg('title') }

We wrote a webpack plugin that, by analyzing the AST tree of component code, finds all the calls to the l10n () function and collects package names from the arguments. Similarly, the plugin collects information about other types of resources needed by the application.

At the output after the assembly for each application, we get a config with its resources:

{
    "events-calendar": {
       "pkg":  [
           "calendar",
           "dates"
       ],
       "cfg":  [
           "config1",
           "config2"
       ],
       "bundleName":  "events-calendar",
       "js":  "events-calendar.js",
       "css":  "events-calendar.css",
    }
}

And of course, we must not forget about updating the texts. Because on the server all texts are always up-to-date, and the client needs a separate cache update mechanism, for example, watcher or push.

Old code in new

With a smooth transition, the problem arises of reusing the old code in new components, because there are large and complex components (for example, a video player), rewriting of which will take a lot of time, and you need to use them in the new stack now.

What are the problems?

  • The old site and new React apps have completely different life cycles.
  • If you paste the code of the old sample inside the React application, then this code will not start, because React does not know how to activate it.
  • Due to different life cycles, React and the old engine may simultaneously try to modify the contents of the old code, which can cause unpleasant side effects.

To solve these problems, a common base class was allocated for components containing old code. The class allows heirs to coordinate the life cycles of React and old-style applications.

export class OldCodeBase extends React.Component {

    ref: React.RefObject = React.createRef();

    componentDidMount() {
        // ЗАПУСК активации при появлении компонента в DOM
        this.props.activate(this.ref.current!); 
    }

    componentWillUnmount() {
        // ЗАПУСК деактивации при удалении компонента из DOM
        this.props.deactivate(this.ref.current!); 
    }

    shouldComponentUpdate() {
        // React не должен модифицировать старый код, 
        // появившийся внутри React-приложения. 
        // Поэтому необходимо запретить обновление компонента.
        return false;
    }

    render() {
        return (
            
); } }

The class allows you to either create pieces of code that work in the old way, or destroy, while there will be no simultaneous interaction with them.

Paste old code on the server

In practice, there is a need for wrapper components (for example, pop-ups), the contents of which can be any, including those made using old technologies. You need to figure out how to embed any code on the server inside such components.

In a previous article, we talked about using attributes to pass parameters to new components on the client and server.


And now we still want to insert a piece of markup there, which in meaning is not an attribute. For this, it was decided to use a system of slots.


    
         old component

As you can see in the example above, inside the code of the cool-app component, the old-code slot containing the old components is described. Then, inside the react component, the place where you want to paste the contents of this slot is indicated:

render() {
    return (
         
            
        
); }

The server engine renders this react component and frames the contents of the slot in a tag by assigning the data-part-id = “old-code” attribute to it.


     
        
            old code