React & BEM - colaboración oficial. Parte historica

Aquí está la historia de integrar la metodología BEM en el universo React. El material que leerá se basa en la experiencia de los desarrolladores de Yandex que desarrollan el servicio más grande y cargado en Rusia: Yandex.Search. Nunca antes habíamos hablado con tanto detalle y profundidad sobre por qué lo hicimos, y no de otra manera, qué nos motivó y qué es lo que realmente queríamos. El extraño recibió comunicados en seco y críticas en las conferencias. Solo al margen se podía escuchar algo así. Como coautor, estaba indignado por la escasez de información cada vez que hablaba de nuevas versiones de bibliotecas. Pero esta vez compartiremos todos los detalles.



Todos han escuchado sobre la metodología BEM. Selectores CSS con guiones bajos. Se habló del enfoque de componentes , teniendo en cuenta la forma en que se escriben los selectores CSS CSS. Pero no habrá una palabra sobre CSS en el artículo. ¡Solo JS, solo hardcore!


Para entender por qué apareció la metodología y qué problemas enfrentó Yandex en ese momento, le recomiendo que se familiarice con la historia de BEM.


Prologo


BEM realmente nació como una salvación de una fuerte conectividad y anidamiento en CSS. Pero dividir la hoja style.css en archivos para cada bloque, elemento o modificador condujo inevitablemente a una estructuración similar del código JavaScript.


En 2011, Open Source adquirió las primeras confirmaciones del i-bem.js , que funcionaba junto con bem-xjst motor de plantillas bem-xjst . Ambas tecnologías surgieron de XSLT y sirvieron a la idea popular de separar la lógica de negocios y la presentación de componentes. En el mundo exterior, estos fueron los grandes momentos de Handlebars y Underscore.


bem-xjst es un tipo diferente de motor de plantillas. Para aumentar mi conocimiento de la arquitectura de los enfoques de estandarización, recomiendo el informe de Sergei Berezhnoy . bem-xjst probar el motor de plantillas bem-xjst en el entorno limitado en línea .


Debido a los detalles de los servicios de búsqueda de Yandex, las interfaces de usuario se crean utilizando datos. La página de resultados de búsqueda es única para cada consulta.



Consulta de búsqueda por enlace



Consulta de búsqueda por referencia



Consulta de búsqueda por referencia


Cuando la división en un bloque, elemento y modificador se extendió al sistema de archivos, esto hizo posible recopilar solo el código necesario de manera más efectiva, de hecho para cada página, para cada solicitud de usuario. Pero 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 —   

Estructura moderna del directorio de componentes


Como en otras compañías, en Yandex, los desarrolladores de interfaces son responsables de la interfaz, que consiste en la parte del cliente en el navegador y la parte del servidor en Node.js La parte del servidor procesa los datos de la búsqueda "grande" y les impone plantillas. El procesamiento primario de datos convierte JSON a BEMJSON , la estructura de datos para bem-xjst motor de plantillas bem-xjst . El motor de plantillas rodea cada nodo del árbol e impone una plantilla sobre él. Dado que la conversión primaria se lleva a cabo en el servidor, y debido a la división en pequeñas entidades, los nodos corresponden a los archivos, durante la generación de la plantilla empujamos el código al navegador que se usará solo en la página actual.


A continuación se muestra la correspondencia de los nodos BEMJSON con los archivos del sistema de archivos.


 module.exports = { block: 'Select', elem: 'Item', elemMods: { type: 'navigation' } }; 

 src/components ├── Select │ ├── Item │ │ _type │ │ ├── Select-Item_type_navigation.js │ │ └── Select-Item_type_navigation.css 

El sistema modular YModules fue responsable de aislar los componentes del código JavaScript en el navegador. Le permite entregar módulos de forma sincrónica y asincrónica al navegador. i-bem.js se puede encontrar un ejemplo de cómo funcionan los componentes con YModules e i-bem.js . Hoy, para la mayoría de los desarrolladores, el webpack y el estándar inédito de importaciones dinámicas hacen esto .


Un conjunto de metodología BEM, motor de plantillas declarativas y marco JS con un sistema modular permitieron resolver cualquier problema. Pero con el tiempo, la dinámica ha llegado a las interfaces de usuario.


Nueva esperanza


En 2013, React encantó el código abierto. De hecho, Facebook comenzó a usarlo en 2011. James Long, en sus notas de la conferencia estadounidense JS Conf , dice:


Las dos últimas sesiones fueron una sorpresa. El primero fue dado por dos desarrolladores de Facebook y anunciaron Facebook React . No tomé muchas notas porque estaba en estado de shock por la mala idea que creo que es. Esencialmente, crearon un lenguaje llamado JSX que le permite incrustar XML en JavaScript para crear interfaces de usuario reactivas en vivo. XML En JavaScript

React ha cambiado el enfoque para diseñar aplicaciones web. Se ha vuelto tan popular que hoy no puedes encontrar un desarrollador que no haya oído hablar de React. Pero otra cosa es importante: las aplicaciones se han vuelto diferentes, el SPA ha llegado a nuestras vidas.


En general, se acepta que los desarrolladores de Yandex tienen un sentido especial de belleza con respecto a la tecnología. A veces extraño, que es difícil discutir, pero nunca sin razón. Cuando React estaba ganando estrellas en GitHub , muchos de los que estaban familiarizados con las tecnologías web de Yandex insistieron: Facebook ganó, abandonó sus manualidades y corrió a reescribir todo en React antes de que sea demasiado tarde. Es importante entender dos cosas.


En primer lugar, no hubo guerra. Las empresas no compiten en la creación del mejor marco en la Tierra. Si una empresa comienza a pasar menos tiempo (lectura - dinero) en tareas de infraestructura con la misma productividad, todos se beneficiarán de esto. No tiene sentido escribir marcos para escribir marcos. Los mejores desarrolladores crean herramientas que resuelven las tareas de la empresa de la mejor manera. Empresas, servicios, objetivos: todo es diferente. De ahí la variedad de herramientas.


En segundo lugar, estábamos buscando una forma de usar React de la forma en que nos gustaría que fuera. Con todas las características que dieron nuestras tecnologías descritas anteriormente.


Se cree ampliamente que el código que usa React es rápido por defecto. Si tú también lo crees, estás profundamente equivocado. Lo único que hace React es, en la mayoría de los casos, ayuda a interactuar de manera óptima con el DOM.


Hasta la versión 16, React tenía un defecto fatal. Fue 10 veces más lento que bem-xjst en el servidor. No podríamos permitirnos ese desperdicio. El tiempo de respuesta para Yandex es una de las métricas clave. Imagine que cuando solicita una receta de vino caliente, obtendrá una respuesta 10 veces más lenta de lo habitual. No estará contento con las excusas, incluso si sabe algo sobre desarrollo web. ¿Qué podemos decir sobre la explicación como "pero los desarrolladores se han sentido más cómodos hablando con el DOM"? Agregue aquí la relación entre el precio de implementación y el beneficio, y usted mismo tomará la única decisión correcta.


Afortunadamente para el dolor, los desarrolladores son personas extrañas. Si algo no funciona, entonces esta no es una razón para dejar todo ...


Boca abajo


Estábamos seguros de que podríamos vencer la lentitud de React. Ya tenemos un motor de plantillas rápido. Todo lo que necesita es generar HTML en el servidor usando bem-xjst , y en el cliente para "forzar" a React a aceptar este marcado como propio. La idea era tan simple que nada presagiaba un fracaso.


En versiones de hasta 15 inclusive, React validó la validez del marcado utilizando una suma de hash, un algoritmo que convierte cualquier optimización en una calabaza. Para convencer a React de la validez del marcado, era necesario anotar una identificación para cada nodo y calcular la suma hash de todos los nodos. También significaba admitir un conjunto dual de plantillas: React para el cliente y bem-xjst para el servidor. Pruebas de velocidad simples con instalación de identificación dejaron en claro que no tenía sentido continuar.


El bem-xjst bem bem-xjst es una herramienta muy subestimada. Mira el informe del responsable principal de Glory Oliyanchuk y compruébalo por ti mismo. bem-xjst se basa en una arquitectura que le permite usar una sintaxis de plantilla para diferentes transformaciones del árbol de origen. Muy similar a React, ¿no? Esta característica hoy permite que react-sketchapp herramientas como react-sketchapp .


Fuera de la caja bem-xjst contiene dos tipos de conversiones: en HTML y en JSON. Cualquier desarrollador lo suficientemente diligente puede escribir su propio motor para transformar plantillas en cualquier cosa. bem-xjst transformar un árbol de datos en una secuencia de llamadas a funciones HyperScript . Lo que significaba compatibilidad total con React y otras implementaciones del algoritmo Virtual DOM, por ejemplo, Preact .



Una introducción detallada a la generación de llamadas a la función HyperScript


Dado que las plantillas React requieren la coexistencia del diseño y la lógica empresarial, tuvimos que incorporar la lógica de i-bem.js a nuestras plantillas, que no estaban destinadas a esto. Para ellos no era natural. Iban de manera diferente. Por cierto!


A continuación se muestra un ejemplo de las profundidades de pegar mundos diferentes en un tiempo de ejecución.


 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) )) ); }) ); 

Por supuesto, tuvimos nuestra propia asamblea. Como sabes, la operación más rápida es la concatenación de cadenas. El motor bem-xjst se construyó sobre él, el ensamblaje se construyó sobre él. Los archivos de bloques, elementos y modificadores yacían en carpetas, y el ensamblaje solo tenía que pegar los archivos en la secuencia correcta. Con este enfoque, puede pegar JS, CSS y plantillas en paralelo, así como las propias entidades. Es decir, si tiene cuatro componentes en un proyecto, cuatro núcleos en la computadora portátil y el ensamblaje de la tecnología de un componente lleva un segundo, la construcción del proyecto tomará dos segundos. Aquí debería quedar más claro cómo logramos insertar solo el código necesario en el navegador.


Todo esto para nosotros lo hizo ENB . Recibimos el árbol final para la estandarización solo en tiempo de ejecución, y dado que la dependencia entre los componentes tuvo que surgir un poco antes para recolectar paquetes, esta función fue asumida por la tecnología poco conocida deps.js Le permitió crear un gráfico de dependencia entre componentes, después de lo cual el recopilador podría pegar el código en la secuencia deseada, sin pasar por el gráfico.


React versión 16 dejó de funcionar en esta dirección. La velocidad de ejecución de las plantillas en el servidor fue igual . En las instalaciones de producción, la diferencia se hizo imperceptible.


Nodo: v8.4.0
Niños: 5K


renderizadorhora mediaops / sec
preact v8.2.666.235ms15
bem-xjst v8.8.471.326 ms14
reaccionar v16.1.073,966 ms14

Usando los enlaces a continuación puede restaurar el historial del enfoque:



¿Hemos intentado algo más?




Motivación


En el medio de la historia, será útil hablar sobre lo que nos motivó. Valió la pena hacer esto al principio, pero, quién recuerda el viejo, ese ojo como un regalo. ¿Por qué necesitamos todo esto? ¿Qué puede aportar BEM que React no puede hacer? Preguntas que casi todos hacen.


Descomposición


La funcionalidad de los componentes se vuelve más complicada de año en año, y aumenta el número de variaciones. Esto se expresa por las construcciones if o switch , como resultado, la base del código crece inevitablemente, como resultado: el peso del componente y el proyecto que usa dicho componente aumenta. La parte principal de la lógica del componente React está contenida en el método render() . Para cambiar la funcionalidad de un componente, es necesario reescribir la mayor parte del método, lo que inevitablemente conduce a un aumento exponencial en el número de componentes altamente especializados.


Todos conocen las bibliotecas material-ui , fabric-ui y react-bootstrap . En general, todas las bibliotecas conocidas con componentes tienen el mismo inconveniente. Imagine que tiene varios proyectos y todos usan la misma biblioteca. Toma los mismos componentes, pero en diferentes variaciones: aquí hay selecciones con casillas de verificación, no hay, hay botones azules con un icono, hay botones rojos sin él. El peso de CSS y JS que le brinda la biblioteca será el mismo en todos los proyectos. Pero por que? Las variaciones de componentes están incrustadas dentro del componente en sí y vienen con él, lo desee o no. Para nosotros esto es inaceptable.


Yandex también tiene su propia biblioteca con componentes: Lego. Se aplica en ~ 200 servicios. ¿Queremos que el uso de Lego en la búsqueda cueste lo mismo para Yandex.Health? Ya sabes la respuesta.


Desarrollo multiplataforma


Para admitir múltiples plataformas, la mayoría de las veces crean una versión separada para cada plataforma o una versión adaptativa.


El desarrollo de versiones individuales requiere recursos adicionales: cuantas más plataformas, más esfuerzo. Mantener el estado síncrono de las propiedades del producto en diferentes versiones causará nuevas dificultades.


El desarrollo de una versión adaptativa complica el código, aumenta el peso, reduce la velocidad del producto con la diferencia adecuada entre las plataformas.


¿Queremos que nuestros padres / amigos / colegas / hijos usen versiones de escritorio en dispositivos móviles con menor velocidad de Internet y menor productividad? Ya sabes la respuesta.


Los experimentos


Si está desarrollando proyectos para una gran audiencia, debe estar seguro de cada cambio. Los experimentos A / B son una forma de ganar esa confianza.


Formas de organizar el código para experimentos:


  • bifurcación del proyecto y la creación de instancias de servicio en producción;
  • condiciones de punto dentro de la base del código.

Si el proyecto tiene muchos experimentos largos, ramificar la base del código causa costos significativos. Es necesario mantener actualizada cada rama con el experimento: errores de puerto corregidos y funcionalidad del producto. La ramificación de la base de código complica los experimentos de intersección varias veces.


Las condiciones puntuales funcionan de manera más flexible, pero complican la base del código: las condiciones del experimento pueden afectar diferentes partes del proyecto. Una gran cantidad de condiciones degradan el rendimiento al aumentar la cantidad de código para el navegador. Es necesario eliminar las condiciones, hacer que el código sea básico o eliminar completamente el experimento fallido.


En Search ~ 100 experimentos en línea en varias combinaciones para diferentes audiencias. Podrías verlo por ti mismo. Recuerde, tal vez notó la funcionalidad y una semana después desapareció mágicamente. ¿Queremos probar las teorías de productos a costa de mantener cientos de sucursales de la base de código activa de 500,000 líneas, que son cambiadas por ~ 60 desarrolladores diariamente? Ya sabes la respuesta.


Cambio global


Por ejemplo, puede crear un componente CustomButton heredado de Button de una biblioteca. Pero el CustomButton heredado no se aplicará a todos los componentes de la biblioteca que contiene Button . Una biblioteca puede tener un componente de Search creado a partir de Input y Button . En este caso, el CustomButton heredado no aparece dentro del componente de Search . ¿Queremos recorrer manualmente toda la base de código donde Button usa Button ?



Largo camino a la composición


Decidimos cambiar la estrategia. En el enfoque anterior, tomaron la tecnología Yandex como base y trataron de hacer que React funcionara sobre esta base. Nuevas tácticas sugirieron lo contrario. Así surgió el proyecto bem-react-core .


Basta! ¿Por qué reaccionar en absoluto?

Vimos en él la oportunidad de deshacernos del renderizado inicial explícito en HTML y del soporte manual del estado del componente JS más adelante en tiempo de ejecución; de hecho, fue posible fusionar plantillas BEMHMTL y componentes JS en una sola tecnología.


v1.0.0


Inicialmente, planeamos transferir todas las mejores prácticas y propiedades bem-xjst a la biblioteca en la parte superior de React. Lo primero que llama la atención es la firma o, si lo prefiere, la sintaxis para describir los componentes.


¿Qué has hecho, hay JSX!


La primera versión se creó sobre la base de la herencia , una biblioteca que ayuda a implementar clases y herencia. Como algunos de ustedes recuerdan, en aquellos días, los prototipos prototipos en JavaScript no tenían clases, no había super . En general, todavía están ausentes, más precisamente, estas no son las clases que primero vienen a la mente. inherit hizo todo lo que las clases en el estándar ES2015 pueden hacer ahora, y lo que se considera magia negra: herencia múltiple y fusión de prototipos en lugar de reconstruir la cadena, lo que afecta positivamente el rendimiento. No se equivocará si cree que parece tener sentido como hereda en Node.js , pero funcionan de manera diferente.


A continuación se muestra un ejemplo de la sintaxis de las plantillas 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> 

Las plantillas de dispositivo para componentes más complejos se pueden encontrar aquí .


Dado que una clase es un objeto, y trabajar con objetos en JavaScript es más conveniente, la sintaxis es apropiada. La sintaxis luego migró a su mente maestra bem-xjst .


La biblioteca era un depósito global de declaraciones de objetos: los resultados de ejecutar la función decl , partes de entidades: un bloque, elemento o modificador. BEM proporciona un mecanismo de nomenclatura único y, por lo tanto, es adecuado para crear claves en una bóveda. El componente React resultante se pegó en su lugar de uso. El truco es que el decl funcionó al importar el módulo. Esto permitió indicar qué partes del componente se necesitan en cada lugar en particular utilizando una simple lista de importaciones. Pero recuerde: los componentes son complejos, hay muchas partes, la lista de importaciones es larga, los desarrolladores son flojos.


Importar Magia


Como puede ver, en los ejemplos de código hay líneas que import AppHeader from 'b:App e:Header' .


¡Rompiste el estándar! ¡Es imposible! ¡Simplemente no funcionará!


En primer lugar, el estándar de importación no funciona con términos en el espíritu "debe haber una ruta a un módulo real en la línea de importación". En segundo lugar, es el azúcar sintáctico que se convirtió con Babel. En tercer lugar, extrañas construcciones de puntuación de import txt from 'raw-loader!./file.txt'; para webpack import txt from 'raw-loader!./file.txt'; por alguna razón no molestaron a nadie.
Entonces, nuestro bloque se presenta en dos plataformas: desktop , touch .


 import Hello from 'b:Hello'; //     : var Hello = [ require('path/to/desktop/Hello/Hello.js'), require('path/to/touch/Hello/Hello.js') ][0].applyDecls(); 

Aquí, el código importará secuencialmente todas las definiciones de componentes Hello, y luego llamará a una función applyDeclsque pega todas las declaraciones de bloque desde el repositorio global inherity crea un nuevo componente React que es único para un lugar específico en el proyecto.


Aquí puede encontrar un complemento para Babel que realiza esta conversión . Y el cargador para webpack, que estaba buscando definiciones de componentes en el sistema de archivos, está aquí .


Al final, lo que era bueno:


  • breve sintaxis declarativa de plantillas que le permite redefinir diferentes partes del componente en cualquier parte del proyecto;
  • sin cadenas prototipo en herencia;
  • Componente de reacción único para cada lugar de uso.

Y eso fue malo:


  • TypeScript/Flow;
  • React- ;
  • - ;
  • .

v2.0.0


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'; } //   Text class Text extends Elem { block = 'Button'; elem = 'Text'; tag() { return 'span'; } } //   Button class Button<T extends IModsProps> extends Block<T> { block = 'Button'; tag() { return 'button'; } mods() { return { type: this.props.type }; } content() { return ( <Text>{this.props.children}</Text> ); } } //    Button,    type   link class ButtonLink extends Button<IModsProps> { static mod = ({ type }: any) => type === 'link'; tag() { return 'a'; } mods() { return { type: this.props.type }; } attrs() { return { href: 'www.yandex.ru' }; } } //   Button  ButtonLink const ButtonView = withMods(Button, ButtonLink); ReactDOM.render( <React.Fragment> <ButtonView type='button'>Click me</ButtonView> <ButtonView type='link'>Click me</ButtonView> </React.Fragment>, document.getElementById('root') ); 

, .


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:


  • CSS-.
  • (, );

. . React.ComponentType -. HOC compose .


.


dependency injection, React.ContextAPI . , , . , . DI — HOC, . . , , .


, , . , , 4 , 1.5Kb .


. Gracias a quienes leyeron hasta el final. , React . .

Source: https://habr.com/ru/post/438598/


All Articles