WU / DOCSMICROFRONTENDS UNIVERSAIS
PT

Um contador entre React e Vue

Conecte as duas apps do tutorial pelo store do WU, com arquivos completos, permissões, inscrições e um teste de desmontagem.

O que você vai construir

Continue em mi-plataforma, o mesmo workspace de Adicionar uma segunda app. Você substituirá os contadores locais por um contador compartilhado: somar no React também muda o Vue, e somar no Vue também muda o React.

A página Astro /contador/ mostrará as duas apps juntas. Elas continuarão com seus próprios componentes e renderizadores. O dado comum ficará em wu.store, no caminho demo.counter.

Você precisa do Node.js 24 e do workspace criado com WU CLI 1.2.0, com mi-plataforma em React e inspector em Vue. O código usa as APIs publicadas de wu-framework 4.0.0 e as dependências que você já instalou com wu install.

1. Aplicar os arquivos ao mesmo projeto

Pare o servidor com Ctrl+C. Baixe os arquivos completos do contador e copie seu conteúdo para a raiz de mi-plataforma/, ao lado do seu wu.config.json.

O ZIP contém arquivos para incorporar ao workspace do tutorial; não é instalado nem executado separadamente. Substitua os arquivos de mesmo nome e mantenha as outras pastas do projeto. Se já personalizou esses arquivos, salve suas alterações antes de substituí-los.

Arquivo Responsabilidade
mf-mi-plataforma/src/App.tsx Mostrar e atualizar o dado pelo React.
mf-mi-plataforma/src/main.tsx Conectar a montagem e a desmontagem do React ao WU.
mf-inspector/src/App.vue Mostrar e atualizar o mesmo dado pelo Vue.
mf-inspector/src/main.ts Conectar a montagem e a desmontagem do Vue ao WU.
wu.json dentro de cada app Solicitar acesso apenas a demo.counter.
vite.config.js dentro de cada app Levar o manifesto fonte ao build dessa app.
wu.config.json Manter as duas apps e conceder permissões ao usar o shell gerado.
shell/src/pages/contador.astro Montar ambas as apps, conceder permissões e inicializar o dado.
shell/astro.config.mjs Emitir o CSS do shell em arquivos que wu serve pode servir.

Se já adicionou mais apps ao workspace, mantenha seu wu.config.json e adicione "grants": ["store.read:demo.counter", "store.write:demo.counter"] apenas às entradas existentes mi-plataforma e inspector. O arquivo do ZIP corresponde ao tutorial de duas apps.

Você também pode copiar manualmente os onze arquivos completos apresentados abaixo. Não precisa adicionar uma biblioteca de componentes nem instalar novos pacotes.

2. Fazer o build e executar

Na raiz do workspace:

npm --prefix shell run build
wu dev --native --port 3000 --no-open

Espere npm --prefix shell run build terminar sem erros. Ele prepara a nova página Astro antes de iniciar, pois um build existente do shell pode ser reutilizado. Enquanto wu dev --native está ativo, a CLI 1.2.0 observa as fontes do shell, recompila e recarrega a página quando mudam. Você pode continuar editando sem parar o servidor; confira o terminal se o Astro informar um erro.

Abra http://127.0.0.1:3000/contador/. Você deve ver Un contador, React y Vue, os dois cartões e um zero em cada um.

  1. Clique em Sumar desde React: ambos os valores passam de 0 para 1.
  2. Clique em Sumar desde Vue: ambos passam de 1 para 2.
  3. Clique em Desmontar Vue: o cartão dele desaparece e React mantém o 2.
  4. Clique em Sumar desde React: React mostra 3.
  5. Clique em Montar Vue: Vue lê o dado atual e também mostra 3.
  6. Clique em Restar desde Vue: ambas as apps mostram 2.

Desmontar a app remove sua interface e sua inscrição; não apaga o dado compartilhado. Recarregar a página inteira inicia uma nova sessão com zero: este exemplo guarda o dado na memória, sem persistência no navegador ou em um servidor.

3. Como uma alteração chega às duas apps

O shell inicializa demo.counter uma única vez. Cada componente lê seu valor inicial e se inscreve com store.on. Um botão chama store.update; o WU grava o novo valor e notifica os dois componentes. React atualiza seu useState e Vue atualiza seu ref.

update calcula o novo valor a partir do dado atual do store. Assim, os botões não dependem de a última notificação já ter sido desenhada na tela. As notificações chegam em uma microtask; on não entrega automaticamente o valor inicial.

As apps usam context.wu, a fachada que o WU entrega durante a montagem. waitForWu localiza o runtime do shell para registrar o ciclo de vida com wu.define; as leituras e escritas do componente passam pela fachada da própria app.

O componente React

mf-mi-plataforma/src/App.tsx · Abrir arquivo completo

import { useEffect, useState } from 'react';
import type { WuAppFacade } from 'wu-framework';

export default function App({ appWu }: { appWu: WuAppFacade }) {
  const [count, setCount] = useState(() => appWu.store.get('demo.counter') ?? 0);

  useEffect(() => {
    const stop = appWu.store.on('demo.counter', (value) => setCount(value));
    setCount(appWu.store.get('demo.counter') ?? 0);
    return stop;
  }, [appWu]);

  function change(delta: number) {
    appWu.store.update('demo.counter', (current) => (current ?? 0) + delta);
  }

  return (
    <section aria-label="Contador React">
      <h2>React · mi-plataforma</h2>
      <p>Valor compartido: <output aria-live="polite">{count}</output></p>
      <button type="button" onClick={() => change(-1)}>Restar desde React</button>{' '}
      <button type="button" onClick={() => change(1)}>Sumar desde React</button>
    </section>
  );
}

O efeito se inscreve e lê o dado novamente para cobrir o intervalo entre a primeira renderização e a conexão. Seu return stop libera a inscrição quando React desmonta o componente.

mf-mi-plataforma/src/main.tsx · Abrir arquivo completo

import React from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { waitForWu } from 'wu-framework/adapters/shared';
import App from './App.tsx';

const wu = await waitForWu();
let root: Root | undefined;

wu.define('mi-plataforma', {
  mount(container, context) {
    if (!context?.wu) throw new Error('Esta app necesita el contexto de WU.');
    root = createRoot(container);
    root.render(<App appWu={context.wu} />);
  },
  unmount() {
    root?.unmount();
    root = undefined;
  },
});

O ciclo de vida cria a raiz do React e passa context.wu como prop. Ao desmontar a microapp, root.unmount() executa a limpeza do efeito.

O componente Vue

mf-inspector/src/App.vue · Abrir arquivo completo

<script setup lang="ts">
import { onUnmounted, ref } from 'vue';
import type { WuAppFacade } from 'wu-framework';

const { appWu } = defineProps<{ appWu: WuAppFacade }>();
const count = ref(appWu.store.get('demo.counter') ?? 0);
const stop = appWu.store.on('demo.counter', (value) => { count.value = value; });
onUnmounted(stop);

function change(delta: number) {
  appWu.store.update('demo.counter', (current) => (current ?? 0) + delta);
}
</script>

<template>
  <section aria-label="Contador Vue">
    <h2>Vue · inspector</h2>
    <p>Valor compartido: <output aria-live="polite">{{ count }}</output></p>
    <button type="button" @click="change(-1)">Restar desde Vue</button>
    {{ ' ' }}
    <button type="button" @click="change(1)">Sumar desde Vue</button>
  </section>
</template>

Vue mantém um ref para desenhar o dado. onUnmounted(stop) libera a inscrição quando o componente é removido.

mf-inspector/src/main.ts · Abrir arquivo completo

import { createApp, type App as VueApp } from 'vue';
import { waitForWu } from 'wu-framework/adapters/shared';
import App from './App.vue';

const wu = await waitForWu();
let app: VueApp | undefined;

wu.define('inspector', {
  mount(container, context) {
    if (!context?.wu) throw new Error('Esta app necesita el contexto de WU.');
    app = createApp(App, { appWu: context.wu });
    app.mount(container);
  },
  unmount() {
    app?.unmount();
    app = undefined;
  },
});

app.unmount() executa a limpeza do Vue. Ao montar novamente, uma nova instância é criada e o valor atual do WU é lido; o componente não reinicia o contador.

4. Quais permissões são concedidas

Cada manifesto solicita store.read e store.write apenas para demo.counter. A leitura permite consultar e se inscrever; update precisa tanto de leitura quanto de escrita. O shell concede esses mesmos recursos por grants.

Uma app não ganha acesso ao store inteiro por conhecer um caminho. O WU calcula as permissões efetivas a partir do que a app solicitou e do que o host concedeu. required: true faz uma solicitação sem cobertura impedir o registro da app.

mf-mi-plataforma/wu.json · Abrir arquivo completo

{
  "name": "mi-plataforma",
  "version": "1.0.0",
  "entry": "src/main.tsx",
  "styleMode": "shared",
  "wu": {
    "permissions": [
      { "capability": "store.read", "resource": "demo.counter", "required": true },
      { "capability": "store.write", "resource": "demo.counter", "required": true }
    ]
  }
}

mf-inspector/wu.json · Abrir arquivo completo

{
  "name": "inspector",
  "version": "1.0.0",
  "entry": "src/main.ts",
  "styleMode": "shared",
  "wu": {
    "permissions": [
      { "capability": "store.read", "resource": "demo.counter", "required": true },
      { "capability": "store.write", "resource": "demo.counter", "required": true }
    ]
  }
}

Manter o manifesto ao compilar

No desenvolvimento nativo, a CLI serve o wu.json da raiz de cada microapp. Cada configuração Vite adiciona um pequeno plugin que grava esse mesmo arquivo em seu dist/. Depois, wu build mantém suas permissões e atualiza entry para apontar para o JavaScript compilado. Você não mantém uma segunda cópia do manifesto manualmente.

mf-mi-plataforma/vite.config.js · Abrir arquivo completo

import { readFileSync } from 'node:fs';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react(),
    {
      name: 'emit-wu-manifest',
      generateBundle() {
        this.emitFile({
          type: 'asset',
          fileName: 'wu.json',
          source: readFileSync(new URL('./wu.json', import.meta.url), 'utf8'),
        });
      },
    },
  ],
  build: { target: 'esnext' },
});

mf-inspector/vite.config.js · Abrir arquivo completo

import { readFileSync } from 'node:fs';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [
    vue(),
    {
      name: 'emit-wu-manifest',
      generateBundle() {
        this.emitFile({
          type: 'asset',
          fileName: 'wu.json',
          source: readFileSync(new URL('./wu.json', import.meta.url), 'utf8'),
        });
      },
    },
  ],
  build: { target: 'esnext' },
});

Manter o shell gerado

Esta configuração mantém as duas apps do tutorial e adiciona suas concessões. Assim, você também pode abri-las pela navegação da página principal do workspace. Essa navegação mostra uma app por vez; /contador/ permite ver as duas simultaneamente.

wu.config.json · Abrir arquivo completo

{
  "name": "mi-plataforma",
  "version": "1.1.0",
  "shell": {
    "dir": "shell", "port": 4321, "framework": "astro",
    "dev_cmd": "npx astro dev", "build_cmd": "npx astro build"
  },
  "apps": [
    {
      "name": "mi-plataforma", "dir": "mf-mi-plataforma", "framework": "react", "port": 5001,
      "grants": ["store.read:demo.counter", "store.write:demo.counter"]
    },
    {
      "name": "inspector", "dir": "mf-inspector", "framework": "vue", "port": 5002,
      "grants": ["store.read:demo.counter", "store.write:demo.counter"]
    }
  ],
  "proxy": { "port": 3000, "open_browser": true }
}

5. A página Astro que as conecta

Esta é a página completa. Seu script é executado no navegador, inicializa o WU e monta cada app no respectivo contêiner. Declara explicitamente as concessões desta página porque ela é um host adicional ao shell gerado.

A inicialização do dado fica no host e é executada antes da montagem. O botão do Vue usa unmount com force: true para aguardar a limpeza imediata antes de permitir outra montagem.

shell/src/pages/contador.astro · Abrir arquivo completo

<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Un contador compartido · WU</title>
    <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
  </head>
  <body>
    <main>
      <a href="/">Volver al workspace</a>
      <h1>Un contador, React y Vue</h1>
      <p>Las dos apps leen y escriben el mismo dato en WU.</p>
      <p id="estado" role="status">Cargando las apps…</p>
      <div class="contadores">
        <div id="react-counter"></div>
        <div id="vue-counter"></div>
      </div>
      <button id="toggle-vue" type="button" disabled>Desmontar Vue</button>
      <p>Desmonta Vue, cambia el valor desde React y vuelve a montar Vue.</p>
    </main>
    <script>
      import { wu } from 'wu-framework';

      const status = document.querySelector<HTMLElement>('#estado')!;
      const toggle = document.querySelector<HTMLButtonElement>('#toggle-vue')!;
      let vueMounted = false;

      try {
        await wu.init({
          sandbox: 'module',
          apps: [
            {
              name: 'mi-plataforma', url: '/mf-mi-plataforma',
              grants: ['store.read:demo.counter', 'store.write:demo.counter'],
            },
            {
              name: 'inspector', url: '/mf-inspector',
              grants: ['store.read:demo.counter', 'store.write:demo.counter'],
            },
          ],
        });
        if (wu.store.get('demo.counter') === undefined) wu.store.set('demo.counter', 0);
        await wu.mount('mi-plataforma', '#react-counter');
        await wu.mount('inspector', '#vue-counter');
        vueMounted = true;
        toggle.disabled = false;
        status.textContent = 'React y Vue conectados.';
      } catch (error) {
        status.textContent = `No se pudo iniciar: ${error}`;
      }

      toggle.addEventListener('click', async () => {
        toggle.disabled = true;
        try {
          if (vueMounted) {
            await wu.unmount('inspector', { force: true });
          } else {
            await wu.mount('inspector', '#vue-counter');
          }
          vueMounted = !vueMounted;
          toggle.textContent = vueMounted ? 'Desmontar Vue' : 'Montar Vue';
          status.textContent = vueMounted ? 'React y Vue conectados.' : 'Vue desmontado. React sigue conectado.';
        } catch (error) {
          status.textContent = `No se pudo cambiar Vue: ${error}`;
        } finally {
          toggle.disabled = false;
        }
      });
    </script>
    <style is:global>
      body { margin: 0; font: 1rem/1.6 system-ui, sans-serif; color: #192a3b; background: #f5f7fb; }
      main { max-width: 64rem; margin: auto; padding: 2rem 1rem; }
      a { color: #124ca6; }
      .contadores { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); gap: 1rem; margin: 1.5rem 0; }
      section { padding: 1.5rem; border: 1px solid #bac7d6; border-radius: 1rem; background: white; }
      h2 { margin-top: 0; font-size: 1.25rem; }
      output { font-size: 2rem; font-weight: 700; }
      button { font: inherit; padding: .6rem .9rem; margin-block: .25rem; border: 1px solid #617387; border-radius: .4rem; background: white; color: #192a3b; cursor: pointer; }
      button:focus-visible, a:focus-visible { outline: 3px solid #155fc9; outline-offset: 3px; }
      button:disabled { cursor: wait; opacity: .6; }
    </style>
  </body>
</html>

Emitir o CSS do shell em arquivos

Esta configuração faz o Astro gerar arquivos CSS externos, incluindo os estilos de /contador/. Assim, eles são carregados com a política de estilos de wu serve, que permite recursos da mesma origem. Mantenha essa opção ao personalizar a configuração do shell.

shell/astro.config.mjs · Abrir arquivo completo

import { defineConfig } from 'astro/config';

export default defineConfig({
  server: { port: 4321 },
  build: { inlineStylesheets: 'never' },
});

6. Verificar a saída de produção

Pare o servidor com Ctrl+C e execute na raiz:

wu build
wu serve 4100 --mode static

Abra http://127.0.0.1:4100/contador/index.html e repita as seis interações. Recarregue essa mesma rota: as duas apps devem carregar novamente e começar em zero. Você está servindo o dist/ combinado do workspace.

wu serve 4100 --mode static resolve índices de diretório e retorna 404 reais para rotas inexistentes. Use --mode spa apenas quando rotas do cliente precisarem de fallback para o shell.

Se algo não corresponder

O que você observa O que revisar
/contador/ mostra a página anterior ou não existe Confira shell/src/pages/contador.astro e o build do shell no terminal. Se copiou os arquivos com o desenvolvimento parado, execute npm --prefix shell run build antes de iniciar.
Uma permissão é negada Cada app deve ter seu wu.json; as concessões do host devem cobrir store.read e store.write de demo.counter.
Funciona no desenvolvimento, mas a permissão é negada em produção Verifique se copiou ambos os vite.config.js e se dist/mf-mi-plataforma/wu.json e dist/mf-inspector/wu.json mantêm wu.permissions.
Apenas um contador muda Substitua tanto App quanto main em cada app; os quatro arquivos devem corresponder a esta receita.
O valor inicial não aparece A página deve inicializar o store antes da montagem; cada componente deve ler ao se conectar.

O próximo passo é Pedir uma soma a outra app: uma app oferece uma operação e outra pede seu resultado. Para aprofundar em caminhos, snapshots e notificações, consulte Estado compartilhado.

CONSTRUA O WU CONOSCO

Encontrou um bug? Tem uma ideia?

Sua experiência melhora o WU. Conte o que falhou ou ajude a manter seu desenvolvimento.