Novo frontend do Odnoklassniki: iniciando o React em Java. parte II



Continuamos a história de como, dentro do Odnoklassniki, usando o GraalVM, conseguimos fazer amizade com Java e JavaScript e começamos a migrar para um sistema enorme com muito código legado.

Na segunda parte do artigo, falaremos em detalhes sobre o lançamento, a montagem e a integração de aplicativos na nova pilha, abordaremos as especificidades de seus trabalhos no cliente e no servidor, além de discutirmos as dificuldades encontradas no caminho e descreveremos soluções para ajudá-los a superar .

Se você não leu a primeira parteEu recomendo fazer isso. A partir disso, você aprenderá sobre a história do front-end em Odnoklassniki e se familiarizar com seus recursos históricos, percorrerá o caminho de encontrar uma solução para os problemas que se acumularam em nossos 13 anos de projeto e, no final, mergulhará nos recursos técnicos da implementação do servidor da decisão que tomamos.

Configuração da interface do usuário


Para escrever o código da interface do usuário, escolhemos as ferramentas mais avançadas: Reaja junto com MobX, CSS Modules, ESLint, TypeScript, Lerna. Tudo isso é coletado usando o Webpack.



Arquitetura de aplicativos


Como foi escrito na parte anterior deste artigo, para implementar a migração gradual, inseriremos novos componentes no site em elementos DOM com nomes personalizados que funcionarão dentro da nova pilha da interface do usuário, enquanto no restante do site parecerá um elemento DOM com sua API. O conteúdo desses elementos pode ser renderizado no servidor.

O que é isso? Lá dentro, há um aplicativo MVC moderno, bacana e moderno, executando o React e fornecendo a API DOM padrão externa: atributos, métodos neste elemento DOM e eventos.



Para executar esses componentes, desenvolvemos um mecanismo especial. O que ele está fazendo? Inicialmente, inicializa o aplicativo de acordo com sua descrição. Em segundo lugar, vincula o componente ao nó DOM específico no qual é iniciado. Também existem dois mecanismos (para o cliente e o servidor) que podem encontrar e renderizar esses componentes.



Por que isso é necessário? O fato é que, quando todo o site é criado no React, geralmente o componente do site é renderizado no elemento raiz da página, e esse componente não importa o que está fora, mas apenas o que está dentro é interessante.

No nosso caso, tudo é mais complicado: várias aplicações precisam da oportunidade de dizer à nossa página no site "Eu sou, e algo está mudando em mim". Por exemplo, o calendário precisa lançar um evento em que o usuário clicou no botão, e a data foi alterada ou fora dela, você precisa da capacidade para que, dentro do calendário, você possa alterar a data. Para isso, o mecanismo de aplicativo implementa fachadas na funcionalidade básica do aplicativo.

Ao entregar um componente para um cliente, é necessário que o mecanismo do site antigo possa iniciar esse componente. Para fazer isso, durante a construção, as informações necessárias para seu lançamento são coletadas.

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


Marcadores especiais são adicionados aos atributos da tag do componente, que dizem que esse aplicativo é de um novo tipo, seu código pode ser obtido de um arquivo JS específico. Ao mesmo tempo, possui seus próprios atributos necessários para inicializar esse componente: eles formam o estado inicial do componente na loja.

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


Para reidratação, não é uma conversão do estado do aplicativo usado, mas atributos, que permitem economizar no tráfego. Eles vêm em uma forma normalizada e, em regra, são menores que o armazenamento que o aplicativo cria. Ao mesmo tempo, o tempo para recriar a loja a partir dos atributos no cliente é curto, portanto eles geralmente podem ser negligenciados.

Por exemplo, para o calendário, os atributos têm apenas uma data destacada e a loja já possui uma matriz com informações completas para o mês. Obviamente, não faz sentido transferi-lo do servidor.

Como executar o código?


O conceito foi testado em funções simples que fornecem uma linha para o servidor ou escrevem innerHTML para o cliente. Mas no código real existem módulos e TypeScript.

Existem soluções padrão para o cliente, por exemplo, coletando código usando o Webpack, que processa tudo e o fornece ao cliente na forma de um pacote configurável. E o que fazer para o servidor ao usar o GraalVM?



Vamos considerar duas opções. A primeira é digitar TypeScript em JavaScript, como acontece no Node.js. Infelizmente, esta opção não funciona em nossa configuração quando o JavaScript é o idioma do convidado no GraalVM. Nesse caso, o JavaScript não possui um sistema modular nem assincronia. Porque a modularidade e o trabalho com assincronia fornecem um tempo de execução específico: NodeJS ou um navegador. E, no nosso caso, o servidor possui JavaScript que só pode executar código de forma síncrona.

A segunda opção - você pode simplesmente executar no código do servidor a partir dos mesmos arquivos que foram coletados para o cliente. E esta opção funciona. Mas há um problema que o servidor precisa de outras implementações para vários métodos. Por exemplo, a função renderToString () será chamada no servidor para renderizar o componente e ReactDOM.render () no cliente. Ou outro exemplo do artigo anterior: para obter textos e configurações no servidor, a função que Java fornece será chamada e, no cliente, será uma implementação em JS.

Como solução para esse problema, você pode usar aliases do Webpack. Eles permitem que você crie duas implementações da classe que precisamos: para o cliente e o servidor. Em seguida, nos arquivos de configuração do cliente e servidor, especifique a implementação apropriada.



Mas dois arquivos de configuração são dois assemblies. Cada vez, coletar tudo separadamente para o servidor e para o cliente é longo e difícil no suporte.

Você precisa criar essa configuração para que tudo seja coletado de uma só vez.

Configuração do Webpack para executar JS no servidor e cliente


Para encontrar uma solução para esse problema, vamos ver em quais partes o projeto consiste:



Primeiro, o projeto possui tempo de execução de terceiros (fornecedores), o mesmo para o cliente e o servidor. Quase nunca muda. O Rantime pode ser fornecido ao usuário e ele será armazenado em cache no cliente até atualizarmos a versão da biblioteca de terceiros.

Em segundo lugar, existe o nosso tempo de execução (core), que garante o lançamento do aplicativo. Possui métodos com diferentes implementações para o cliente e o servidor. Por exemplo, obtendo textos de localização, configurações e assim por diante. Esse tempo de execução também muda com pouca frequência.

Em terceiro lugar, há um código de componente. É o mesmo para o cliente e o servidor, o que permite depurar o código do aplicativo no navegador sem iniciar o servidor. Se algo der errado no cliente, você poderá ver os erros no console do navegador, lembre-se de tudo e verifique se não haverá erros ao iniciar no servidor.

No total, são obtidas três partes que precisam ser montadas. Nós queremos:
  • Configure separadamente a montagem de cada peça.
  • Coloque as dependências entre eles para que cada parte não caia no que está na outra.
  • Colete tudo de uma só vez.


Como descrever separadamente as partes em que a montagem será composta? Há uma configuração multiconfigurada no webpack: você simplesmente distribui uma matriz de exportações dos módulos incluídos em cada parte.

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


Tudo ficaria bem, mas em cada uma dessas partes o código dos módulos dos quais essa parte depende será duplicado:



Felizmente, no conjunto básico de plugins do webpack, há o DllPlugin , que permite obter uma lista dos módulos incluídos para cada peça montada. Por exemplo, para fornecedor, você pode descobrir quais módulos específicos estão incluídos nesta parte.

Ao construir outra parte, por exemplo, bibliotecas principais, podemos dizer que elas dependem da parte do fornecedor.



Em seguida, durante a montagem do webpack, o DllPlugin verá o núcleo, dependendo de alguma biblioteca que já esteja no fornecedor, e não o adicionará ao núcleo, mas simplesmente colocará um link para ele.

Como resultado, três peças são montadas por vez e dependem uma da outra. Quando o primeiro aplicativo é baixado para o cliente, o tempo de execução e as bibliotecas principais são salvas no cache do navegador. E como o Odnoklassniki é um site, a guia com a qual o usuário pode abrir "para sempre", a exclusão ocorrerá muito raramente. Na maioria dos casos, com os lançamentos de novas versões do site, apenas o código do aplicativo será atualizado.

Entrega de Recursos


Considere o problema pelo exemplo de trabalho com textos localizados armazenados em um banco de dados separado.

Se anteriormente, em algum lugar do servidor, você precisava de texto no componente, poderia chamar a função para obter o texto.

const pkg = l10n('smiles');

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


Obter texto no servidor não é difícil, porque o aplicativo do servidor pode fazer uma solicitação rápida ao banco de dados ou até armazenar em cache todos os textos na memória.

Como obter textos em componentes em uma reação que são renderizados em um servidor no GraalVM?

Conforme discutido na primeira parte do artigo, no contexto JS, você pode adicionar métodos ao objeto global que deseja acessar do JavaScript. Decidiu-se criar uma classe com todos os métodos disponíveis para JavaScript.

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


Em seguida, coloque uma instância dessa classe no contexto global do JavaScript:

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


Como resultado, do JavaScript na implementação do servidor, simplesmente chamamos a função:

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


De fato, essa será uma chamada de função em Java que retornará o texto solicitado. Interação síncrona direta e sem chamadas HTTP.

Infelizmente, no cliente, leva muito tempo para revisar o HTTP e receber textos para cada chamada para a função de inserção de texto nos componentes. Você pode fazer o pré-download de todos os textos para o cliente, mas apenas os textos pesam dezenas de megabytes e existem outros tipos de recursos.



O usuário se cansará de esperar até que tudo seja baixado antes de iniciar o aplicativo. Portanto, este método não é adequado.

Gostaria de receber apenas os textos necessários em uma aplicação específica. Nossos textos são divididos em pacotes. Portanto, você pode coletar os pacotes necessários para o aplicativo e baixá-los junto com o pacote. Quando o aplicativo iniciar, todos os textos já estarão no cache do cliente.

Como descobrir quais textos um aplicativo precisa?

Entramos em um acordo de que pacotes de textos no código são obtidos chamando a função l10n (), na qual o nome do pacote é transmitido SOMENTE na forma de uma string literal:

const pkg = l10n('smiles');

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


Nós escrevemos um plugin webpack que, analisando a árvore AST da árvore de códigos de componentes, localiza todas as chamadas para a função l10n () e coleta nomes de pacotes dos argumentos. Da mesma forma, o plug-in coleta informações sobre outros tipos de recursos necessários ao aplicativo.

Na saída após a montagem para cada aplicativo, obtemos uma configuração com seus recursos:

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


E, é claro, não devemos esquecer a atualização dos textos. Como no servidor, todos os textos estão sempre atualizados e o cliente precisa de um mecanismo de atualização de cache separado, por exemplo, observador ou push.

Código antigo em novo


Com uma transição suave, surge o problema de reutilizar o código antigo em novos componentes, porque existem componentes grandes e complexos (por exemplo, um reprodutor de vídeo), reescrições que levarão muito tempo e você precisará usá-los agora na nova pilha.



Quais são os problemas?

  • O site antigo e os novos aplicativos React têm ciclos de vida completamente diferentes.
  • Se você colar o código da amostra antiga no aplicativo React, esse código não será iniciado, porque o React não sabe como ativá-lo.
  • Devido a diferentes ciclos de vida, o React e o mecanismo antigo podem tentar modificar simultaneamente o conteúdo do código antigo, o que pode causar efeitos colaterais desagradáveis.


Para resolver esses problemas, uma classe base comum foi alocada para componentes contendo código antigo. A classe permite que os herdeiros coordenem os ciclos de vida dos aplicativos React e de estilo antigo.

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


A classe permite que você crie trechos de código que funcionam da maneira antiga ou destrua, enquanto não haverá interação simultânea com eles.

Cole o código antigo no servidor


Na prática, há a necessidade de componentes de wrapper (por exemplo, pop-ups), cujo conteúdo pode ser qualquer, incluindo aqueles criados usando tecnologias antigas. Você precisa descobrir como incorporar qualquer código no servidor dentro desses componentes.

Em um artigo anterior, falamos sobre o uso de atributos para passar parâmetros para novos componentes no cliente e no servidor.

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


E agora ainda queremos inserir um pedaço de marcação lá, o que em sentido não é um atributo. Para isso, decidiu-se usar um sistema de slots.

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


Como você pode ver no exemplo acima, dentro do código do componente do aplicativo legal, é descrito um slot de código antigo contendo componentes antigos. Em seguida, dentro do componente react, o local em que você deseja colar o conteúdo deste slot é indicado:

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


O mecanismo do servidor renderiza esse componente de reação e enquadra o conteúdo do slot na tag <ui-part>, atribuindo o atributo data-part-id = "old-code" a ele.

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


Se a renderização do servidor de JS no GraalVM não couber no tempo limite, fazemos o fallback da renderização do cliente. Para fazer isso, o mecanismo no servidor fornece apenas slots, enquadrando-os na tag template, para que o navegador não interaja com seu código.

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


O que está acontecendo no cliente? O mecanismo do cliente simplesmente varre o código do componente, coleta as tags <ui-part>, recebe seu conteúdo como seqüências de caracteres e as passa para a função de renderização junto com o restante dos parâmetros.

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


O código do componente que insere os slots no local desejado é o seguinte:

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


Ao mesmo tempo, é herdado da classe OldCodeBase, que resolve os problemas de interação entre a pilha antiga e a nova.



Agora você pode escrever um pop-up e preenchê-lo usando a nova pilha ou solicitação do servidor usando a abordagem antiga. Nesse caso, os componentes funcionarão corretamente.

Isso permite migrar gradualmente os componentes do site para uma nova pilha.
Apenas esse foi um dos principais requisitos para o novo frontend.

Sumário


Todo mundo está se perguntando o quão rápido o GraalVM funciona. Os desenvolvedores do Odnoklassniki realizaram vários testes com os aplicativos React.

Uma função simples que retorna uma string após o aquecimento leva cerca de 1 microssegundo.

Componentes (novamente após o aquecimento) - de 0,5 a 6 milissegundos, dependendo do tamanho.

O GraalVM acelera mais lentamente que o V8. Mas, durante o período de aquecimento, a situação é suavizada graças ao fallback da renderização do cliente. Como existem muitos usuários, a máquina virtual esquenta rapidamente.

O que você conseguiu fazer



  • Execute o JavaScript no servidor no mundo Java dos Classmates.
  • Crie código isomórfico para a interface do usuário.
  • Use uma pilha moderna que todos os fornecedores de front-end conhecem.
  • Crie uma plataforma comum e uma abordagem única para escrever a interface do usuário.
  • Inicie uma transição suave sem complicar a operação e sem diminuir a renderização do servidor.


Esperamos que as experiências e exemplos do Odnoklassniki sejam úteis para você e você os encontrará para usar em seu trabalho.

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


All Articles