Aqui está a história da integração da metodologia BEM no universo React. O material que você lerá é baseado na experiência dos desenvolvedores Yandex no desenvolvimento do maior e mais carregado serviço na Rússia - Yandex.Search. Nunca falamos com tanto detalhe e profundidade sobre por que o fizemos, e não de outra forma, o que nos motivou e o que realmente queríamos. O forasteiro recebeu lançamentos secos e críticas em conferências. Somente à margem podia-se ouvir algo assim. Como co-autor, fiquei indignado por causa da escassez de informações externas sempre que falava sobre novas versões de bibliotecas. Mas desta vez vamos compartilhar todos os detalhes.

Todo mundo já ouviu falar sobre a metodologia BEM. Seletores de CSS com sublinhados. A abordagem de componente mencionada, tendo em mente a maneira como os seletores de CSS CSS são escritos. Mas não haverá uma palavra sobre CSS no artigo. Apenas JS, apenas hardcore!
Para entender por que a metodologia apareceu e quais problemas a Yandex enfrentou, recomendo que você se familiarize com o histórico do BEM.
Prólogo
O BEM realmente nasceu como uma salvação da forte conectividade e aninhamento no CSS. Mas dividir a planilha style.css
em arquivos para cada bloco, elemento ou modificador inevitavelmente levou a uma estrutura semelhante do código JavaScript.
Em 2011, o Open Source adquiriu os primeiros commits da estrutura i-bem.js
, que funcionavam em conjunto com bem-xjst
modelo bem-xjst
. Ambas as tecnologias surgiram do XSLT e serviram à ideia popular de separar a lógica de negócios e a apresentação de componentes. No mundo exterior, esses foram os grandes tempos do guidão e do sublinhado.
bem-xjst
é um tipo diferente de mecanismo de modelo. Para aumentar meu conhecimento da arquitetura das abordagens de padronização, recomendo vivamente o relatório de Sergei Berezhnoy . bem-xjst
pode experimentar o bem-xjst
modelo bem-xjst
na sandbox online .
Devido às especificidades dos serviços de pesquisa Yandex, as interfaces do usuário são construídas usando dados. A página de resultados da pesquisa é exclusiva para cada consulta.

Consulta de pesquisa por referência

Consulta de pesquisa por link

Consulta de pesquisa por referência
Quando a divisão em um bloco, elemento e modificador se espalha pelo sistema de arquivos, isso permite coletar o máximo possível apenas o código necessário, de fato para cada página, para cada solicitação do usuário. Mas como
src/components ├── ComponentName │ ├── _modName │ │ ├── ComponentName_modName.tsx — │ │ └── ComponentName_modName_modVal.tsx — │ ├── ElementName │ │ └── ComponentName-ElementName.tsx — ComponentName │ ├── ComponentName.i18n — │ │ ├── ru.ts — │ │ ├── en.ts — │ │ └── index.ts — │ ├── ComponentName.test — │ │ ├── ComponentName.page-object.js — Page Object │ │ ├── ComponentName.hermione.js — │ │ └── ComponentName.test.tsx — unit- │ ├── ComponentName.tsx — │ ├── ComponentName.scss — │ ├── ComponentName.examples.tsx — Storybook │ └── README.md —
Estrutura moderna de diretório de componentes
Como em outras empresas, no Yandex, os desenvolvedores de interface são responsáveis pelo frontend, que consiste na parte do cliente no navegador e na parte do servidor no Node.js
A parte do servidor processa os dados da pesquisa "grande" e impõe modelos a eles. O processamento de dados primário converte JSON em BEMJSON , a estrutura de dados bem-xjst
modelo bem-xjst
. O mecanismo de modelo percorre cada nó da árvore e impõe um modelo a ele. Como a conversão primária ocorre no servidor e, devido à divisão em pequenas entidades, os nós correspondem aos arquivos, durante a geração do modelo, enviamos o código para o navegador que será usado apenas na página atual.
Abaixo está a correspondência dos nós BEMJSON com os arquivos no sistema de arquivos.
module.exports = { block: 'Select', elem: 'Item', elemMods: { type: 'navigation' } };
src/components ├── Select │ ├── Item │ │ _type │ │ ├── Select-Item_type_navigation.js │ │ └── Select-Item_type_navigation.css
O sistema modular YModules
foi responsável por isolar os componentes do código JavaScript no navegador. Ele permite que você entregue módulos de forma síncrona e assíncrona ao navegador. Um exemplo de como os componentes funcionam com o YModules
e o i-bem.js
pode ser encontrado aqui . Hoje, para a maioria dos desenvolvedores, o webpack
e o padrão inédito de importações dinâmicas fazem isso .
Um conjunto de metodologia BEM, mecanismo de modelo declarativo e estrutura JS com um sistema modular tornou possível resolver qualquer problema. Mas com o tempo, a dinâmica chegou às interfaces do usuário.
Nova esperança
Em 2013, o React encantou o código-fonte aberto. De fato, o Facebook começou a usá-lo em 2011. James Long, em suas anotações da conferência JS Conf US , diz:
As duas últimas sessões foram uma surpresa. O primeiro foi dado por dois desenvolvedores do Facebook e eles anunciaram o Facebook React . Não fiz muitas anotações porque fiquei meio chocado com a idéia que acho ruim. Essencialmente, eles criaram uma linguagem chamada JSX, que permite incorporar XML em JavaScript para criar interfaces de usuário reativas ao vivo. XML Em JavaScript.
O React mudou a abordagem para projetar aplicativos da web. Tornou-se tão popular que hoje você não consegue encontrar um desenvolvedor que nunca ouviu falar do React. Mas outra coisa é importante: as aplicações se tornaram diferentes, o SPA entrou em nossas vidas.
É geralmente aceito que os desenvolvedores Yandex tenham um senso especial de beleza com relação à tecnologia. Às vezes estranho, difícil de argumentar, mas nunca sem razão. Quando o React estava ganhando estrelas no GitHub , muitos que estavam familiarizados com as tecnologias da Web Yandex insistiram: o Facebook venceu, abandonou seu trabalho e reescreve tudo no React antes que seja tarde demais. É importante entender duas coisas.
Em primeiro lugar, não houve guerra. As empresas não competem na criação da melhor estrutura na Terra. Se uma empresa começar a gastar menos tempo (dinheiro de leitura) em tarefas de infraestrutura com a mesma produtividade, todos serão beneficiados. Não faz sentido escrever estruturas para escrever estruturas. Os melhores desenvolvedores criam ferramentas que resolvem as tarefas da empresa da melhor maneira. Empresas, serviços, objetivos - é tudo diferente. Daí a variedade de ferramentas.
Em segundo lugar, estávamos procurando uma maneira de usar o React da maneira que gostaríamos que fosse. Com todos os recursos que nossas tecnologias descritas acima forneceram.
Acredita-se que o código usando o React seja rápido por padrão. Se você também pensa assim, está profundamente enganado. A única coisa que o React faz é, na maioria dos casos, ajudar a interagir de maneira ideal com o DOM.
Até a versão 16, o React tinha uma falha fatal. Era 10 vezes mais lento que o bem-xjst
no servidor. Não podíamos pagar esse desperdício. O tempo de resposta para o Yandex é uma das principais métricas. Imagine que, ao solicitar uma receita de vinho quente, você terá uma resposta 10 vezes mais lenta que o normal. Você não ficará satisfeito com as desculpas, mesmo que saiba algo sobre desenvolvimento web. O que podemos dizer sobre a explicação, como "mas tornou-se mais conveniente para os desenvolvedores se comunicarem com o DOM". Adicione aqui a relação entre o preço da implementação e o lucro - e você mesmo tomará a única decisão certa.
Felizmente para a tristeza, os desenvolvedores são pessoas estranhas. Se algo não der certo, esse não é um motivo para largar tudo ...
De cabeça para baixo
Estávamos confiantes de que poderíamos derrotar a lentidão do React. Já temos um mecanismo de modelo rápido. Tudo que você precisa é gerar HTML no servidor usando o bem-xjst
e, no cliente, "forçar" o React a aceitar esta marcação como sua. A idéia era tão simples que nada indicava um fracasso.
Nas versões até 15, inclusive, o React validou a validade da marcação usando uma soma de hash - um algoritmo que transforma qualquer otimização em uma abóbora. Para convencer o React da validade da marcação, foi necessário definir um ID para cada nó e calcular a soma de hash de todos os nós. Também significava suportar um conjunto duplo de modelos: reagir para o cliente e bem-xjst
para o servidor. Testes simples de velocidade com instalação de identificação deixaram claro que não havia sentido em continuar.
O bem-xjst
bem bem-xjst
é uma ferramenta muito subestimada. Veja o relatório do principal mantenedor da Glory Oliyanchuk e veja por si mesmo. bem-xjst
é baseado em uma arquitetura que permite usar uma sintaxe de modelo para diferentes transformações da árvore de origem. Muito parecido com React, não é? Hoje, esse recurso permite que ferramentas como o react-sketchapp
.
Fora da caixa bem-xjst
contém dois tipos de conversões: em HTML e em JSON. Qualquer desenvolvedor suficientemente diligente pode escrever seu próprio mecanismo para transformar modelos em qualquer coisa. bem-xjst
transformar uma árvore de dados em uma sequência de chamadas para funções HyperScript . O que significava total compatibilidade com o React e outras implementações do algoritmo Virtual DOM, por exemplo, Preact .
Uma introdução detalhada à geração de chamadas de função HyperScript
Como os modelos React exigem a coexistência de layout e lógica de negócios, tivemos que trazer a lógica do i-bem.js
para nossos modelos, que não foram projetados para isso. Para eles, isso não era natural. Eles estavam indo de maneira diferente. A propósito!
Abaixo está um exemplo das profundidades de colar diferentes mundos em um tempo de execução.
block('select').elem('menu')( def()(function() { const React = require('react'); const Menu = require('../components/menu/menu'); const MenuItem = require('../components/menu-item/menu-item'); const _select = this.ctx._select; const selectComponent = _select._select; return React.createElement.apply(React, [ Menu, { mix: { block : this.block, elem : this.elem }, ref: menu => selectComponent._menu = menu, size: _select.mods.size, disabled: _select.mods.disabled, mode: _select.mods.mode, content: _select.options, checkedItems: _select.bindings.checkedItems, style: _select.bindings.popupMenuWidth, onKeyDown: _select.bindings.onKeyDown, theme: _select.mods.theme, }].concat(_select.options.map(option => React.createElement( MenuItem, { onClick: _select.bindings.onOptionCheck, theme: _select.mods.theme, val: option.value, }, option.content) )) ); }) );
Claro, tivemos nossa própria assembléia. Como você sabe, a operação mais rápida é a concatenação de cadeias. O mecanismo bem-xjst
foi construído sobre ele, a montagem sobre ele. Arquivos de blocos, elementos e modificadores estão em pastas, e a montagem apenas teve que colar os arquivos na seqüência correta. Com essa abordagem, você pode colar JS, CSS e modelos em paralelo, bem como as próprias entidades. Ou seja, se você possui quatro componentes em um projeto, quatro núcleos no laptop e a montagem de uma tecnologia de componente leva um segundo, a criação do projeto leva dois segundos. Aqui deve ficar mais claro como conseguimos inserir apenas o código necessário no navegador.
Tudo isso para nós fez a ENB . Recebemos a árvore final para padronização apenas em tempo de execução e, como a dependência entre os componentes precisava surgir um pouco antes para coletar pacotes, essa função foi assumida pela pouco conhecida tecnologia deps.js
Ele permitiu criar um gráfico de dependência entre os componentes, após o qual o coletor poderia colar o código na sequência desejada, ignorando o gráfico.
A versão 16 do React parou de funcionar nessa direção.A velocidade de execução dos modelos no servidor foi igual . Nas instalações de produção, a diferença se tornou imperceptível.
Nó: v8.4.0
Crianças: 5K
renderizador | tempo médio | ops / s |
---|
pré- execução v8.2.6 | 66.235ms | 15 |
bem-xjst v8.8.4 | 71.326ms | 14 |
react v16.1.0 | 73.966ms | 14 |
Usando os links abaixo, você pode restaurar o histórico da abordagem:
Já tentamos mais alguma coisa?

Motivação
No meio da história, será útil falar sobre o que nos motivou. Valeu a pena fazer isso no começo, mas - quem se lembra do velho, daquele olho de presente. Por que precisamos de tudo isso? O que o BEM pode trazer que o React não pode fazer? Perguntas que quase todo mundo faz.
Decomposição
A funcionalidade dos componentes se torna mais complicada de ano para ano, e o número de variações aumenta. Isso é expresso pelas construções if
ou switch
, como resultado, a base de código aumenta inevitavelmente, como resultado - o peso do componente e do projeto usando esse componente aumenta. A parte principal da lógica do componente React está contida no método render()
. Para alterar a funcionalidade de um componente, é necessário reescrever a maior parte do método, o que inevitavelmente leva a um aumento exponencial no número de componentes altamente especializados.
Todo mundo conhece as bibliotecas material-ui , fabric-ui e react-bootstrap . Em geral, todas as bibliotecas conhecidas com componentes têm a mesma desvantagem. Imagine que você tem vários projetos e todos usam a mesma biblioteca. Você usa os mesmos componentes, mas em variações diferentes: aqui existem seleções com caixas de seleção, não existem, existem botões azuis com um ícone, existem botões vermelhos sem. O peso de CSS e JS que a biblioteca traz para você será o mesmo em todos os projetos. Mas porque? Variações de componentes são incorporadas no próprio componente e vêm com ele, se você deseja ou não. Para nós, isso é inaceitável.
O Yandex também possui sua própria biblioteca com componentes - Lego. É aplicado em ~ 200 serviços. Queremos que o uso do Lego na Pesquisa custe o mesmo para o Yandex.Health? Você sabe a resposta.
Para oferecer suporte a várias plataformas, na maioria das vezes, eles criam uma versão separada para cada plataforma ou uma adaptável.
O desenvolvimento de versões individuais requer recursos adicionais: quanto mais plataformas, mais esforço. Manter o estado síncrono das propriedades do produto em diferentes versões causará novas dificuldades.
O desenvolvimento de uma versão adaptável complica o código, aumenta o peso, reduz a velocidade do produto com a diferença adequada entre as plataformas.
Queremos que nossos pais / amigos / colegas / filhos usem versões para computador no celular com menor velocidade da Internet e menor produtividade? Você sabe a resposta.
Os experimentos
Se você estiver desenvolvendo projetos para um grande público, precisará ter certeza de todas as alterações. Os experimentos A / B são uma maneira de ganhar essa confiança.
Maneiras de organizar o código para experimentos:
- bifurcação do projeto e criação de instâncias de serviço na produção;
- condições pontuais dentro da base de código.
Se o projeto tiver muitas experiências longas, a ramificação da base de código causará custos significativos. É necessário manter cada filial atualizada com o experimento: erros de porta corrigidos e funcionalidade do produto. A ramificação da base de código complica experimentos cruzados várias vezes.
As condições pontuais funcionam com mais flexibilidade, mas complicam a base de código: as condições do experimento podem afetar diferentes partes do projeto. Um grande número de condições prejudica o desempenho aumentando a quantidade de código para o navegador. É necessário remover as condições, tornar o código básico ou excluir completamente o experimento com falha.
Na Pesquisa ~ 100 experiências on-line em várias combinações para diferentes públicos. Você poderia ver por si mesmo. Lembre-se, talvez você tenha notado a funcionalidade e, uma semana depois, ela desapareceu magicamente. Queremos testar as teorias de produtos ao custo de manter centenas de ramificações da base de código ativa de 500.000 linhas, que são alteradas por ~ 60 desenvolvedores diariamente? Você sabe a resposta.
Mudança global
Por exemplo, você pode criar um componente CustomButton
herdado de Button
de uma biblioteca. Mas o CustomButton
herdado não se aplicará a todos os componentes da biblioteca que contém Button
. Uma biblioteca pode ter um componente de Search
criado a partir de Input
e Button
. Nesse caso, o CustomButton
herdado não aparece dentro do componente Search
. Queremos contornar manualmente toda a base de códigos em que o Button
usado?

Longo caminho para a composição
Decidimos mudar a estratégia. Na abordagem anterior, eles tomaram a tecnologia Yandex como base e tentaram fazer com que o React funcionasse com base nisso. Novas táticas sugeriam o contrário. Foi assim que surgiu o projeto bem-react-core .
Pare com isso! Por que reagir?
Vimos nela a oportunidade de nos livrarmos da renderização inicial explícita em HTML e do suporte manual do estado do componente JS posteriormente no tempo de execução - na verdade, tornou-se possível mesclar modelos BEMHMTL e componentes JS em uma única tecnologia.
Inicialmente, planejamos transferir todas as melhores práticas e propriedades bem-xjst
para a biblioteca no topo do React. A primeira coisa que chama sua atenção é a assinatura ou, se preferir, a sintaxe para descrever os componentes.
O que você fez, existe o JSX!
A primeira versão foi construída com base na herança - uma biblioteca que ajuda a implementar classes e herança. Como alguns de vocês lembram, naquela época, protótipos de protótipo em JavaScript não tinham classes, não havia super
. Em geral, eles ainda estão ausentes, mais precisamente, essas não são as classes que primeiro vêm à mente. inherit
fez tudo o que as classes no padrão ES2015 podem fazer agora e o que é considerado magia negra: herança múltipla e fusão de protótipos em vez de reconstruir a cadeia, o que afeta positivamente o desempenho. Você não se enganará se acha que parece fazer sentido como herda no Node.js , mas eles funcionam de maneira diferente.
Abaixo está um exemplo da sintaxe dos modelos bem-react-core@v1.0.0
.
App-Header.js
import { decl } from 'bem-react-core'; export default decl({ block: 'App', elem: 'Header', attrs: { role: 'heading' }, content() { return ' '; } });
App-Header@desktop.js
import { decl } from 'bem-react-core'; export default decl({ block: 'App', elem: 'Header', tag: 'h1', attrs() { return { ...this.__base(...arguments), 'aria-level': 1 }, }, content() { return ` ${this.__base(...arguments)} h1`; } });
App-Header@touch.js
import { decl } from 'bem-react-core'; export default decl({ block: 'App', elem: 'Header', tag: 'h2', content() { return ` ${this.__base(...arguments)} `; } });
index.js
import ReactDomServer from 'react-dom/server'; import AppHeader from 'b:App e:Header'; ReactDomServer.renderToStaticMarkup(<AppHeader />);
output@desktop.html
<h1 class="App-Header" role="heading" aria-level="1">A h1</h2>
output@touch.html
<h2 class="App-Header" role="heading"> </h2>
Os modelos de dispositivo para componentes mais complexos podem ser encontrados aqui .
Como uma classe é um objeto e o trabalho com objetos em JavaScript é mais conveniente, a sintaxe é apropriada. A sintaxe posteriormente migrou para sua mente bem-xjst
.
A biblioteca era um repositório global de declarações de objetos - os resultados da execução da função decl
, partes de entidades: um bloco, elemento ou modificador. O BEM fornece um mecanismo de nomeação exclusivo e, portanto, é adequado para criar chaves em um cofre. O componente React resultante foi colado no local de uso. O truque é que decl
funcionou ao importar o módulo. Isso tornou possível indicar quais partes do componente são necessárias em cada local específico, usando uma lista simples de importações. Mas lembre-se: os componentes são complexos, existem muitas partes, a lista de importações é longa, os desenvolvedores são preguiçosos.
Import Magic
Como você pode ver, nos exemplos de código, existem linhas import AppHeader from 'b:App e:Header'
.
Você quebrou o padrão! É impossível! Simplesmente não vai funcionar!
Em primeiro lugar, o padrão de importação não opera com os termos do espírito "deve haver um caminho para um módulo real na linha de importação". Em segundo lugar, é o açúcar sintático que foi convertido usando Babel. Terceiro, construções de pontuação de import txt from 'raw-loader!./file.txt';
estranhas para o webpack import txt from 'raw-loader!./file.txt';
por algum motivo eles não incomodaram ninguém.
Portanto, nosso bloco é apresentado em duas plataformas: desktop
, touch
.
import Hello from 'b:Hello';
Hello
, applyDecls
, inherit
, React-.
Babel, , . webpack, , .
, :
:
- TypeScript/Flow;
- React- ;
- - ;
- .
bem-react-core@v1.0.0
, .
import { Elem } from 'bem-react-core'; import { Button } from '../Button'; export class AppHeader extends Elem { block = 'App'; elem = 'Header'; tag() { return 'h2'; } content() { return ( <Button> </Button> ); } }
, . , , TypeScript/Flow. , inherit
«» , , .
:
— webpack Babel;
— ;
— , .
HOC , .
import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { Block, Elem, withMods } from 'bem-react-core'; interface IButtonProps { children: string; } interface IModsProps extends IButtonProps { type: 'link' | 'button'; }
, .
withMods
, (), . , , withMods , . . , , , ( ) . . , , — , .
, :
- . , . , TS. , . ES5 TS super , . , TS , .
- . TS ES6 Babel ES5. , npm- . , Babel.
:
- , . , . : DOM-. HOC, . withMods .
- (, , ) . SFC .
- CSS-. CSS- JS- . , , .
v2.
, . . , , 1 2. .
— . CSS- HOC, — dependency injection .
React:
. . React.ComponentType
-. HOC compose .
.
dependency injection, React.ContextAPI
. , , . , . DI — HOC, . . , , .
, , . , , 4 , 1.5Kb
.
. Obrigado a quem leu até o fim. , React . .