How we sawed the monolith. Part 2, Frame Manager

Hi, my name is Stas, I work in the Tinkoff Business team. In the last article, my colleague Vanya told how our application architecture is arranged . Several times Vanya mentioned a certain Frame Manager, which serves as an application orchestrator, and now I will tell you about it in more detail.



image



What is Tinkoff Business



Tinkoff Business offers solutions for small and medium-sized businesses: a salary project, cash and settlement services, a document designer and about 20 other products.

All this is implemented in applications. These applications are developed by separate teams and have their own release cycles. And all of these applications work with a single authorization, contain a common part of business logic in a separate library, and use common UI components.



Let's go back 2 years ago



A typical Tinkoff Business application looked something like this:



image



Above - a header with navigation through the application, and on the right - a sidebar with product navigation.

Back then, the idea of ​​a microfront was not yet so popular, but we were already moving in this direction: the sidebar was a separate Angular application. The main application loaded the sidebar into an iframe, which allowed the application to be released independently.



This approach had its drawbacks: when switching between products, you had to wait for a full page load with two Angular applications. And because most applications use the same backend API requests, users had to wait for them to re-execute.



Frame Manager idea



We lived with such an architecture until a global task for all applications appeared - redesign. Then the idea came up: why not do some kind of inversion of control and instead of applications loading the sidebar inside themselves, the sidebar would load applications itself?



This made it possible to preserve the advantages of the current architecture and get rid of the above problems (and bring new ones, haha).



Initial prototype



Initially, we created a prototype with the minimum functionality required to load other applications. A separate domain was created on the test bench. The routes in nginx for application statics have changed: earlier , the statics of the corresponding applications were loaded along the paths / sme , / account , / salary , etc. Now the Frame Manager statics were sent along all paths, and the postfix / static was added to the static routes of the applications themselves .



To make it clearer, let's look at an example: you need to load an application located on the path / some-app with some route some-route . Leaving the details aside, let's see what process happens when loading / some-app / some-route / :

  1. Nginx sends Frame Manager statics along this path.
  2. Frame Manager is loading. Based on the route, it understands to load some-app .
  3. An iframe is created with src = '/ some-app / static /' where the main app is loaded.


At the same time, significant improvements were required in the applications themselves. Therefore, we forked the master of the application branch and added the necessary changes there, after which we raised individual copies of the applications themselves with the changes made.



First problems



So we transferred 4 applications to Frame Manager and made sure that the solution is working. All other applications were to be translated. And here we ran into a problem: it turned out to be too expensive to simultaneously maintain the regular version and the version for working with Frame Manager.



We had to constantly update new versions of applications for each change to the master of older versions, resolve emerging conflicts, existing functionality often broke, the cost of regression testing almost doubled - all this took too much time. It was clear that a new solution was needed.



Improvements



If one version of the app could work with both the sidebar and the Frame Manager, that would save us a lot of problems. Let's see what can be done.



First of all, you need to somehow determine whether the application is running in the Frame Manager. This is quite simple: you need to compare the references of window.top and window.self. If they are not equal, then we are in a frame, that is, in the Frame Manager! But, if there are applications that open in an iframe by default, you need to add additional logic. So, we had a widget application that initially opened in a frame and began to assume that it is always in the Frame Manager, which is why it broke in the old mode.



Now let's take a closer look at what changes are needed in applications and how you can support work in two modes:

  1. url. iframe, . , - , β€” . url’ Frame Manager, . .

  2. . Frame Manager’ . : . . , , , post messages custom events. Frame Manager.

  3. . , Frame Manager. , Angular .

  4. . , , TCS, config.js . Frame Manager .

  5. base href. nginx, base href ( /static/). : , base href , . , , , , base href, .

  6. Authorization. For authorization in all applications, a separate script is used, which is embedded in index.html. In the new version, this script is embedded in the Frame Manager, and its reuse in applications will lead to errors. You can change the script logic so that it is ignored if the application is loaded inside the Frame Manager.



These are all working solutions, but they are not flexible enough. New branches have been added with logic that also needs to be maintained in different places. In general, everything looks like an overcomplicated and rather unstable structure.



Reinventing the iframe



Then I got the idea to hack the process of loading the index.html application a little. Instead of loading the application into an iframe specifying the src attribute, you can make an xhr request for index.html, get the page in text form, process it, and load it into the iframe. This will give complete control over the loaded application: it will allow you to define base href, remove unnecessary scripts, patch styles, override variables and much more!



Yes, mokey patching is discouraged by developers and is considered bad practice, but if the Angular team uses it in the zone.js library, how are we worse? There can be doubts about performance: parsing html looks like an expensive operation. But, as a rule, the start page of an Angular application does not exceed 50 lines, and in all browsers (even IE 10!) There is a convenient apiDOMParser , which allows you to get the DOM from a string.



Let's take a look at what the Frame Manager does while loading the application (the Frame Manager itself is already loaded):

  1. Based on the path, loads the index.html of the application.

  2. Parses the page, converting it to DOM, removes unnecessary scripts in memory, replaces base href, global variables with configs and styles.

  3. Creates an iframe element that writes the resulting document (converted back to a string) using document.write ().
  4. Puts the application a route to which it should be wired. It also feeds the models necessary for the business logic to work through the data exchange service.



Thus, of the above six necessary changes in the logic, only the first one (url synchronization) needs to be implemented inside the application, the rest is taken over by the Frame Manager!



What got



We completely changed the appearance of the application, practically without making any changes to the code of the application itself.

Before. The sidebar is circled in red. Embedded in an iframe
sidebar



After. Frame Manager is highlighted in red. App loaded in iframe
frame manager



Got the ability to override or add global variables and styles.

For example, this is how the style config for the application looks like
export const business = {
    'sidebar.b-main__sidebar': {
        display: 'none'
    },
    '.b-main': {
        'margin-left': '260',
        position: 'relative',
        display: 'block',
        width: '1104px',
        'min-height': '100vh',
        margin: '0 auto'
    }
};




And so - the config of the application itself
{
        id: 'products',
        name: ' ',
        icon: 'products',
        frameSupported: true,
        applications: [
            {
                id: 'products',
                path: '/products',
                apiPrefix: '/products',
                hasMenuConfig: true,
                dynamicCompanyChange: true,
            }
        ]
    }




At the same time, the configs are in a repository separate from the Frame Manager, which allows you to change some parameters of the application's work without releasing.



We also created seamless transitions between applications, made authorization in Frame Manager. We have achieved that due to data sharing between Frame Manager and applications, unnecessary requests are not made.



Not without problems: some chrome plugins (CryptoPro, redux devtools) stopped working in the downloaded application, as the link to window was lost during interaction. Additional improvements were required.



As a result, at the end of 2019, we successfully transferred all applications to Frame Manager, and the sidebar has sunk into oblivion. But work on the Frame Manager continued, and a new question arose: is it possible to somehow improve and optimize the work of the frontend in Tinkoff Business? It turned out that you can! But more on that in the next article.



All Articles