Nueva interfaz de Odnoklassniki: lanzamiento de React en Java. Parte II



Continuamos la historia de cómo, dentro de Odnoklassniki, con la ayuda de GraalVM, logramos hacer amigos con Java y JavaScript y comenzar a migrar a un gran sistema con mucho código heredado.

En la segunda parte del artículo, hablaremos en detalle sobre el lanzamiento, el ensamblaje y la integración de aplicaciones en la nueva pila, profundizaremos en los detalles de su trabajo tanto en el cliente como en el servidor, así como discutiremos las dificultades encontradas en nuestro camino y describiremos soluciones para ayudarlos a superar .

Si no has leído la primera parteRecomiendo mucho hacer esto. A partir de él, aprenderá sobre la historia de la interfaz en Odnoklassniki y se familiarizará con sus características históricas, recorrerá el camino para encontrar una solución a los problemas que se han acumulado en nuestros 13 años de proyecto, y al final se sumergirá en las características técnicas de la implementación del servidor de la decisión que tomamos.

Configuración de la interfaz de usuario


Para escribir el código de la interfaz de usuario, elegimos las herramientas más avanzadas: reaccionar junto con MobX, módulos CSS, ESLint, TypeScript, Lerna. Todo esto se recopila utilizando Webpack.



Arquitectura de la aplicación


Como se escribió en la parte anterior de este artículo, para implementar la migración gradual, insertaremos nuevos componentes en el sitio en elementos DOM con nombres personalizados que funcionarán dentro de la nueva pila de interfaz de usuario, mientras que para el resto del sitio se verá como un elemento DOM con su API El contenido de estos elementos se puede representar en el servidor.

¿Qué es? En el interior hay una aplicación MVC moderna, moderna y moderna que se ejecuta en React y proporciona la API DOM externa estándar: atributos, métodos en este elemento DOM y eventos.



Para ejecutar dichos componentes, hemos desarrollado un mecanismo especial. ¿Qué está haciendo? En primer lugar, inicializa la aplicación según su descripción. En segundo lugar, vincula el componente al nodo DOM específico en el que se inicia. También hay dos motores (para el cliente y para el servidor) que pueden encontrar y representar estos componentes.



¿Por qué se necesita esto? El hecho es que cuando todo el sitio se crea en React, generalmente el componente del sitio se representa en el elemento raíz de la página, y este componente no importa lo que está afuera, pero solo lo que está adentro es interesante.

En nuestro caso, todo es más complicado: varias aplicaciones necesitan la oportunidad de decirle a nuestra página en el sitio "Estoy, y algo está cambiando en mí". Por ejemplo, el calendario necesita lanzar un evento en el que el usuario hizo clic en el botón, y la fecha ha cambiado, o afuera necesita la habilidad para que dentro del calendario pueda cambiar la fecha. Para esto, el motor de la aplicación implementa fachadas en la funcionalidad básica de la aplicación.

Al entregar un componente a un cliente, es necesario que el motor del sitio anterior pueda iniciar este componente. Para hacer esto, durante la compilación, se recopila la información necesaria para su lanzamiento.

{
    "events-calendar": {
        "bundleName": "events-calendar",
        "js": "events-calendar-h4h5m.js",
        "css": "events-calendar-h4h5m.css"
    }
}


Se agregan marcadores especiales a los atributos de la etiqueta del componente, que dicen que esta aplicación es de un nuevo tipo, su código se puede tomar de un archivo JS específico. Al mismo tiempo, tiene sus propios atributos que son necesarios para inicializar este componente: forman el estado inicial del componente en la tienda.

<events-calendar	data-module="react-loader"
			data-bundle="events-calendar.js"
			date=".."
			marks="[{..}]"
			…
/>


Para la rehidratación, no se utiliza un reparto del estado de la aplicación, sino atributos, lo que permite ahorrar en el tráfico. Vienen en forma normalizada y, como regla, son más pequeños que la tienda que crea la aplicación. Al mismo tiempo, el tiempo para recrear la tienda a partir de los atributos en el cliente es corto, por lo que generalmente se pueden descuidar.

Por ejemplo, para el calendario, los atributos solo tienen una fecha resaltada, y la tienda ya tiene una matriz con información completa para el mes. Obviamente, no tiene sentido transferirlo desde el servidor.

¿Cómo ejecutar el código?


El concepto se probó en funciones simples que dan una línea para el servidor o escriben innerHTML para el cliente. Pero en el código real hay módulos y TypeScript.

Existen soluciones estándar para el cliente, por ejemplo, la recopilación de código mediante Webpack, que en sí mismo lo tritura todo y se lo entrega al cliente en forma de un paquete de paquetes. ¿Y qué hacer para el servidor cuando se usa GraalVM?



Consideremos dos opciones. El primero es escribir TypeScript en JavaScript, como lo hacen para Node.js. Desafortunadamente, esta opción no funciona en nuestra configuración cuando JavaScript es el idioma invitado en GraalVM. En este caso, JavaScript no tiene un sistema modular, ni siquiera asincrónico. Porque la modularidad y el trabajo con asincronía proporciona un tiempo de ejecución específico: NodeJS o un navegador. Y en nuestro caso, el servidor tiene JavaScript que solo puede ejecutar código sincrónicamente.

La segunda opción: simplemente puede ejecutar el código del servidor desde los mismos archivos que se recopilaron para el cliente. Y esta opción funciona. Pero existe el problema de que el servidor necesita otras implementaciones para varios métodos. Por ejemplo, la función renderToString () se llamará en el servidor para representar el componente y ReactDOM.render () en el cliente. O otro ejemplo del artículo anterior: para obtener textos y configuraciones en el servidor, se llamará a la función que proporciona Java, y en el cliente será una implementación en JS.

Como solución a este problema, puede usar alias de Webpack. Le permiten crear dos implementaciones de la clase que necesitamos: para el cliente y el servidor. Luego, en los archivos de configuración para el cliente y el servidor, especifique la implementación adecuada.



Pero dos archivos de configuración son dos ensamblajes. Cada vez, recopilar todo por separado para el servidor y para el cliente es largo y difícil de soportar.

Debe crear dicha configuración para que todo se recopile de una vez.

Configuración del paquete web para ejecutar JS en el servidor y el cliente


Para encontrar una solución a este problema, veamos en qué partes consiste el proyecto:



Primero, el proyecto tiene tiempo de ejecución de terceros (proveedores), lo mismo para el cliente y para el servidor. Casi nunca cambia. Rantime se puede dar al usuario, y se almacenará en caché en el cliente hasta que actualicemos la versión de la biblioteca de terceros.

En segundo lugar, está nuestro tiempo de ejecución (núcleo), que garantiza el lanzamiento de la aplicación. Tiene métodos con diferentes implementaciones para el cliente y el servidor. Por ejemplo, obtener textos de localización, configuraciones, etc. Este tiempo de ejecución también cambia con poca frecuencia.

En tercer lugar, hay un código de componente. Es lo mismo para el cliente y para el servidor, lo que le permite depurar el código de la aplicación en el navegador sin iniciar el servidor. Si algo salió mal en el cliente, puede ver los errores en la consola del navegador, recordar todo y asegurarse de que no habrá errores al iniciar en el servidor.

En total, se obtienen tres partes que deben ensamblarse. Queremos:
  • Configure por separado el ensamblaje de cada parte.
  • Anote las dependencias entre ellos para que cada parte no caiga en lo que está en la otra.
  • Recoge todo en una sola pasada.


¿Cómo describir por separado las partes en que consistirá el ensamblaje? Hay una configuración múltiple en el paquete web: simplemente regala una serie de exportaciones de los módulos incluidos en cada parte.

module.exports = [{
  entry: './vendors.js',
}, {
  entry: './core.js'
}, {
 entry: './app.js'
}];


Todo estaría bien, pero en cada una de estas partes se duplicará el código de los módulos de los que depende esta parte:



Afortunadamente, en el conjunto básico de complementos de paquete web hay DllPlugin , que le permite obtener una lista de los módulos incluidos para cada parte ensamblada. Por ejemplo, para el proveedor, puede averiguar qué módulos específicos se incluyen en esta parte.

Al construir otra parte, por ejemplo, bibliotecas centrales, podemos decir que dependen de la parte del proveedor.



Luego, durante el ensamblaje del paquete web, DllPlugin verá el núcleo dependiendo de alguna biblioteca que ya esté en el proveedor, y no lo agregará al núcleo, sino que simplemente le pondrá un enlace.

Como resultado, se ensamblan tres piezas a la vez y dependen unas de otras. Cuando la primera aplicación se descarga al cliente, las bibliotecas principales y de tiempo de ejecución se guardarán en la memoria caché del navegador. Y dado que Odnoklassniki es un sitio, la pestaña con la que el usuario puede abrir "para siempre", el desplazamiento se producirá muy raramente. En la mayoría de los casos, con lanzamientos de nuevas versiones del sitio, solo se actualizará el código de la aplicación.

Entrega de recursos


Considere el problema con el ejemplo de trabajar con textos localizados que se almacenan en una base de datos separada.

Si anteriormente en algún lugar del servidor necesitaba texto en el componente, podría llamar a la función para obtener el texto.

const pkg = l10n('smiles');

<div>
    : { pkg.getText('title') }
</div>


Obtener texto en el servidor no es difícil, porque la aplicación del servidor puede realizar una solicitud rápida a la base de datos o incluso almacenar en caché todos los textos en la memoria.

¿Cómo obtener textos en componentes en una reacción que se representan en un servidor en GraalVM?

Como se discutió en la primera parte del artículo, en el contexto JS, puede agregar métodos al objeto global al que desea acceder desde JavaScript. Se decidió hacer una clase con todos los métodos disponibles para JavaScript.

public class ServerMethods {
    
    /**
     *     
     */
    public String getText(String pkg, String key) {
    }
    
}


Luego ponga una instancia de esta clase en el contexto global de JavaScript:

//     Java   
js.putMember("serverMethods", serverMethods);


Como resultado, desde JavaScript en la implementación del servidor, simplemente llamamos a la función:

function getText(pkg: string, key: string): string {
    return global.serverMethods.getText(pkg, key);
}


De hecho, esta será una llamada de función en Java que devolverá el texto solicitado. Interacción síncrona directa y sin llamadas HTTP.

Desafortunadamente, en el cliente, lleva mucho tiempo revisar HTTP y recibir textos para cada llamada a la función de inserción de texto en los componentes. Puede descargar previamente todos los textos al cliente, pero solo los textos pesan decenas de megabytes, y existen otros tipos de recursos.



El usuario se cansará de esperar hasta que todo se descargue antes de iniciar la aplicación. Por lo tanto, este método no es adecuado.

Me gustaría recibir solo los textos que se necesitan en una aplicación en particular. Nuestros textos se dividen en paquetes. Por lo tanto, puede recopilar los paquetes necesarios para la aplicación y descargarlos junto con el paquete. Cuando se inicia la aplicación, todos los textos ya estarán en la memoria caché del cliente.

¿Cómo saber qué textos necesita una aplicación?

Acordamos que los paquetes de textos en el código se obtienen llamando a la función l10n (), en la cual el nombre del paquete se transmite SOLAMENTE en forma de un literal de cadena:

const pkg = l10n('smiles');

<div>
    { pkg.getLMsg('title') }
</div>


Escribimos un complemento de paquete web que, al analizar el árbol AST del código de componente, encuentra todas las llamadas a la función l10n () y recopila nombres de paquetes de los argumentos. Del mismo modo, el complemento recopila información sobre otros tipos de recursos que necesita la aplicación.

En la salida después del ensamblaje para cada aplicación, obtenemos una configuración con sus recursos:

{
    "events-calendar": {
       "pkg":  [
           "calendar",
           "dates"
       ],
       "cfg":  [
           "config1",
           "config2"
       ],
       "bundleName":  "events-calendar",
       "js":  "events-calendar.js",
       "css":  "events-calendar.css",
    }
}


Y, por supuesto, no debemos olvidarnos de actualizar los textos. Debido a que en el servidor todos los textos están siempre actualizados, y el cliente necesita un mecanismo de actualización de caché separado, por ejemplo, vigilante o inserción.

Código antiguo en nuevo


Con una transición suave, surge el problema de reutilizar el código antiguo en componentes nuevos, porque hay componentes grandes y complejos (por ejemplo, un reproductor de video) que tomará mucho tiempo reescribir, y ahora debe usarlos en la nueva pila.



¿Cuáles son los problemas?

  • El sitio antiguo y las nuevas aplicaciones React tienen ciclos de vida completamente diferentes.
  • Si pega el código de la muestra anterior dentro de la aplicación React, este código no se iniciará porque React no sabe cómo activarlo.
  • Debido a los diferentes ciclos de vida, React y el motor antiguo pueden intentar simultáneamente modificar el contenido del código anterior, lo que puede causar efectos secundarios desagradables.


Para resolver estos problemas, se asignó una clase base común para componentes que contienen código antiguo. La clase permite a los herederos coordinar los ciclos de vida de React y las aplicaciones de estilo antiguo.

export class OldCodeBase<T> extends React.Component<T> {

    ref: React.RefObject<HTMLElement> = React.createRef();

    componentDidMount() {
        //       DOM
        this.props.activate(this.ref.current!); 
    }

    componentWillUnmount() {
        //       DOM
        this.props.deactivate(this.ref.current!); 
    }

    shouldComponentUpdate() {
        // React     , 
        //   React-. 
        //     .
        return false;
    }

    render() {
        return (
            <div ref={this.ref}></div>
        );
    }
}


La clase le permite crear fragmentos de código que funcionan a la antigua usanza o destruirlos, mientras que no habrá interacción simultánea con ellos.

Pegue el código antiguo en el servidor


En la práctica, existe la necesidad de componentes de envoltura (por ejemplo, ventanas emergentes), cuyo contenido puede ser cualquiera, incluidos los creados con tecnologías antiguas. Debe descubrir cómo incrustar cualquier código en el servidor dentro de dichos componentes.

En un artículo anterior, hablamos sobre el uso de atributos para pasar parámetros a nuevos componentes en el cliente y el servidor.

<cool-app users="[1,2,3]" />


Y ahora todavía queremos insertar un trozo de marcado allí, que en sentido no es un atributo. Para esto, se decidió utilizar un sistema de tragamonedas.

<cool-app>
    <ui:part id="old-code">
        <div>old component</div>
    </ui:part>
</cool-app>


Como puede ver en el ejemplo anterior, dentro del código del componente cool-app, se describe una ranura de código antiguo que contiene componentes antiguos. Luego, dentro del componente de reacción, se indica el lugar donde desea pegar el contenido de esta ranura:

render() {
    return (
        <div>
            <UiPart id="old-code" />
        </div>
    );
}


El motor del servidor procesa este componente de reacción y enmarca el contenido de la ranura en la etiqueta <ui-part>, asignándole el atributo data-part-id = "old-code".

<cool-app>
    <div>
        <ui-part data-part-id="old-code">
            old code
        </ui-part>
    </div>
</cool-app>


Si la representación del lado del servidor de JS en GraalVM no se ajusta al tiempo de espera, entonces recurrimos a la representación del cliente. Para hacer esto, el motor en el servidor solo proporciona ranuras, enmarcandolos en la etiqueta de la plantilla para que el navegador no interactúe con su código.

<cool-app>
    <template>
        <ui-part data-part-id="old-code">
            old code
        </ui-part>
    </template>
</cool-app>


¿Qué está pasando en el cliente? El motor del cliente simplemente escanea el código del componente, recopila las etiquetas <ui-part>, recibe su contenido en forma de cadenas y las pasa a la función de representación junto con el resto de los parámetros.

var tagName = 'cool-app';
var reactComponent = components[tagName];
reactComponent.render({
       tagName: tagName,
       attrs: attrs,
       parts: parts,
       node: element
});


El código del componente que inserta las ranuras en la ubicación deseada es el siguiente:

export class UiPart extends OldCodeBase<IProps> {

	render() {
		const id = this.props.id;
		const parts = this.props.parts;

		if (!parts.hasOwnProperty(id)) {
			return null;
		}

		return React.createElement('ui-part', {
			'data-part-id': id,
			ref: this.ref,
			dangerouslySetInnerHTML: { __html: parts[id] }
		});
	}
}


Al mismo tiempo, se hereda de la clase OldCodeBase, que resuelve los problemas de interacción entre la pila antigua y la nueva.



Ahora puede escribir una ventana emergente y llenarla utilizando la nueva pila o solicitud del servidor utilizando el enfoque anterior. En este caso, los componentes funcionarán correctamente.

Esto le permite migrar gradualmente los componentes del sitio a una nueva pila.
Solo este fue uno de los requisitos principales para la nueva interfaz.

Resumen


Todos se preguntan qué tan rápido funciona GraalVM. Los desarrolladores de Odnoklassniki realizaron varias pruebas con las aplicaciones React.

Una función simple que devuelve una cadena después del calentamiento tarda aproximadamente 1 microsegundo.

Componentes (nuevamente después del calentamiento): de 0,5 a 6 milisegundos, según su tamaño.

GraalVM acelera más lentamente que V8. Pero para el momento de su calentamiento, la situación se ha suavizado gracias al respaldo para la representación del cliente. Como hay tantos usuarios, la máquina virtual se calienta rápidamente.

¿Qué lograste hacer?



  • Ejecute JavaScript en el servidor en el mundo Java de Classmates.
  • Crea un código isomorfo para la interfaz de usuario.
  • Use una pila moderna que todos los vendedores de front-end conozcan.
  • Cree una plataforma común y un enfoque único para escribir la interfaz de usuario.
  • Comience una transición sin problemas sin complicar la operación y sin ralentizar la representación del servidor.


Esperamos que las experiencias de Odnoklassniki y los ejemplos le sean útiles y los encuentre para usar en su trabajo.

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


All Articles