Jet frontend. The story of how we rewrote everything again

    Hi, this is Katya from Yandex.Money again. I continue my story about how I stopped making up and started to live. In the first part, I told how I was brought here and what our front-tenders are doing. Today it’s about the front-end stack, where the React is from and where BEM went.

    Spoiler: BEM has not gone anywhere yet ¯ \ _ (ツ) _ / ¯. Let's go!



    Attention: high concentration of frontend. A lot of text, pictures and code, as promised.

    Part 2. About technology


    Faraway 2016. Trying to write on React, it is pretty bearable. I still do not suspect that in a year I will be transferring entire services to React. 2017 starts in Yandex.Money, I have a BEM brain begins, and I still do not suspect.

    Backend on Node.js, my first time


    To get acquainted with the project, a new developer receives a test task. I was lucky: I had this task from backlog. And on the very first day I encountered Node.js.

    The front-end Yandex.Money is responsible not only for the client side, but also for the server stratum in the form of a Node.js application. The application's task is to orchestrate data from a Java backend for preparation in view-oriented form, as well as server-side rendering and routing. I would say this a couple of years ago, I would not understand anything, and everything is quite simple: when a request comes from the browser to the server, Node.js generates HTTP requests for the backend, receives the necessary data and templates the web pages. We use Express as a server framework , and for developing internal applications without a legacy we decided to useKoa2 . The developers loved the framework design, and we decided not to downgrade to Express, so Koa2 remained on the stack. But we are not rolling out Koa2 code to external users: the framework does not have sufficient support, but there are open vulnerabilities.

    We already wrote about the place of Node.js in our front end, but since then something has changed. Node.js 8 has become LTS and is already running on our production servers. We also want to abandon the Nginx servers, which we raise on each host to distribute statics - they will be replaced by separate servers with Nginx, and sometime with CDNs.

    To fumble the code between projects, but not to post it in open access, we use a whole set of tools: we store the modules in Bitbucket and assemble them in Jenkins. We also use the local package registry and due to this we do not go to the external network - this speeds up the assembly and increases the security of the entire system. Such an approach was suggested to us by javists, they are great. Love your back-tenders;)

    And we conducted an experiment - we implemented a process manager in one of the applications, which simplified the administration of services on Node.js. He helped with clustering, and also relieved us of one old bash script that ran applications.

    And the whole stack is small


    We have javascript everywhere in the frontend. And on the server, and on the client, and under the hood of the internal tools. We know other languages, but javascript copes with everything perfectly.

    But BEM in our tasks does not cope with everything.

    What is BEM
    БЭМ – подход к веб-разработке, придуманный Яндексом во времена статических HTML-страниц и CSS-каскадов длиною в жизнь. Никакого компонентного подхода еще не было, а поддерживать единообразие множества сервисов было нужно. Яндекс не растерялся и разработал свой собственный компонентный подход, который сегодня позволяет создавать изолированные компоненты и писать гибкий декларативный код.

    БЭМ не только методология, но и большой набор технологий и библиотек. Часть из них заточена под специфику БЭМ, а некоторые вполне могут использоваться в отрыве от БЭМ-архитектуры. Если вам в проекте понадобится мощный шаблонизатор или достойный пример компонентной абстракции над DOM, вы знаете, где их можно найти ;)

    Therefore, we started to transfer services to React. Some of them already live in two applications built on different stacks:

    - a platform characteristic of Yandex BEM;
    - a young and trendy React ecosystem.

    Yandex Technologies


    It's time to tell why I fell in love with BEM.

    Override levels


    Levels, levels, levels ... BEM! Profit!
    Override levels are one of the main features of the BEM methodology. To understand how they work, let's look at the picture: The


    picture is formed by overlaying layers. Each layer changes the final image, but does not change the other layers. The layer can be easily pulled out or added on top, and the image will change.
    Override levels do the same with the code: The


    behavior of the component is formed when the code is assembled. To add additional behavior it is enough to connect the required level to the assembly. The module code from different levels overlaps each other. At the same time, the source code does not change, and we get different behavior, combining different levels.

    What are the levels
    На картинке выше несколько уровней переопределения:
    • Базовый уровень – библиотека – поставляет исходный модуль кода;
    • Следующий уровень – проект – модифицирует этот модуль под нужды проекта;
    • Уровень выше – платформа – делает тот же модуль специфичным для разных устройств;
    • Вишенка на торте – уровень экспериментов – изменяет модуль для проведения A/B тестирования.


    Уровень проекта не зависит от уровня библиотеки, поэтому библиотеку легко обновлять. Уровень платформы позволяет использовать разную сборку для разных устройств. А уровень с экспериментом подключается для тестирования на пользователях и также легко выключается, когда получены результаты.

    Разработчик сам решает, какие уровни ему нужны: можно создать уровень с темой оформления или уровень с тем же кодом на другом фреймворке.

    Levels allow you to write complex modules based on simple ones, it is easy to combine the behavior and fumble the same code between services. And collects this code ENB - Webpack in the world of BEM.

    When I got acquainted with BEM, I was particularly pleased with the UI libraries, which contain ready-made components. We are expanding these components in the framework of new libraries and felting them between projects. This makes life a lot easier: I rarely make up, do not write JS of the same type, and quickly assemble interfaces from ready-made blocks.



    Now we will take a closer look at the BEM platform tools in order to understand what BEM is not doing well enough and why it was not suitable for solving our problems.

    BEM-XJST


    I'll start with my beloved, the bem-xjst template engine . Before Yandex, I used Jade, and Bem-xjst perfectly illustrated the minuses of Jade, which I did not see then. The bem-xjst templates are declarative [1], they are not if hell [2], and they perfectly meet the requirements of the component approach [3]. All this is clearly seen in the example:



    In the sandbox you can see the result of templating and play with it.

    How it works. Inside - the secret of ideal architecture;)
    • пишем BEMJSON. BEMJSON это JSON, описывающий БЭМ-дерево. БЭМ-дерево это представление DOM-дерева в виде независимых компонент;
    • bem-xjst принимает на вход BEMJSON и применяет шаблоны. Этот процесс можно сравнить с рендерингом в браузере. Браузер обходит DOM-дерево и постепенно применяет CSS правила к его DOM-узлам: размер, цвет текста, отступы. Так же bem-xjst обходит BEMJSON, ищет соответствующие его узлам шаблоны и постепенно применяет их: тег, атрибуты, содержимое. «Применить шаблон» значит сгенерировать из него HTML-строку. Генерацией HTML из BEMJSON занимается один из движков шаблонизатора – BEMHTML.


    Писать шаблоны просто: выделяем сущность и пишем функции, которые шаблонизатор вызовет для рендера частей HTML-строки. Самое сложное – выделить сущность. Правильные сущности – залог хорошей архитектуры!

    Чем длиннее ваша борода, тем выше шанс, что вы уже заметили отсылку в названии шаблонизатора: XSLT (eXtensible Stylesheet Language Transformations) => XJST (eXtensible JavaScript Transformations). Он использует принципы из XSLT и потому такой декларативный. Если вы не знаете, что такое XSLT, считайте, что вам повезло :)

    Bem-xjst is isomorphic. We render the HTML pages on the server and dynamically change it on the client. For templating in runtime, bem-xjst provides the API that we use when writing client-side javascript code.

    I-bem


    With the help of bem-xjst we describe the presentation, and the logic with the help of i-bem . I-bem is an abstraction over DOM that provides a high-level API for working with components. Simply put, it allows you to write this:



    instead:



    To write code, you do not need to know about the internal implementation of the component. We operate on entities that are described in the template: no matter if it is a jQuery selector or a DOM element. We can create custom events that are suitable for a particular object model, and working with native events and interfaces will be hidden in the internal implementation. The low-level logic is also described there, which means that we do not load the code with the main logic with unnecessary checks. As a result, the code is easy to read and does not depend on a specific technology.

    I-bem allows you to describe the logic of the component as a set of states [1]. This is declarative javascript. I-bem implements its own Event Emitter: when state changes, components automatically generate events for which another component can subscribe [2].

    This is how most of the client-side javascript code on BEM looks like:



    How it works
    • по событию domReady i-bem находит в DOM-дереве компоненты (блоки) и инициализирует их – создает в памяти браузера js-объект, соответствующий блоку;
    • при наступлении нужных событий мы устанавливаем блоку маркеры, отражающие состояние. Роль маркеров выполняют CSS-классы. Например, при клике на input, мы добавляем ему класс «input_focused», который служит маркером;
    • при выставлении таких маркеров i-bem запускает коллбэки, заданные в javascript-реализации блока.

    Писать логику просто: нужно описать возможные состояния блока (те самые маркеры) и задать обработчики изменения этих состояний (те самые коллбэки).

    With i-bem, we can easily override the behavior of the components, create a harmonious API of their interaction and dynamically change them in runtime. So what's missing?
    We love BEM for its declarative, easy scalability and high-level abstractions, but we are not ready to put up with its limitations anymore. Below we consider the problem of client rendering, data storage and other limitations of the BEM platform. Over time, these problems may be resolved by BEM distributors, but we are not ready to wait.

    Modern web c SPA and adaptability for mobile devices requires adaptability from us. Therefore, we decided to switch to our own stack. And chose React.

    New Maple Stack at React


    React brought virtual DOM, hot reload, CSS in JS and a large community into our lives, of which we have become a part.

    Migration of our services to React is in full swing, some applications are already partially or completely rewritten to React. We get acquainted with new approaches and tools and improve the architecture of our applications.

    Libraries


    The partitioning of the interface entities into independent BEM blocks is perfectly friendly with the component approach of React. The Yandex developers wrote bem-react-core and transferred the core UI library of components to React. We wrote an adapter library above it, which takes into account the specifics of these components and delivers them as HOC :



    Such libraries are connected not in the application, but in the main library component: The



    application depends only on the main library and gets all the components from it:



    This reduces the number of dependencies applications, and libraries do not fall into the bundle twice under different versions.

    Technology


    React is not tied to specific technologies and we ourselves choose the tools and libraries. I have axios, redux, redux form, redux thunk, styled-components, typeScript, flow, jest and other cool stuff in my armament. In order to prevent the zoo, we coordinate the use of new technologies with other developers - we send a pull-request to a special repository with an analysis of what the technology is useful for and why we chose it.

    The front-fender enters the bar, and the barman tells him


    For React applications, we create a platform that integrates libraries and processes for their creation and maintenance. The heart of this platform is the console utility Frontend Bar. Bar knows how to cook a lot of tasty pieces.

    On the menu:

    1. Config with ice: bar will mix and shake your yml variables and prepare a config template for ansible.
    2. Juice with the scent of configurators: bar will create a new application based on the modular preform - Juice.
    3. Set of basic library settings. Expected soon.

    Creating a juicy app is now easy - “frontend-bar make juice”. Make juice, not war! When Bar deploys a new application, it performs a set of configurations from Juice: package.json, .babelrc, middleware code and routs code, root component code is generated. Frontend Bar will facilitate the allocation of new microservices and help you follow uniform rules for writing code.

    When we switched to the new stack, we began to improve the server architecture of the applications - we wrote a new logger for the client and a library with a set of abstractions for implementing MVC . Today we decide what the new server architecture will be.



    Spoiler: choose onion.

    What happened and was it better? Let's understand


    Dynamic interfaces


    It was


    Above, I wrote that bem-xjst provides an API for templating in runtime. I-bem, in turn, is able to work with the DOM-tree. Make friends with them and we can dynamically generate and modify HTML. Let's try to change the button by event:




    In this example, the weak side of BEM is visible: i-bem does not want to be friends with bem-xjst and does not want to know anything about the templates. It adds a class to the block, but does not apply the pattern [1]. We re-render the component manually [2]:

    • describe a new piece of BEM tree [3];
    • then use the new template [4];
    • and initialize another component on the current DOM node [5].

    In addition, i-bem does not create diff BEM-trees, so the re-render of the entire component, and not the changed parts. Consider a simple example: re-render the contents of a modal window on demand. It consists of three elements:



    For simplicity, we assume that only one element can change.



    I want to do [1] and relax. But i-bem will not understand what has changed, completely re-render the entire component and also relax. In this example, there will be no serious consequences, but what if it is so careless to re-render whole forms? This degrades performance and causes unpleasant side effects: an input is flashed somewhere, an ownerless tooltip hangs somewhere. Because of this, we are sad and manually manage parts of the component to make a point re-peer [2]. This complicates the development, and we are again sad.

    It became


    React came and ruined everything. It monitors the state of the components, we no longer manage the manual rendering and do not think about interaction with the DOM. React contains the virtual DOM implementation . When React.createElement is called, a js-object of the DOM node with its properties and successors is created - the virtual DOM of this component, which is stored inside React. When the component changes, the React computes the new virtual DOM, and then the diff saved and the new, and updates only the part of the DOM that has changed. Everything flies, and we can only optimize complex logic using shouldComponentUpdate. This is a success!

    Data storage


    It was


    In BEM, we prepare all the data on the server and transfer it to the page components:



    The components are isolated and will not share data with each other, which means that the same data will have to be passed to different components [1]. We will not be able to get them on the client; therefore, each component receives in advance a set of data that is needed for all possible scenarios of its operation. This means that we load the component with data that it may not need [2].

    Sometimes we are rescued by a global entity in which part of the common data is stored, but global storage of variables does not fit well into the concept of BEM. Therefore, we wrote bem-redux , which adapts Redux for BEM. Redux- State Manager, managing the flow of data. It perfectly manages our data within simple interfaces, but when developing a complex component we run into the rendering problem I described above. Redux is not friendly with i-bem, we fix bugs and feel sad.

    It became


    Redux + React = <3
    Redux stores data for the entire application in one place [1]:



    The component decides when and what data it needs [2]:



    We only need to describe the scenarios of the components [3] and specify where to get the data for performing it [4]:



    And React does the rest [5]:



    This approach allows you to follow the principle of one-stop responsibility and encapsulate the logic of the component in the component itself, rather than spreading it in the page code. This is a success!

    You have to pay for everything


    For success, we paid off with a fair amount of legacy on React. It hurts to see how your code, written just a couple of months ago, smoothly turns into deprecated.

    The fact is that React is a view-layer library, not a full-fledged framework. You can choose all the tools, but you have to choose all the tools. And also organize the code yourself, formulate approaches to solving typical tasks, work out a set of agreements and write the missing plugins. We write our own validators for redux forms and have not yet learned how to work with complex animations. And we try and throw out, write and rewrite. And we do not always rewrite, why our backlog is growing.

    React is young enough and not ready for enterprise development, unlike BEM. And while we were learning how to cook it, they stuffed all their kitchen and messed themselves up to their elbows. And we are still arguing about whether we need a flow or not, and still do not fully understand what to store in the stack, and what is in the local stete. We write as it is necessary and we go to conferences to find out how to. We beat the bumps, but we are confidently moving forward.

    Unexpected Buns


    The new stack made it possible to take a fresh look at a number of tasks and provided simple ways to solve them.

    CSS in JS


    It was


    Consider a simple case from life: to color and animate an icon by event, something like this:



    Code of nothing at all:



    True, according to the BEM rules, you will have to spread it in as many as three directories:



    Overhead? Controversial issue. More importantly, in js we add these classes manually when the necessary events occur. The usual situation, but the more custom or more complex the interface, the more often you'll have to add and remove classes. And if you need to change not only the icon, but also the text? Not quite the logic that you want to see in the js-code:



    And what if the duration of the animation depends on something and is set dynamically? Then we will rewrite the CSS animation on jQuery and slightly a bit sad.

    It became


    Styled-components , I love you! СSS in JS - one love! My internal layout designer rejoices:



    Modularity is preserved, CSS-animation works and no manual work with classes. A nice bonus is the new stack.

    Typing


    It was


    We used to write tons of jsDoc. Let's see if it is useful:



    This example is taken from production code. What does the state contain? I have no idea. Yes, there is a readme, but alas, it is a bit outdated. Yes, we are ashamed, but with documentation and comments it happens so often, they are unreliable. We'll have to delve into the code. Or do not go deep and inadvertently break everything. We hurry, do not go deep, break and sad.

    It became


    Typing came to the rescue. "Tyk" on the type, and all the ins and outs of the method before his eyes. Too lazy to understand? Precommit checker will start flow , and you still have to figure it out.

    I disliked flow at first sight. The terms are burning, the manager pings, and you and there “cannot get property”, and then “property is missing”. But recently I was told that types can be designed O_o How to design types? Something like this:



    My world turned upside down. Flow has ceased to be a nightmare. Describing the API of modules by type before writing code turned out to be convenient and useful. Reliable code - a nice bonus!

    So no more BEM?


    Not. BEM is alive, and we continue to support applications on the BEM stack. Over time, they will move to React, but for now we are preparing the ground for this: we are translating component libraries, forming a set of tools and agreements, and learning how to plan migration dates.

    BEM implemented our e-mail mailing template. We are preparing letters on the server, and the above limitations of the BEM platform do not affect this application. Using BEM to design it is an appropriately elegant solution.

    In addition, our designers prototype using BEM and sometimes bring us overlaid components instead of mockups. And even if we stop writing on BEM, he will still find us :)

    I read the first part. What is it about web designers?


    I participated in the translation of one of the applications from BEM to React and clarified an important thing.

    Before joining Yandex.Money, I was a simple typesetter and spent more than one year, tons of HTML and JSX. I did not take seriously the frontend community and its changing world. I did not understand why to study the first Angular, to forget about it tomorrow and to study the second. I did not understand why change jQuery.Ajax to Fetch, to replace Fetch with Axios later.

    It turned out that when you translate a project from one framework to another, you do not just transfer the code. We have to analyze and improve the architecture of the application, straighten logic, refactor. And the constant change of tools is not an attempt to ride the wave of HYIP, but the constant search for the best solution that meets the requirements of the time. A dynamic field like nothing else contributes to the development of your product and your professional development, respectively. And the frontend is just such an area. Let's fight for it together!

    React everyone!

    Also popular now: