Migração de Frankenstein: Abordagem agnóstica de estrutura (Parte 2)

Publicados: 2022-03-10
Resumo rápido ↬ Recentemente discutimos o que é “Frankenstein Migration”, comparamos com os tipos convencionais de migrações e mencionamos dois blocos de construção principais: microsserviços e Web Components . Também obtivemos uma base teórica de como funciona esse tipo de migração. Se você não leu ou esqueceu essa discussão, talvez queira voltar à Parte 1 primeiro porque isso ajuda a entender tudo o que abordaremos nesta segunda parte do artigo.

Neste artigo, testaremos toda a teoria realizando a migração passo a passo de um aplicativo, seguindo as recomendações da parte anterior. Para simplificar as coisas, reduzir incertezas, incógnitas e suposições desnecessárias, para o exemplo prático de migração, decidi demonstrar a prática em um aplicativo simples de tarefas.

É hora de testar a teoria
É hora de testar a teoria. (Visualização grande)

Em geral, presumo que você tenha uma boa compreensão de como funciona um aplicativo genérico de tarefas. Esse tipo de aplicativo atende muito bem às nossas necessidades: é previsível, mas possui um número mínimo viável de componentes necessários para demonstrar diferentes aspectos da migração de Frankenstein. No entanto, não importa o tamanho e a complexidade do seu aplicativo real, a abordagem é bem escalável e deve ser adequada para projetos de qualquer tamanho.

Uma visualização padrão de um aplicativo TodoMVC
Uma visualização padrão de um aplicativo TodoMVC (visualização grande)

Para este artigo, como ponto de partida, escolhi um aplicativo jQuery do projeto TodoMVC — um exemplo que já pode ser familiar para muitos de vocês. jQuery é legado o suficiente, pode refletir uma situação real com seus projetos e, o mais importante, requer manutenção e hacks significativos para alimentar um aplicativo dinâmico moderno. (Isso deve ser suficiente para considerar a migração para algo mais flexível.)

O que é esse “mais flexível” para o qual vamos migrar então? Para mostrar um caso altamente prático e útil na vida real, tive que escolher entre os dois frameworks mais populares atualmente: React e Vue. No entanto, qualquer que eu escolhesse, perderíamos alguns aspectos da outra direção.

Mais depois do salto! Continue lendo abaixo ↓

Então, nesta parte, estaremos executando os dois itens a seguir:

  • Uma migração de um aplicativo jQuery para React e
  • Uma migração de um aplicativo jQuery para Vue .
Nossos objetivos: resultados da migração para React e Vue
Nossos objetivos: resultados da migração para React e Vue. (Visualização grande)

Repositórios de código

Todo o código mencionado aqui está disponível publicamente e você pode acessá-lo sempre que quiser. Existem dois repositórios disponíveis para você jogar:

  • Frankenstein TodoMVC
    Este repositório contém aplicativos TodoMVC em diferentes frameworks/bibliotecas. Por exemplo, você pode encontrar ramificações como vue , angularjs , react e jquery neste repositório.
  • Demonstração de Frankenstein
    Ele contém várias ramificações, cada uma representando uma direção de migração específica entre aplicativos, disponíveis no primeiro repositório. Existem ramificações como migration/jquery-to-react e migration/jquery-to-vue , em particular, que abordaremos mais adiante.

Ambos os repositórios estão em andamento e novas ramificações com novos aplicativos e direções de migração devem ser adicionadas a eles regularmente. ( Você também pode contribuir! ) O histórico de commits em branches de migração é bem estruturado e pode servir como documentação adicional com ainda mais detalhes do que eu poderia abordar neste artigo.

Agora, vamos sujar as mãos! Temos um longo caminho pela frente, então não espere que seja um passeio tranquilo. Cabe a você decidir como deseja acompanhar este artigo, mas você pode fazer o seguinte:

  • Clone o branch jquery do repositório Frankenstein TodoMVC e siga rigorosamente todas as instruções abaixo.
  • Alternativamente, você pode abrir uma ramificação dedicada à migração para React ou migração para Vue do repositório Frankenstein Demo e acompanhar o histórico de commits.
  • Alternativamente, você pode relaxar e continuar lendo porque vou destacar o código mais crítico aqui, e é muito mais importante entender a mecânica do processo do que o código real.

Gostaria de mencionar mais uma vez que seguiremos rigorosamente os passos apresentados na primeira parte teórica do artigo.

Vamos mergulhar direto!

  1. Identificar microsserviços
  2. Permitir acesso de host para estrangeiro
  3. Escreva um microsserviço/componente alienígena
  4. Gravar wrapper de componente da Web em torno do serviço estrangeiro
  5. Substituir o serviço de host pelo componente da Web
  6. Enxágue e repita para todos os seus componentes
  7. Mudar para alienígena

1. Identifique os microsserviços

Como sugere a Parte 1, nesta etapa, temos que estruturar nosso aplicativo em pequenos serviços independentes dedicados a um trabalho específico . O leitor atento pode notar que nosso aplicativo de tarefas já é pequeno e independente e pode representar um único microsserviço por conta própria. É assim que eu mesmo trataria se esta aplicação vivesse em algum contexto mais amplo. Lembre-se, no entanto, que o processo de identificação de microsserviços é totalmente subjetivo e não há uma resposta correta.

Então, para ver o processo de migração de Frankenstein com mais detalhes, podemos dar um passo adiante e dividir esse aplicativo de tarefas em dois microsserviços independentes:

  1. Um campo de entrada para adicionar um novo item.
    Este serviço também pode conter o cabeçalho da aplicação, baseado puramente na proximidade de posicionamento destes elementos.
  2. Uma lista de itens já adicionados.
    Esse serviço é mais avançado e, junto com a própria lista, também contém ações como filtragem, ações do item da lista e assim por diante.
Aplicativo TodoMVC dividido em dois microsserviços independentes
Aplicativo TodoMVC dividido em dois microsserviços independentes. (Visualização grande)

Dica : Para verificar se os serviços escolhidos são genuinamente independentes, remova a marcação HTML, representando cada um desses serviços. Certifique-se de que as funções restantes ainda funcionam. No nosso caso, deve ser possível adicionar novas entradas em localStorage (que este aplicativo está usando como armazenamento) a partir do campo de entrada sem a lista, enquanto a lista ainda renderiza as entradas de localStorage mesmo se o campo de entrada estiver ausente. Se seu aplicativo gerar erros quando você remover a marcação de um microsserviço em potencial, dê uma olhada na seção “Refatorar se necessário” na Parte 1 para obter um exemplo de como lidar com esses casos.

Claro, poderíamos continuar e dividir o segundo serviço e a listagem dos itens ainda mais em microsserviços independentes para cada item específico. No entanto, pode ser muito granular para este exemplo. Então, por enquanto, concluímos que nosso aplicativo terá dois serviços; eles são independentes, e cada um deles trabalha para sua própria tarefa particular. Por isso, dividimos nosso aplicativo em microsserviços .

2. Permitir acesso de host para estrangeiro

Deixe-me lembrá-lo brevemente do que são.

  • Hospedeiro
    É assim que nosso aplicativo atual é chamado. Está escrito com a estrutura da qual estamos prestes a nos afastar . Neste caso em particular, nosso aplicativo jQuery.
  • Estrangeiro
    Simplificando, esta é uma reescrita gradual do Host no novo framework para o qual estamos prestes a mudar . Novamente, neste caso específico, é um aplicativo React ou Vue.

A regra geral ao dividir Host e Alien é que você deve ser capaz de desenvolver e implantar qualquer um deles sem quebrar o outro - a qualquer momento.

Manter Host e Alien independentes um do outro é crucial para a migração de Frankenstein. No entanto, isso torna a comunicação entre os dois um pouco desafiadora. Como permitimos que o Host acesse o Alien sem juntar os dois?

Adicionando Alien como um submódulo do seu host

Embora existam várias maneiras de obter a configuração de que precisamos, a forma mais simples de organizar seu projeto para atender a esse critério provavelmente é git submodules. É isso que vamos usar neste artigo. Vou deixar para você ler atentamente sobre como os submódulos no git funcionam para entender as limitações e armadilhas dessa estrutura.

Os princípios gerais da arquitetura do nosso projeto com submódulos git devem ficar assim:

  • Ambos Host e Alien são independentes e são mantidos em repositórios git separados;
  • O host faz referência ao Alien como um submódulo. Nesse estágio, o Host escolhe um estado específico (commit) de Alien e o adiciona como, aparentemente, uma subpasta na estrutura de pastas do Host.
React TodoMVC adicionado como um submódulo git no aplicativo jQuery TodoMVC
React TodoMVC adicionado como um submódulo git no aplicativo jQuery TodoMVC. (Visualização grande)

O processo de adição de um submódulo é o mesmo para qualquer aplicativo. Ensinar git submodules está além do escopo deste artigo e não está diretamente relacionado à própria migração de Frankenstein. Então, vamos dar uma breve olhada nos exemplos possíveis.

Nos trechos abaixo, usamos a direção React como exemplo. Para qualquer outra direção de migração, substitua react pelo nome de uma ramificação do Frankenstein TodoMVC ou ajuste os valores personalizados quando necessário.

Se você seguir usando o aplicativo jQuery TodoMVC original:

 $ git submodule add -b react [email protected]:mishunov/frankenstein-todomvc.git react $ git submodule update --remote $ cd react $ npm i

Se você seguir a ramificação migration/jquery-to-react (ou qualquer outra direção de migração) do repositório Frankenstein Demo, o aplicativo Alien já deve estar lá como um git submodule e você deverá ver uma pasta respectiva. No entanto, a pasta está vazia por padrão e você precisa atualizar e inicializar os submódulos registrados.

Da raiz do seu projeto (seu host):

 $ git submodule update --init $ cd react $ npm i

Observe que, em ambos os casos, instalamos dependências para o aplicativo Alien, mas elas ficam em área restrita para a subpasta e não poluem nosso Host.

Depois de adicionar o aplicativo Alien como um submódulo do seu Host, você obtém aplicativos Alien e Host independentes (em termos de microsserviços). No entanto, Host considera Alien uma subpasta neste caso e, obviamente, isso permite que Host acesse Alien sem problemas.

3. Escreva um microsserviço/componente alienígena

Nesta etapa, temos que decidir qual microsserviço migrar primeiro e gravá-lo/usar no lado do Alien. Vamos seguir a mesma ordem de serviços que identificamos no Passo 1 e começar com o primeiro: campo de entrada para adicionar um novo item. No entanto, antes de começarmos, vamos concordar que, além deste ponto, usaremos um termo componente mais favorável em vez de microsserviço ou serviço , pois estamos nos aproximando das premissas dos frameworks frontend e o termo componente segue as definições de praticamente qualquer estrutura.

As ramificações do repositório Frankenstein TodoMVC contêm um componente resultante que representa o primeiro serviço “Campo de entrada para adicionar um novo item” como um componente de cabeçalho:

  • Componente de cabeçalho no React
  • Componente de cabeçalho no Vue

Escrever componentes na estrutura de sua escolha está além do escopo deste artigo e não faz parte do Frankenstein Migration. No entanto, há algumas coisas a serem lembradas ao escrever um componente Alien.

Independência

Em primeiro lugar, os componentes em Alien devem seguir o mesmo princípio de independência, previamente configurado no lado do Host: os componentes não devem depender de outros componentes de forma alguma.

Interoperabilidade

Graças à independência dos serviços, muito provavelmente, os componentes do seu Host se comunicam de alguma maneira bem estabelecida, seja um sistema de gerenciamento de estado, comunicação por meio de algum armazenamento compartilhado ou diretamente por meio de um sistema de eventos DOM. “Interoperabilidade” de componentes Alien significa que eles devem ser capazes de se conectar à mesma fonte de comunicação, estabelecida pelo Host, para enviar informações sobre suas mudanças de estado e ouvir mudanças em outros componentes. Na prática, isso significa que, se os componentes em seu Host se comunicarem por meio de eventos DOM, construir seu componente Alien exclusivamente com o gerenciamento de estado em mente não funcionará perfeitamente para esse tipo de migração, infelizmente.

Como exemplo, dê uma olhada no arquivo js/storage.js que é o principal canal de comunicação para nossos componentes jQuery:

 ... fetch: function() { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); }, save: function(todos) { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); var event = new CustomEvent("store-update", { detail: { todos } }); document.dispatchEvent(event); }, ...

Aqui, usamos localStorage (já que este exemplo não é crítico de segurança) para armazenar nossos itens pendentes e, uma vez que as alterações no armazenamento sejam registradas, despachamos um evento DOM personalizado no elemento de document que qualquer componente pode ouvir.

Ao mesmo tempo, do lado do Alien (digamos React), podemos configurar a comunicação de gerenciamento de estado tão complexa quanto quisermos. No entanto, provavelmente é inteligente mantê-lo para o futuro: para integrar com sucesso nosso componente Alien React no Host, temos que nos conectar ao mesmo canal de comunicação usado pelo Host. Nesse caso, é localStorage . Para simplificar as coisas, apenas copiamos o arquivo de armazenamento do Host para o Alien e conectamos nossos componentes a ele:

 import todoStorage from "../storage"; class Header extends Component { constructor(props) { this.state = { todos: todoStorage.fetch() }; } componentDidMount() { document.addEventListener("store-update", this.updateTodos); } componentWillUnmount() { document.removeEventListener("store-update", this.updateTodos); } componentDidUpdate(prevProps, prevState) { if (prevState.todos !== this.state.todos) { todoStorage.save(this.state.todos); } } ... }

Agora, nossos componentes Alien podem falar a mesma linguagem com componentes Host e vice-versa.

4. Gravar o wrapper do componente da Web em torno do serviço estrangeiro

Apesar de estarmos agora apenas na quarta etapa, conseguimos bastante:

  • Dividimos nosso aplicativo Host em serviços independentes que estão prontos para serem substituídos por serviços Alien;
  • Configuramos Host e Alien para serem completamente independentes um do outro, mas muito bem conectados via git submodules ;
  • Escrevemos nosso primeiro componente Alien usando o novo framework.

Agora é hora de configurar uma ponte entre Host e Alien para que o novo componente Alien possa funcionar no Host.

Lembrete da Parte 1 : Certifique-se de que seu Host tenha um empacotador de pacotes disponível. Neste artigo, contamos com o Webpack, mas isso não significa que a técnica não funcionará com o Rollup ou qualquer outro bundler de sua escolha. No entanto, deixo o mapeamento do Webpack para seus experimentos.

Convenção de nomes

Conforme mencionado no artigo anterior, vamos usar Web Components para integrar o Alien no Host. Do lado do Host, criamos um novo arquivo: js/frankenstein-wrappers/Header-wrapper.js . (Será nosso primeiro wrapper Frankenstein.) Tenha em mente que é uma boa idéia nomear seus wrappers da mesma forma que seus componentes no aplicativo Alien, por exemplo, apenas adicionando um sufixo “ -wrapper ”. Você verá mais tarde porque isso é uma boa ideia, mas por enquanto, vamos concordar que isso significa que se o componente Alien for chamado Header.js (no React) ou Header.vue (no Vue), o wrapper correspondente no O lado do host deve ser chamado Header-wrapper.js .

Em nosso primeiro wrapper, começamos com o clichê fundamental para registrar um elemento personalizado:

 class FrankensteinWrapper extends HTMLElement {} customElements.define("frankenstein-header-wrapper", FrankensteinWrapper);

Em seguida, temos que inicializar o Shadow DOM para este elemento.

Consulte a Parte 1 para entender por que usamos Shadow DOM.

 class FrankensteinWrapper extends HTMLElement { connectedCallback() { this.attachShadow({ mode: "open" }); } }

Com isso, temos todos os bits essenciais do Web Component configurados e é hora de adicionar nosso componente Alien à mistura. Primeiramente, no início do nosso wrapper Frankenstein, devemos importar todos os bits responsáveis ​​pela renderização do componente Alien.

 import React from "../../react/node_modules/react"; import ReactDOM from "../../react/node_modules/react-dom"; import HeaderApp from "../../react/src/components/Header"; ...

Aqui temos que fazer uma pausa por um segundo. Observe que não importamos as dependências do Alien do node_modules do Host. Tudo vem do próprio Alien que fica na subpasta react/ . É por isso que a Etapa 2 é tão importante e é crucial garantir que o Host tenha acesso total aos ativos do Alien.

Agora, podemos renderizar nosso componente Alien dentro do Shadow DOM do Web Component:

 ... connectedCallback() { ... ReactDOM.render(<HeaderApp />, this.shadowRoot); } ...

Nota : Neste caso, o React não precisa de mais nada. No entanto, para renderizar o componente Vue, você precisa adicionar um nó de encapsulamento para conter seu componente Vue como o seguinte:

 ... connectedCallback() { const mountPoint = document.createElement("div"); this.attachShadow({ mode: "open" }).appendChild(mountPoint); new Vue({ render: h => h(VueHeader) }).$mount(mountPoint); } ...

A razão para isso é a diferença em como o React e o Vue renderizam os componentes: o React anexa o componente ao nó DOM referenciado, enquanto o Vue substitui o nó DOM referenciado pelo componente. Portanto, se fizermos .$mount(this.shadowRoot) para Vue, ele essencialmente substituirá o Shadow DOM.

Isso é tudo o que temos que fazer com nosso wrapper por enquanto. O resultado atual para o wrapper Frankenstein nas direções de migração jQuery-to-React e jQuery-to-Vue pode ser encontrado aqui:

  • Frankenstein Wrapper para componente React
  • Frankenstein Wrapper para componente Vue

Para resumir a mecânica do invólucro de Frankenstein:

  1. Crie um elemento personalizado,
  2. Iniciar Shadow DOM,
  3. Importe tudo o que for necessário para renderizar um componente Alien,
  4. Renderize o componente Alien no Shadow DOM do elemento personalizado.

No entanto, isso não renderiza nosso Alien in Host automaticamente. Temos que substituir a marcação Host existente pelo nosso novo wrapper Frankenstein.

Apertem os cintos, pode não ser tão simples como seria de esperar!

5. Substitua o serviço de host pelo componente da Web

Vamos adicionar nosso novo arquivo Header-wrapper.js ao index.html e substituir a marcação de cabeçalho existente pelo elemento personalizado <frankenstein-header-wrapper> recém-criado.

 ... <!-- <header class="header">--> <!-- <h1>todos</h1>--> <!-- <input class="new-todo" placeholder="What needs to be done?" autofocus>--> <!-- </header>--> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script type="module" src="js/frankenstein-wrappers/Header-wrapper.js"></script>

Infelizmente, isso não funcionará tão simples assim. Se você abrir um navegador e verificar o console, há o Uncaught SyntaxError esperando por você. Dependendo do navegador e seu suporte para módulos ES6, ele estará relacionado às importações do ES6 ou à forma como o componente Alien é renderizado. De qualquer forma, temos que fazer algo a respeito, mas o problema e a solução devem ser familiares e claros para a maioria dos leitores.

5.1. Atualize o Webpack e o Babel quando necessário

Devemos envolver um pouco de magia Webpack e Babel antes de integrar nosso wrapper Frankenstein. Discutir essas ferramentas está além do escopo do artigo, mas você pode dar uma olhada nos commits correspondentes no repositório Frankenstein Demo:

  • Configuração para migração para React
  • Configuração para migração para Vue

Essencialmente, configuramos o processamento dos arquivos, bem como um novo ponto de entrada frankenstein na configuração do Webpack para conter tudo relacionado aos wrappers Frankenstein em um só lugar.

Assim que o Webpack in Host souber como processar o componente Alien e os Web Components, estaremos prontos para substituir a marcação do Host pelo novo wrapper Frankenstein.

5.2. Substituição do Componente Real

A substituição do componente deve ser direta agora. No index.html do seu Host, faça o seguinte:

  1. Substitua <header class="header"> elemento DOM por <frankenstein-header-wrapper> ;
  2. Adicione um novo script frankenstein.js . Este é o novo ponto de entrada no Webpack que contém tudo relacionado aos wrappers de Frankenstein.
 ... <!-- We replace <header class="header"> --> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script src="./frankenstein.js"></script>

É isso! Reinicie seu servidor se necessário e testemunhe a magia do componente Alien integrado ao Host.

No entanto, algo ainda parecia estar faltando. O componente Alien no contexto do Host não tem a mesma aparência que no contexto do aplicativo Alien autônomo. É simplesmente sem estilo.

Componente Alien React sem estilo após ser integrado ao Host
Componente Alien React sem estilo após ser integrado ao Host (visualização grande)

Por que é tão? Os estilos do componente não deveriam ser integrados ao componente Alien no Host automaticamente? Eu gostaria que sim, mas como em muitas situações, depende. Estamos chegando à parte desafiadora da migração de Frankenstein.

5.3. Informações gerais sobre o estilo do componente alienígena

Em primeiro lugar, a ironia é que não há bug na forma como as coisas funcionam. Tudo é como foi projetado para funcionar. Para explicar isso, vamos mencionar brevemente diferentes maneiras de estilizar componentes.

Estilos Globais

Todos nós estamos familiarizados com isso: estilos globais podem ser (e geralmente são) distribuídos sem nenhum componente específico e aplicados em toda a página. Estilos globais afetam todos os nós DOM com seletores correspondentes.

Alguns exemplos de estilos globais são as tags <style> e <link rel="stylesheet"> encontradas em seu index.html . Alternativamente, uma folha de estilo global pode ser importada para algum módulo JS raiz para que todos os componentes possam ter acesso a ela também.

O problema de estilizar aplicativos dessa maneira é óbvio: manter folhas de estilo monolíticas para aplicativos grandes se torna muito difícil. Além disso, como vimos no artigo anterior, estilos globais podem facilmente quebrar componentes que são renderizados diretamente na árvore DOM principal, como em React ou Vue.

Estilos agrupados

Esses estilos geralmente são fortemente acoplados a um componente e raramente são distribuídos sem o componente. Os estilos geralmente residem no mesmo arquivo com o componente. Bons exemplos desse tipo de estilo são os componentes com estilo em React ou Módulos CSS e CSS com escopo em componentes de arquivo único no Vue. No entanto, não importa a variedade de ferramentas para escrever estilos agrupados, o princípio subjacente na maioria delas é o mesmo: as ferramentas fornecem um mecanismo de escopo para bloquear estilos definidos em um componente para que os estilos não quebrem outros componentes ou estilos.

Por que os estilos com escopo podem ser frágeis?

Na Parte 1, ao justificar o uso do Shadow DOM no Frankenstein Migration, abordamos brevemente o tópico de escopo versus encapsulamento) e como o encapsulamento do Shadow DOM é diferente das ferramentas de estilo de escopo. No entanto, não explicamos por que as ferramentas de escopo fornecem um estilo tão frágil para nossos componentes, e agora, quando enfrentamos o componente Alien sem estilo, torna-se essencial para o entendimento.

Todas as ferramentas de escopo para estruturas modernas funcionam da mesma forma:

  • Você escreve estilos para seu componente de alguma forma sem pensar muito sobre escopo ou encapsulamento;
  • Você executa seus componentes com folhas de estilo importadas/embutidas por meio de algum sistema de agrupamento, como Webpack ou Rollup;
  • O empacotador gera classes CSS exclusivas ou outros atributos, criando e injetando seletores individuais para seu HTML e folhas de estilo correspondentes;
  • O empacotador cria uma entrada <style> no <head> do seu documento e coloca os estilos de seus componentes com seletores mesclados exclusivos lá.

É quase isso. Funciona e funciona bem em muitos casos. Exceto quando isso não acontece: quando os estilos de todos os componentes estão no escopo de estilo global, fica fácil quebrá-los, por exemplo, usando maior especificidade. Isso explica a potencial fragilidade das ferramentas de escopo, mas por que nosso componente Alien está completamente sem estilo?

Vamos dar uma olhada no Host atual usando o DevTools. Ao inspecionar o wrapper Frankenstein recém-adicionado com o componente Alien React, por exemplo, podemos ver algo assim:

Invólucro de Frankenstein com componente Alien dentro. Observe as classes CSS exclusivas nos nós do Alien.
Invólucro de Frankenstein com componente Alien dentro. Observe as classes CSS exclusivas nos nós do Alien. (Visualização grande)

Portanto, o Webpack gera classes CSS exclusivas para nosso componente. Excelente! Onde estão os estilos então? Bem, os estilos estão exatamente onde foram projetados para estar — no <head> do documento.

Enquanto o componente Alien está dentro do invólucro de Frankenstein, seus estilos estão na cabeça do documento.
Enquanto o componente Alien está dentro do wrapper Frankenstein, seus estilos estão no <head> do documento. (Visualização grande)

Então, tudo funciona como deveria, e este é o principal problema. Como nosso componente Alien reside no Shadow DOM e, conforme explicado na Parte 1, o Shadow DOM fornece encapsulamento completo de componentes do restante da página e estilos globais, incluindo as folhas de estilo recém-geradas para o componente que não pode cruzar a borda da sombra e chegar ao componente Alien. Portanto, o componente Alien é deixado sem estilo. No entanto, agora, as táticas para resolver o problema devem ser claras: devemos de alguma forma colocar os estilos do componente no mesmo Shadow DOM onde nosso componente reside (em vez do <head> do documento).

5.4. Estilos de fixação para o componente alienígena

Até agora, o processo de migração para qualquer framework era o mesmo. No entanto, as coisas começam a divergir aqui: cada framework tem suas recomendações sobre como estilizar componentes e, portanto, as maneiras de lidar com o problema são diferentes. Aqui, discutimos os casos mais comuns, mas, se o framework com o qual você trabalha usa alguma maneira única de estilizar componentes, você precisa ter em mente as táticas básicas, como colocar os estilos do componente no Shadow DOM em vez de <head> .

Neste capítulo, abordamos correções para:

  • Estilos empacotados com módulos CSS no Vue (as táticas para CSS com escopo são as mesmas);
  • Estilos empacotados com componentes de estilo no React;
  • Módulos CSS genéricos e estilos globais. Eu combino isso porque os Módulos CSS, em geral, são muito parecidos com as folhas de estilo globais e podem ser importados por qualquer componente tornando os estilos desconectados de qualquer componente em particular.

Restrições primeiro: qualquer coisa que fizermos para corrigir o estilo não deve quebrar o próprio componente Alien . Caso contrário, perderemos a independência de nossos sistemas Alien e Host. Então, para resolver o problema de estilo, vamos confiar na configuração do bundler ou no wrapper Frankenstein.

Estilos agrupados em Vue e Shadow DOM

Se você está escrevendo um aplicativo Vue, provavelmente está usando componentes de arquivo único. Se você também estiver usando o Webpack, você deve estar familiarizado com dois carregadores vue-loader e vue-style-loader . O primeiro permite que você escreva esses componentes de arquivo único, enquanto o último injeta dinamicamente o CSS do componente em um documento como uma tag <style> . Por padrão, o vue-style-loader injeta os estilos do componente no <head> do documento. No entanto, ambos os pacotes aceitam a opção shadowMode na configuração, que nos permite alterar facilmente o comportamento padrão e injetar estilos (como o nome da opção indica) no Shadow DOM. Vamos vê-lo em ação.

Configuração do Webpack

No mínimo, o arquivo de configuração do Webpack deve conter o seguinte:

 const VueLoaderPlugin = require('vue-loader/lib/plugin'); ... module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { shadowMode: true } }, { test: /\.css$/, include: path.resolve(__dirname, '../vue'), use: [ { loader:'vue-style-loader', options: { shadowMode: true } }, 'css-loader' ] } ], plugins: [ new VueLoaderPlugin() ] }

Em uma aplicação real, seu bloco test: /\.css$/ será mais sofisticado (provavelmente envolvendo a regra oneOf ) para considerar as configurações de Host e Alien. No entanto, neste caso, nosso jQuery é estilizado com um simples <link rel="stylesheet"> em index.html , então não construímos estilos para Host via Webpack, e é seguro atender apenas Alien.

Configuração do wrapper

Além da configuração do Webpack, também precisamos atualizar nosso wrapper Frankenstein, apontando o Vue para o Shadow DOM correto. Em nosso Header-wrapper.js , a renderização do componente Vue deve incluir a propriedade shadowRoot que leva a shadowRoot do nosso wrapper Frankenstein:

 ... new Vue({ shadowRoot: this.shadowRoot, render: h => h(VueHeader) }).$mount(mountPoint); ...

Depois de atualizar os arquivos e reiniciar seu servidor, você deve obter algo assim em seu DevTools:

Estilos empacotados com o componente Alien Vue colocados no wrapper Frankenstein com todas as classes CSS exclusivas preservadas.
Estilos empacotados com o componente Alien Vue colocados no wrapper Frankenstein com todas as classes CSS exclusivas preservadas. (Visualização grande)

Finalmente, os estilos para o componente Vue estão em nosso Shadow DOM. Ao mesmo tempo, seu aplicativo deve ficar assim:

O componente de cabeçalho começa a ficar mais parecido com o que deveria. No entanto, algo ainda está faltando.
O componente de cabeçalho começa a ficar mais parecido com o que deveria. No entanto, algo ainda está faltando. (Visualização grande)

Começamos a obter algo parecido com nosso aplicativo Vue: estilos empacotados com o componente são injetados no Shadow DOM do wrapper, mas o componente ainda não parece como deveria. A razão é que no aplicativo Vue original, o componente é estilizado não apenas com os estilos agrupados, mas também parcialmente com estilos globais. No entanto, antes de corrigir os estilos globais, temos que colocar nossa integração do React no mesmo estado que a do Vue.

Estilos agrupados em React e Shadow DOM

Como há muitas maneiras de estilizar um componente React, a solução específica para corrigir um componente Alien em Frankenstein Migration depende da maneira como estilizamos o componente em primeiro lugar. Vamos cobrir brevemente as alternativas mais usadas.

componentes estilizados

styled-components é uma das formas mais populares de estilizar componentes React. Para o componente Header React, styled-components é precisamente a maneira como o estilizamos. Como essa é uma abordagem clássica de CSS-in-JS, não há arquivo com uma extensão dedicada na qual possamos conectar nosso empacotador como fazemos para .css ou .js , por exemplo. Felizmente, os styled-components permitem a injeção de estilos do componente em um nó personalizado (Shadow DOM em nosso caso) em vez da head do documento com a ajuda do componente de ajuda StyleSheetManager . É um componente pré-definido, instalado com o pacote styled-components que aceita a propriedade target , definindo “um nó DOM alternativo para injetar informações de estilos”. Exatamente o que precisamos! Além disso, nem precisamos alterar a configuração do nosso Webpack: tudo depende do nosso wrapper Frankenstein.

Devemos atualizar nosso Header-wrapper.js que contém o componente React Alien com as seguintes linhas:

 ... import { StyleSheetManager } from "../../react/node_modules/styled-components"; ... const target = this.shadowRoot; ReactDOM.render( <StyleSheetManager target={target}> <HeaderApp /> </StyleSheetManager>, appWrapper ); ...

Aqui, importamos o componente StyleSheetManager (do Alien, e não do Host) e envolvemos nosso componente React com ele. Ao mesmo tempo, enviamos a propriedade target apontando para nosso shadowRoot . É isso. Se você reiniciar o servidor, deverá ver algo assim em seu DevTools:

Estilos empacotados com o componente React Alien colocado dentro do wrapper Frankenstein com todas as classes CSS exclusivas preservadas.
Estilos empacotados com o componente React Alien colocado dentro do wrapper Frankenstein com todas as classes CSS exclusivas preservadas. (Visualização grande)

Agora, os estilos do nosso componente estão no Shadow DOM em vez de <head> . Dessa forma, a renderização do nosso aplicativo agora se assemelha ao que vimos com o aplicativo Vue anteriormente.

Depois de mover estilos agrupados para o invólucro de Frankenstein, o componente Alien React começa a ficar melhor. No entanto, ainda não chegamos lá.
Depois de mover estilos agrupados para o invólucro de Frankenstein, o componente Alien React começa a ficar melhor. No entanto, ainda não chegamos lá. (Visualização grande)

Mesma história: styled-components são responsáveis ​​apenas pela parte empacotada dos estilos do componente React , e os estilos globais gerenciam os bits restantes. Voltaremos aos estilos globais um pouco depois de revisarmos mais um tipo de componentes de estilo.

Módulos CSS

Se você der uma olhada mais de perto no componente Vue que corrigimos anteriormente, você notará que Módulos CSS é precisamente a maneira como estilizamos esse componente. However, even if we style it with Scoped CSS (another recommended way of styling Vue components) the way we fix our unstyled component doesn't change: it is still up to vue-loader and vue-style-loader to handle it through shadowMode: true option.

When it comes to CSS Modules in React (or any other system using CSS Modules without any dedicated tools), things get a bit more complicated and less flexible, unfortunately.

Let's take a look at the same React component which we've just integrated, but this time styled with CSS Modules instead of styled-components. The main thing to note in this component is a separate import for stylesheet:

 import styles from './Header.module.css'

The .module.css extension is a standard way to tell React applications built with the create-react-app utility that the imported stylesheet is a CSS Module. The stylesheet itself is very basic and does precisely the same our styled-components do.

Integrating CSS modules into a Frankenstein wrapper consists of two parts:

  • Enabling CSS Modules in bundler,
  • Pushing resulting stylesheet into Shadow DOM.

I believe the first point is trivial: all you need to do is set { modules: true } for css-loader in your Webpack configuration. Since, in this particular case, we have a dedicated extension for our CSS Modules ( .module.css ), we can have a dedicated configuration block for it under the general .css configuration:

 { test: /\.css$/, oneOf: [ { test: /\.module\.css$/, use: [ ... { loader: 'css-loader', options: { modules: true, } } ] } ] }

Note : A modules option for css-loader is all we have to know about CSS Modules no matter whether it's React or any other system. When it comes to pushing resulting stylesheet into Shadow DOM, however, CSS Modules are no different from any other global stylesheet.

By now, we went through the ways of integrating bundled styles into Shadow DOM for the following conventional scenarios:

  • Vue components, styled with CSS Modules. Dealing with Scoped CSS in Vue components won't be any different;
  • React components, styled with styled-components;
  • Components styled with raw CSS Modules (without dedicated tools like those in Vue). For these, we have enabled support for CSS modules in Webpack configuration.

However, our components still don't look as they are supposed to because their styles partially come from global styles . Those global styles do not come to our Frankenstein wrappers automatically. Moreover, you might get into a situation in which your Alien components are styled exclusively with global styles without any bundled styles whatsoever. So let's finally fix this side of the story.

Global Styles And Shadow DOM

Having your components styled with global styles is neither wrong nor bad per se: every project has its requirements and limitations. However, the best you can do for your components if they rely on some global styles is to pull those styles into the component itself. This way, you have proper easy-to-maintain self-contained components with bundled styles.

Nevertheless, it's not always possible or reasonable to do so: several components might share some styling, or your whole styling architecture could be built using global stylesheets that are split into the modular structure, and so on.

So having an opportunity to pull in global styles into our Frankenstein wrappers wherever it's required is essential for the success of this type of migration. Before we get to an example, keep in mind that this part is the same for pretty much any framework of your choice — be it React, Vue or anything else using global stylesheets!

Let's get back to our Header component from the Vue application. Take a look at this import:

 import "todomvc-app-css/index.css";

This import is where we pull in the global stylesheet. In this case, we do it from the component itself. It's only one way of using global stylesheet to style your component, but it's not necessarily like this in your application.

Some parent module might add a global stylesheet like in our React application where we import index.css only in index.js , and then our components expect it to be available in the global scope. Your component's styling might even rely on a stylesheet, added with <style> or <link> to your index.html . It doesn't matter. What matters, however, is that you should expect to either import global stylesheets in your Alien component (if it doesn't harm the Alien application) or explicitly in the Frankenstein wrapper. Otherwise, the wrapper would not know that the Alien component needs any stylesheet other than the ones already bundled with it.

Caution . If there are many global stylesheets to be shared between Alien components and you have a lot of such components, this might harm the performance of your Host application under the migration period.

Here is how import of a global stylesheet, required for the Header component, is done in Frankenstein wrapper for React component:

 // we import directly from react/, not from Host import '../../react/node_modules/todomvc-app-css/index.css'

Nevertheless, by importing a stylesheet this way, we still bring the styles to the global scope of our Host, while what we need is to pull in the styles into our Shadow DOM. Como vamos fazer isso?

Webpack configuration for global stylesheets & Shadow DOM

First of all, you might want to add an explicit test to make sure that we process only the stylesheets coming from our Alien. In case of our React migration, it will look similar to this:

 test: /\.css$/, oneOf: [ // this matches stylesheets coming from /react/ subfolder { test: /\/react\//, use: [] }, ... ]

In case of Vue application, obviously, you change test: /\/react\// with something like test: /\/vue\// . Apart from that, the configuration will be the same for any framework. Next, let's specify the required loaders for this block.

 ... use: [ { loader: 'style-loader', options: { ... } }, 'css-loader' ]

Two things to note. First, you have to specify modules: true in css-loader 's configuration if you're processing CSS Modules of your Alien application.

Second, we should convert styles into <style> tag before injecting those into Shadow DOM. In the case of Webpack, for that, we use style-loader . The default behavior for this loader is to insert styles into the document's head. Typically. And this is precisely what we don't want: our goal is to get stylesheets into Shadow DOM. However, in the same way we used target property for styled-components in React or shadowMode option for Vue components that allowed us to specify custom insertion point for our <style> tags, regular style-loader provides us with nearly same functionality for any stylesheet: the insert configuration option is exactly what helps us achieve our primary goal. Boas notícias! Let's add it to our configuration.

 ... { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }

However, not everything is so smooth here with a couple of things to keep in mind.

Folhas de estilo globais e opção de insert do style-loader de estilos

Se você verificar a documentação dessa opção, perceberá que esta opção usa um seletor por configuração. Isso significa que, se você tiver vários componentes Alien exigindo estilos globais inseridos em um wrapper Frankenstein, será necessário especificar style-loader para cada um dos wrappers Frankenstein. Na prática, isso significa que você, provavelmente, precisa confiar na regra oneOf em seu bloco de configuração para servir a todos os wrappers.

 { test: /\/react\//, oneOf: [ { test: /1-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '1-frankenstein-wrapper' } }, `css-loader` ] }, { test: /2-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '2-frankenstein-wrapper' } }, `css-loader` ] }, // etc. ], }

Não muito flexível, concordo. No entanto, não é grande coisa, desde que você não tenha centenas de componentes para migrar. Caso contrário, isso pode dificultar a manutenção da configuração do seu Webpack. O verdadeiro problema, porém, é que não podemos escrever um seletor CSS para Shadow DOM.

Tentando resolver isso, podemos notar que a opção insert também pode receber uma função em vez de um seletor simples para especificar uma lógica mais avançada para inserção. Com isso, podemos usar esta opção para inserir folhas de estilo diretamente no Shadow DOM! De forma simplificada, pode ser semelhante a isto:

 insert: function(element) { var parent = document.querySelector('frankenstein-header-wrapper').shadowRoot; parent.insertBefore(element, parent.firstChild); }

Tentador, não é? No entanto, isso não funcionará para o nosso cenário ou funcionará longe do ideal. Nosso <frankenstein-header-wrapper> está realmente disponível em index.html (porque o adicionamos na Etapa 5.2). Mas quando o Webpack processa todas as dependências (incluindo as folhas de estilo) para um componente Alien ou um wrapper Frankenstein, o Shadow DOM ainda não foi inicializado no wrapper Frankenstein: as importações são processadas antes disso. Portanto, apontar insert diretamente para shadowRoot resultará em um erro.

Há apenas um caso em que podemos garantir que o Shadow DOM seja inicializado antes que o Webpack processe nossa dependência de folha de estilo. Se o componente Alien não importa uma folha de estilo em si e cabe ao wrapper Frankenstein importá-la, podemos empregar a importação dinâmica e importar a folha de estilo necessária depois de configurar o Shadow DOM:

 this.attachShadow({ mode: "open" }); import('../vue/node_modules/todomvc-app-css/index.css');

Isso funcionará: tal importação, combinada com a configuração de insert acima, de fato encontrará o Shadow DOM correto e inserirá a tag <style> nele. No entanto, obter e processar a folha de estilo levará tempo, o que significa que seus usuários em uma conexão lenta ou dispositivos lentos podem enfrentar um momento do componente sem estilo antes que sua folha de estilo seja colocada no Shadow DOM do wrapper.

O componente Alien sem estilo é renderizado antes que a folha de estilo global seja importada e adicionada ao Shadow DOM.
O componente Alien sem estilo é renderizado antes que a folha de estilo global seja importada e adicionada ao Shadow DOM. (Visualização grande)

Portanto, apesar de insert aceitar a função, infelizmente, não é suficiente para nós, e temos que recorrer a seletores CSS simples como frankenstein-header-wrapper . No entanto, isso não coloca folhas de estilo no Shadow DOM automaticamente, e as folhas de estilo residem em <frankenstein-header-wrapper> fora do Shadow DOM.

style-loader coloca a folha de estilo importada no wrapper Frankenstein, mas fora do Shadow DOM.
style-loader coloca a folha de estilo importada no wrapper Frankenstein, mas fora do Shadow DOM. (Visualização grande)

Precisamos de mais uma peça do quebra-cabeça.

Configuração do wrapper para folhas de estilo globais e Shadow DOM

Felizmente, a correção é bastante direta no lado do wrapper: quando o Shadow DOM é inicializado, precisamos verificar se há folhas de estilo pendentes no wrapper atual e puxá-las para o Shadow DOM.

O estado atual da importação da folha de estilo global é o seguinte:

  • Importamos uma folha de estilo que deve ser adicionada ao Shadow DOM. A folha de estilo pode ser importada no próprio componente Alien ou, explicitamente, no wrapper Frankenstein. No caso de migração para React, por exemplo, a importação é inicializada a partir do wrapper. No entanto, na migração para o Vue, o próprio componente semelhante importa a folha de estilo necessária e não precisamos importar nada no wrapper.
  • Como apontado acima, quando o Webpack processa importações .css para o componente Alien, graças à opção insert do style-loader , as folhas de estilo são injetadas em um wrapper Frankenstein, mas fora do Shadow DOM.

A inicialização simplificada do Shadow DOM no wrapper de Frankenstein, deve atualmente (antes de puxarmos qualquer folha de estilo) ser semelhante a isto:

 this.attachShadow({ mode: "open" }); ReactDOM.render(); // or `new Vue()`

Agora, para evitar a oscilação do componente sem estilo, o que precisamos fazer agora é puxar todas as folhas de estilo necessárias após a inicialização do Shadow DOM, mas antes da renderização do componente Alien.

 this.attachShadow({ mode: "open" }); Array.prototype.slice .call(this.querySelectorAll("style")) .forEach(style => { this.shadowRoot.prepend(style); }); ReactDOM.render(); // or new Vue({})

Foi uma explicação longa com muitos detalhes, mas principalmente, tudo o que é preciso para inserir folhas de estilo globais no Shadow DOM:

  • Na configuração do Webpack, adicione style-loader com a opção de insert apontando para o wrapper Frankenstein necessário.
  • No próprio wrapper, puxe as folhas de estilo “pendentes” após a inicialização do Shadow DOM, mas antes da renderização do componente Alien.

Depois de implementar essas alterações, seu componente deve ter tudo o que precisa. A única coisa que você pode querer (isso não é um requisito) adicionar é algum CSS personalizado para ajustar um componente Alien no ambiente do Host. Você pode até estilizar seu componente Alien de forma completamente diferente quando usado no Host. Ele vai além do ponto principal do artigo, mas você examina o código final do wrapper, onde pode encontrar exemplos de como substituir estilos simples no nível do wrapper.

  • Wrapper Frankenstein para o componente React
  • Wrapper Frankenstein para o componente Vue

Você também pode dar uma olhada na configuração do Webpack nesta etapa da migração:

  • Migração para React com styled-components
  • Migração para React com Módulos CSS
  • Migração para Vue

E, finalmente, nossos componentes se parecem exatamente com o que pretendíamos.

Resultado da migração do componente Header escrito com Vue e React. A listagem dos itens a fazer ainda é aplicação jQuery.
Resultado da migração do componente Header escrito com Vue e React. A listagem dos itens a fazer ainda é aplicação jQuery. (Visualização grande)

5.5. Resumo dos estilos de fixação para o componente Alien

Este é um ótimo momento para resumir o que aprendemos neste capítulo até agora. Pode parecer que tivemos que fazer um trabalho enorme para corrigir o estilo do componente Alien; no entanto, tudo se resume a:

  • Corrigir estilos empacotados implementados com componentes de estilo em módulos React ou CSS e CSS com escopo no Vue é tão simples quanto algumas linhas na configuração do wrapper do Frankenstein ou do Webpack.
  • A correção de estilos, implementada com Módulos CSS, começa com apenas uma linha na configuração css-loader . Depois disso, os Módulos CSS são tratados como uma folha de estilo global.
  • A correção de folhas de estilo globais requer a configuração do pacote style-loader com a opção de insert no Webpack e a atualização do wrapper Frankenstein para inserir as folhas de estilo no Shadow DOM no momento certo do ciclo de vida do wrapper.

Afinal, temos o componente Alien com estilo adequado migrado para o Host. No entanto, há apenas uma coisa que pode ou não incomodá-lo, dependendo de qual estrutura você migra.

Boas notícias primeiro: se você estiver migrando para o Vue , a demonstração deve estar funcionando bem e você poderá adicionar novos itens de tarefas do componente Vue migrado. No entanto, se você estiver migrando para o React e tentar adicionar um novo item de tarefas pendentes, não terá sucesso. Adicionar novos itens simplesmente não funciona e nenhuma entrada é adicionada à lista. Mas por que? Qual é o problema? Sem preconceito, mas o React tem opiniões próprias sobre algumas coisas.

5.6. Eventos React e JS no Shadow DOM

Não importa o que a documentação do React lhe diga, o React não é muito amigável para Web Components. A simplicidade do exemplo na documentação não suporta nenhuma crítica, e qualquer coisa mais complicada do que renderizar um link no Web Component requer alguma pesquisa e investigação.

Como você viu ao corrigir o estilo para o nosso componente Alien, ao contrário do Vue, onde as coisas se encaixam nos Web Components quase fora da caixa, o React não está pronto para Web Components. Por enquanto, temos um entendimento de como fazer componentes React pelo menos parecerem bons dentro de Web Components, mas também há funcionalidades e eventos JavaScript para corrigir.

Para encurtar a história: o Shadow DOM encapsula eventos e os redireciona, enquanto o React não suporta esse comportamento do Shadow DOM nativamente e, portanto, não captura eventos vindos do Shadow DOM. Existem razões mais profundas para esse comportamento, e há até um problema em aberto no rastreador de bugs do React se você quiser mergulhar em mais detalhes e discussões.

Felizmente, pessoas inteligentes prepararam uma solução para nós. @josephnvu forneceu a base para a solução, e Lukas Bombach a converteu no módulo npm react react-shadow-dom-retarget-events . Então você pode instalar o pacote, seguir as instruções na página dos pacotes, atualizar o código do seu wrapper e seu componente Alien magicamente começará a funcionar:

 import retargetEvents from 'react-shadow-dom-retarget-events'; ... ReactDOM.render( ... ); retargetEvents(this.shadowRoot);

Se você quiser que ele tenha mais desempenho, você pode fazer uma cópia local do pacote (a licença MIT permite isso) e limitar o número de eventos para ouvir como é feito no repositório Frankenstein Demo. Para este exemplo, eu sei quais eventos preciso redirecionar e especificar apenas aqueles.

Com isso, estamos finalmente (eu sei que foi um processo longo) feito com a migração adequada do primeiro componente Alien com estilo e totalmente funcional. Arranja uma boa bebida. Você merece isso!

6. Enxágue e repita para todos os seus componentes

Após migrarmos o primeiro componente, devemos repetir o processo para todos os nossos componentes. No caso do Frankenstein Demo, resta apenas um, porém: aquele, responsável por renderizar a listagem de pendências.

Novos invólucros para novos componentes

Vamos começar adicionando um novo wrapper. Seguindo a convenção de nomenclatura, discutida acima (já que nosso componente React é chamado MainSection.js ), o wrapper correspondente na migração para React deve ser chamado MainSection-wrapper.js . Ao mesmo tempo, um componente semelhante no Vue é chamado Listing.vue , portanto, o wrapper correspondente na migração para o Vue deve ser chamado Listing-wrapper.js . No entanto, não importa a convenção de nomenclatura, o wrapper em si será quase idêntico ao que já temos:

  • Wrapper para listagem React
  • Wrapper para listagem Vue

Há apenas uma coisa interessante que apresentamos neste segundo componente no aplicativo React. Às vezes, por esse ou outro motivo, você pode querer usar algum plugin jQuery em seus componentes. No caso do nosso componente React, introduzimos duas coisas:

  • Plugin de dica de ferramenta do Bootstrap que usa jQuery,
  • Uma alternância para classes CSS como .addClass() e .removeClass() .

    Nota : Este uso de jQuery para adicionar/remover classes é meramente ilustrativo. Por favor, não use jQuery para este cenário em projetos reais - confie em JavaScript simples.

Claro, pode parecer estranho introduzir jQuery em um componente Alien quando migramos do jQuery, mas seu Host pode ser diferente do Host neste exemplo — você pode migrar do AngularJS ou qualquer outra coisa. Além disso, a funcionalidade do jQuery em um componente e o jQuery global não são necessariamente a mesma coisa.

No entanto, o problema é que, mesmo que você confirme que o componente funciona bem no contexto do seu aplicativo Alien, quando você o coloca no Shadow DOM, seus plugins jQuery e outros códigos que dependem do jQuery simplesmente não funcionam.

jQuery no Shadow DOM

Vamos dar uma olhada em uma inicialização geral de um plugin jQuery aleatório:

 $('.my-selector').fancyPlugin();

Desta forma, todos os elementos com .my-selector serão processados ​​pelo fancyPlugin . Esta forma de inicialização assume que .my-selector está presente no DOM global. No entanto, uma vez que tal elemento é colocado no Shadow DOM, assim como com os estilos, os limites de sombra impedem que o jQuery entre nele. Como resultado, o jQuery não consegue encontrar elementos no Shadow DOM.

A solução é fornecer um segundo parâmetro opcional para o seletor que define o elemento raiz para o jQuery pesquisar. E é aqui que podemos fornecer nosso shadowRoot .

 $('.my-selector', this.shadowRoot).fancyPlugin();

Dessa forma, os seletores jQuery e, como resultado, os plugins funcionarão perfeitamente.

Tenha em mente que os componentes Alien devem ser usados ​​tanto: no Alien sem shadow DOM, quanto no Host dentro do Shadow DOM. Portanto, precisamos de uma solução mais unificada que não assuma a presença do Shadow DOM por padrão.

Analisando o componente MainSection em nosso aplicativo React, descobrimos que ele define a propriedade documentRoot .

 ... this.documentRoot = this.props.root? this.props.root: document; ...

Então, verificamos a propriedade root passada e, se existir, é isso que usamos como documentRoot . Caso contrário, voltamos para document .

Aqui está a inicialização do plugin de dica de ferramenta que usa esta propriedade:

 $('[data-toggle="tooltip"]', this.documentRoot).tooltip({ container: this.props.root || 'body' });

Como bônus, usamos a mesma propriedade root para definir um contêiner para injetar a dica de ferramenta neste caso.

Agora, quando o componente Alien estiver pronto para aceitar a propriedade root , atualizamos a renderização do componente no wrapper Frankenstein correspondente:

 // `appWrapper` is the root element within wrapper's Shadow DOM. ReactDOM.render(<MainApp root={ appWrapper } />, appWrapper);

E é isso! O componente funciona tão bem no Shadow DOM quanto no DOM global.

Configuração do Webpack para cenário de vários wrappers

A parte emocionante está acontecendo na configuração do Webpack ao usar vários wrappers. Nada muda para os estilos empacotados como os módulos CSS nos componentes Vue, ou os componentes estilizados no React. No entanto, os estilos globais devem ter uma pequena reviravolta agora.

Lembre-se, dissemos que style-loader (responsável por injetar folhas de estilo globais no Shadow DOM correto) é inflexível, pois leva apenas um seletor por vez para sua opção de insert . Isso significa que devemos dividir a regra .css no Webpack para ter uma sub-regra por wrapper usando a regra oneOf ou similar, se você estiver em um empacotador diferente do Webpack.

É sempre mais fácil explicar usando um exemplo, então vamos falar sobre o da migração para o Vue desta vez (o da migração para o React, no entanto, é quase idêntico):

 ... oneOf: [ { issuer: /Header/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }, ... ] }, { issuer: /Listing/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-listing-wrapper' } }, ... ] }, ] ...

Excluí css-loader , pois sua configuração é a mesma em todos os casos. Vamos falar sobre style-loader . Nesta configuração, inserimos a tag <style> em *-header-* ou *-listing-* , dependendo do nome do arquivo que solicita essa folha de estilo (regra do issuer no Webpack). Mas temos que lembrar que a folha de estilo global necessária para renderizar um componente Alien pode ser importada em dois lugares:

  • O próprio componente Alien,
  • Um invólucro de Frankenstein.

E aqui, devemos apreciar a convenção de nomenclatura para wrappers, descrita acima, quando o nome de um componente Alien e um wrapper correspondente corresponderem. Se, por exemplo, tivermos uma folha de estilo, importada em um componente Vue chamado Header.vue , ela corrigirá o wrapper *-header-* . Ao mesmo tempo, se importarmos a folha de estilo no wrapper, tal folha de estilo seguirá precisamente a mesma regra se o wrapper for chamado Header-wrapper.js sem nenhuma alteração na configuração. A mesma coisa para o componente Listing.vue e seu wrapper correspondente Listing-wrapper.js . Usando essa convenção de nomenclatura, reduzimos a configuração em nosso bundler.

Após a migração de todos os seus componentes, é hora da etapa final da migração.

7. Mude para Alien

Em algum momento, você descobre que os componentes identificados na primeira etapa da migração foram todos substituídos por wrappers Frankenstein. Nenhum aplicativo jQuery é realmente deixado e o que você tem é, essencialmente, o aplicativo Alien que é colado usando os meios do Host.

Por exemplo, a parte de conteúdo de index.html no aplicativo jQuery — após a migração de ambos os microsserviços — se parece com isso agora:

 <section class="todoapp"> <frankenstein-header-wrapper></frankenstein-header-wrapper> <frankenstein-listing-wrapper></frankenstein-listing-wrapper> </section>

Neste momento, não faz sentido manter nosso aplicativo jQuery por perto: em vez disso, devemos mudar para o aplicativo Vue e esquecer todos os nossos wrappers, Shadow DOM e configurações sofisticadas de Webpack. Para fazer isso, temos uma solução elegante.

Vamos falar sobre solicitações HTTP. Vou mencionar a configuração do Apache aqui, mas este é apenas um detalhe de implementação: fazer a troca no Nginx ou qualquer outra coisa deve ser tão trivial quanto no Apache.

Imagine que você tenha seu site servido a partir da pasta /var/www/html em seu servidor. Nesse caso, seu httpd.conf ou httpd-vhost.conf deve ter uma entrada que aponte para essa pasta como:

 DocumentRoot "/var/www/html"

Para mudar seu aplicativo após a migração do Frankenstein do jQuery para o React, tudo o que você precisa fazer é atualizar a entrada DocumentRoot para algo como:

 DocumentRoot "/var/www/html/react/build"

Compile seu aplicativo Alien, reinicie seu servidor e seu aplicativo será servido diretamente da pasta do Alien: o aplicativo React servido da pasta react/ . No entanto, o mesmo vale para o Vue, é claro, ou qualquer outro framework que você migrou também. É por isso que é tão vital manter o Host e o Alien completamente independentes e funcionais a qualquer momento, porque seu Alien se torna seu Host nesta etapa.

Agora você pode remover com segurança tudo ao redor da pasta do seu Alien, incluindo todos os Shadow DOM, wrappers Frankenstein e qualquer outro artefato relacionado à migração. Foi um caminho difícil em alguns momentos, mas você migrou seu site. Parabéns!

Conclusão

Nós definitivamente passamos por terrenos um pouco difíceis neste artigo. No entanto, depois que começamos com um aplicativo jQuery, conseguimos migrá-lo para Vue e React. Descobrimos alguns problemas inesperados e não tão triviais ao longo do caminho: tivemos que corrigir o estilo, tivemos que corrigir a funcionalidade do JavaScript, introduzir algumas configurações de empacotador e muito mais. No entanto, nos deu uma visão melhor do que esperar em projetos reais. No final, temos um aplicativo contemporâneo sem nenhum bit restante do aplicativo jQuery, embora tivéssemos todos os direitos de ser céticos sobre o resultado final enquanto a migração estava em andamento.

Após a mudança para Alien, Frankenstein pode ser aposentado.
Após a mudança para Alien, Frankenstein pode ser aposentado. (Visualização grande)

A migração de Frankenstein não é uma bala de prata nem deve ser um processo assustador. É apenas o algoritmo definido, aplicável a muitos projetos, que ajuda a transformar projetos em algo novo e robusto de maneira previsível.