Microservice architecture has long become the de facto standard in the development of large and complex systems. It has a number of advantages: it is a strict division into modules, and weak connectivity, and resistance to failures, and the gradualness of output in production, and independent versioning of components.
True, often, speaking of microservice architecture, only the backend architecture is mentioned, and the frontend both was and remains monolithic. It turns out that we made a great back, and the front pulls us back.
')
Today I will tell you how we did the microservice front in our SaaS solution and what problems we encountered.
Problematics
Initially, the development in our company looked like this: there are many teams involved in the development of microservices, each of which publishes its own API. And there is a separate team that develops a SPA for the end user, using API of different microservices. With this approach, everything works: the developers of microservices know everything about their implementation, and the SPA developers know all the subtleties of user interactions. But there was a problem: now every frontender must know all the details of all microservices. Microservices become more and more, front-end vendors become more and more - and Agile begins to fall apart, as there is a specialization within the team, that is, interchangeability and universality disappear.
So we came to the next stage - modular development. The frontend team is divided into subcommands. Each was responsible for its own part of the application. It has become much better, but over time this approach has exhausted itself for several reasons.
- All modules are heterogeneous, with their own specifics. For each module, their technology is better suited. At the same time, the choice of technologies is a difficult task in the conditions of the SPA.
- Since the SPA application (and in the modern world this means a compilation into a single bundle or at least an assembly), only outputs of the entire application can be done at the same time. The risk of each issue is growing.
- It's harder to manage dependencies. Different modules need different (possibly specific) versions of dependencies. Someone is not ready to switch to the updated dependency API, but someone cannot make a feature due to bugs in the old dependency branch.
- Due to the second point, the release cycle of all modules must be synchronized. Everyone is waiting for lagging behind.
We cut the frontend
The moment of accumulation of critical mass came, and the frontend was decided to divide into ... frontend microservices. Let's define what a frontend microservice is:
- completely isolated part of the UI, in no way dependent on others; radical isolation; literally developed as a separate application;
- Each frontend microservice is responsible for a certain set of business functions from beginning to end, that is, it is fully functional in itself;
- can be written on any technology.
But we went further and introduced another level of division.
Fragment concept
We call a fragment a bundle consisting of
js + css +
. In essence, this is an independent part of the UI that must fulfill a set of design rules in order for it to be used in a shared SPA. For example, all styles should be as specific as possible for a fragment. There should be no direct interaction with other fragments. You must have a special method to which you can pass a DOM element where the fragment should be drawn.
Thanks to the handle, we can save information about all registered fragments of the environment, and then have access to them by ID.
This approach allows you to place two applications written in different frameworks on one page. It also allows you to write a universal code that allows you to dynamically load the necessary fragments on the page, initialize them and manage the life cycle. For most modern frameworks, it is enough to follow the "rules of hygiene" to make this possible.
In cases where the fragment does not have the ability to “cohabit” with others on the same page, there is a fallback scenario in which we draw the fragment in an iframe (solving related problems is beyond the scope of this article).
All a developer needs to do if he wants to use an existing fragment on a page is:
- Connect the microservice platform script to the page.
<script src="//{URL to static cache service}/api/v1/mui-platform/muiPlatform.js"></script>
- Call the method of adding a fragment to the page.
window.MUI.createFragment(
Also for communication between fragments there is a bus built on
Observable
and
rxjs
. It is written on NativeJS. In addition, the SDK provides wrappers for various frameworks that help to use this bus natively. The example for Angular 6 is a utility method that returns
rxjs/Observable
:
import {fromEvent} from "@netcracker/mui-platform/angular2-factory/modules/shared/utils/event-utils" fromEvent("<event-name>"); fromEvent(EventClassType);
In addition, the platform provides a set of services that are often used by different fragments and are basic in our infrastructure. These are services such as localization / internationalization, authorization service, work with cross-domain cookies, local storage and much more. Wrappers for various frameworks are also supplied for use in the SDK.
We merge frontend
For example, we can consider this approach in the SPA admin (it combines various possible settings from different microservices). We can make the contents of each bookmark a separate fragment, which will be supplied and developed by each microservice separately. Thanks to this, we can make a simple “cap” that will show the corresponding microservice when clicking on a bookmark.
Develop the idea of ​​a fragment
The development of one bookmark with one fragment does not always allow to solve all possible tasks. It is often necessary in one microservice to develop a certain part of the UI, which will then be reused in another microservice.
And here fragments help us too! Since all that the fragment needs is a DOM element for rendering, we give any microservice a global API through which it can place any fragment inside its DOM tree. To do this, simply pass the fragment ID and the container in which it needs to be drawn. The rest will be done by itself!
Now we can build a “matryoshka” of any nesting level and reuse entire UI pieces without the need for support in several places.
It often happens that there are several fragments on one page that should change their state when some general data on the page changes. To do this, they have a global (NativeJS) event bus through which they can communicate and respond to changes.
Shared services
In the microservice architecture, central services inevitably appear, the data from which everyone else needs. For example, a localization service that stores translations. If each microservice separately begins to climb behind this data to the server, we will receive just a shaft of requests during initialization.
To solve this problem, we developed implementations of NativeJS services that provide access to such data. This made it possible not to make unnecessary requests and cache data. In some cases, even in advance to display such data on the page in HTML, in order to completely get rid of requests.
In addition, wrappers over our services for different frameworks were developed to make their use very natural (DI, fixed interface).
Pluses of frontend microservices
The most important thing that we get from the division of a monolith into fragments is the possibility of choosing technologies by each team individually and transparent dependency management. But in addition, it gives the following:
- very clearly defined areas of responsibility;
- independent outputs: each fragment can have its release cycle;
- increasing the stability of the solution as a whole, since the issuance of individual fragments does not affect others;
- the ability to easily roll back features, roll them to the audience in part;
- the piece fits easily into each developer’s head, which leads to real
interchangeability of team members; In addition, each fender can more deeply understand all the subtleties of interaction with the corresponding backend.
The solution with a microserse frontend looks good. After all, now every fragment (microservice) can decide for itself how to deploy: whether you just need nginx to distribute statics, full-fledged middleware to aggregate requests to backups or support websockets, or some other specificity in the form of binary data transfer protocol inside http. In addition, fragments can choose their own assembly methods, optimization methods and so on.
Cons of frontend microservices
You can never do without a fly in the ointment.
- The interaction between fragments cannot be provided by standard tube methods (DI, for example).
- How to deal with common dependencies? After all, the size of the application will grow by leaps and bounds, if they are not taken out of the fragments.
- For routing in the final application, one should still be responsible.
- What to do if one of the fragments is unavailable / cannot be drawn.
- It is not clear what to do with the fact that different microservices can be on different domains.
Conclusion
Our experience with this approach has proven its viability. The speed of displaying features in production has increased significantly. The number of implicit dependencies between parts of the interface has been reduced to almost zero. We got a consistent UI. You can safely test the features without involving a large number of people.
Unfortunately, in one article it is very difficult to highlight the whole range of problems and solutions that can be found along the way of repeating such an architecture. But for us, the pros clearly outweigh the cons. If Habr shows interest in disclosing details of the implementation of this approach, we will definitely write a sequel!