WU / DOCSMICROFRONTENDS UNIVERSAIS
PT

Uma soma entre duas microapps

Crie um cliente e um provedor RPC com WU CLI e Astro: arquivos completos, permissões, versões, erros e desmontagem verificáveis.

O que você vai construir

Um formulário envia 7 e 8 a outra microapp, que valida os números e retorna 15. O cliente mostra 7 + 8 = 15. Depois, você parará o serviço, verificará o erro e o iniciará novamente.

Continue no workspace mi-plataforma de Adicionar uma segunda app. Você também pode aplicar esta receita após o contador compartilhado entre React e Vue: as duas apps mantêm seus componentes. Aqui adicionaremos duas apps pequenas em JavaScript para focar nas chamadas RPC.

Shell Astro: concede permisos y monta las dos apps
  cliente-suma → request('ejemplo:suma', { a: 7, b: 8 })
    sumador → handle('ejemplo:suma', ...) → 15
  cliente-suma → muestra «7 + 8 = 15»

O cálculo acontece no navegador. O WU coordena a solicitação entre as apps; esta receita não abre uma API HTTP. Para estudar um provedor com mais operações e um contrato descoberto com provide/consume, continue depois com Um serviço conectado com WU.

Versões desta receita: Node.js 24, WU CLI 1.2.0 e wu-framework 4.0.0. Use a CLI que instalou em Sua primeira microapp.

1. Adicionar o cliente e o provedor

Pare o servidor com Ctrl+C. Na raiz mi-plataforma/, ao lado de wu.config.json, execute:

wu add vanilla sumador --no-install
wu add vanilla cliente-suma --no-install

A ordem é framework, nome. A CLI cria mf-sumador/ e mf-cliente-suma/, com suas dependências e entradas HTML, e registra ambas as apps.

Baixe todos os arquivos da receita. Copie as três pastas do ZIP para o workspace, mescle as pastas existentes e substitua os arquivos de mesmo nome. Copie também wu.config.json. Substitua também o index.html de cada app: ele deve carregar src/main.js, a entrada JavaScript desta receita. Mantenha os demais arquivos do workspace.

mi-plataforma/
├── wu.config.json
├── mf-sumador/
│   ├── index.html
│   ├── package.json
│   ├── vite.config.js
│   ├── styles.css
│   ├── wu.json
│   └── src/main.js
├── mf-cliente-suma/
│   ├── index.html
│   ├── package.json
│   ├── vite.config.js
│   ├── styles.css
│   ├── wu.json
│   └── src/main.js
└── shell/
    ├── astro.config.mjs
    └── src/pages/suma.astro

A página suma.astro substitui o exemplo anterior que fazia a soma no próprio host, caso você o tenha criado. As páginas do workspace e do contador continuam disponíveis. Se personalizou seu workspace, incorpore as novas entradas e concessões da configuração abaixo, mantendo os demais ajustes.

Você também pode copiar os arquivos completos apresentados a seguir.

2. Solicitar permissões e concedê-las

O provedor solicita rpc.provide:ejemplo:suma; o cliente solicita rpc.call:ejemplo:suma. required: true faz a inicialização falhar com um diagnóstico se o host não conceder essa permissão. O manifesto descreve a necessidade; os grants do host dão a autorização.

mf-sumador/wu.json

Ver arquivo completo

{
  "name": "sumador",
  "version": "1.0.0",
  "entry": "src/main.js",
  "styleMode": "isolated",
  "wu": {
    "permissions": [
      { "capability": "rpc.provide", "resource": "ejemplo:suma", "required": true }
    ]
  }
}

mf-cliente-suma/wu.json

Ver arquivo completo

{
  "name": "cliente-suma",
  "version": "1.0.0",
  "entry": "src/main.js",
  "styleMode": "isolated",
  "wu": {
    "permissions": [
      { "capability": "rpc.call", "resource": "ejemplo:suma", "required": true }
    ]
  }
}

Esta é a configuração completa do workspace após adicionar as duas apps. As concessões do contador mantêm o escopo demo.counter. As duas novas concessões permitem a operação de soma pela navegação do shell gerado.

wu.config.json

Ver 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"
      ]
    },
    {
      "name": "sumador",
      "dir": "mf-sumador",
      "framework": "vanilla",
      "port": 5003,
      "grants": [
        "rpc.provide:ejemplo:suma"
      ]
    },
    {
      "name": "cliente-suma",
      "dir": "mf-cliente-suma",
      "framework": "vanilla",
      "port": 5004,
      "grants": [
        "rpc.call:ejemplo:suma"
      ]
    }
  ],
  "proxy": {
    "port": 3000,
    "open_browser": true
  }
}

3. Implementar o provedor

wu.define registra o ciclo de vida de sumador. O WU entrega context.wu ao montá-lo: essa fachada carrega a identidade e as permissões desta app. O handler valida a entrada, calcula o resultado e mostra qual cliente o chamou.

version: '1.0.0' é a versão da operação. allow: ['cliente-suma'] adiciona uma verificação de identidade: apenas esse cliente pode invocá-la, mesmo que outra app tenha uma concessão para o mesmo canal. A função retornada por handle remove o handler; nós a executamos em unmount.

mf-sumador/src/main.js

Ver arquivo completo

import { waitForWu } from 'wu-framework/adapters/shared';

const wu = await waitForWu();

let release;

wu.define('sumador', {
  mount(container, context) {
    container.innerHTML = `
      <link rel="stylesheet" href="/mf-sumador/styles.css">
      <section aria-labelledby="service-title">
        <h2 id="service-title">Servicio de suma</h2>
        <p role="status">Listo para recibir solicitudes.</p>
      </section>
    `;
    const status = container.querySelector('[role="status"]');

    release = context.wu.handle('ejemplo:suma', ({ a, b }, request) => {
      if (!Number.isFinite(a) || !Number.isFinite(b)) {
        throw new TypeError('Escribe dos números finitos.');
      }
      const total = a + b;
      if (!Number.isFinite(total)) {
        throw new RangeError('La suma excede el rango numérico.');
      }
      status.textContent = `Respondí a ${request.caller.appName}: ${a} + ${b} = ${total}`;
      return total;
    }, {
      version: '1.0.0',
      allow: ['cliente-suma'],
    });
  },

  unmount(container) {
    release?.();
    release = undefined;
    container.replaceChildren();
  },
});

4. Implementar o cliente

O cliente não importa a função do provedor. Envia os dois números por context.wu.request, espera a resposta e atualiza seu próprio DOM. Solicita uma versão compatível com ^1.0.0 e limita a espera a 1000 ms.

Um campo vazio é enviado como null para que você observe a rejeição da validação do provedor. try/catch apresenta os erros sem deixar a interface bloqueada. Ao desmontar o cliente, seus listeners são removidos e qualquer solicitação pendente é cancelada.

mf-cliente-suma/src/main.js

Ver arquivo completo

import { waitForWu } from 'wu-framework/adapters/shared';

const wu = await waitForWu();

let dispose;

wu.define('cliente-suma', {
  mount(container, context) {
    container.innerHTML = `
      <link rel="stylesheet" href="/mf-cliente-suma/styles.css">
      <section aria-labelledby="client-title">
        <h2 id="client-title">Cliente de suma</h2>
        <form>
          <label>Primer número<input name="a" type="number" step="any" value="7"></label>
          <label>Segundo número<input name="b" type="number" step="any" value="8"></label>
          <button type="submit">Calcular</button>
        </form>
        <output aria-live="polite">Pulsa Calcular para solicitar una suma.</output>
      </section>
    `;
    const form = container.querySelector('form');
    const first = form.elements.namedItem('a');
    const second = form.elements.namedItem('b');
    const button = form.querySelector('button');
    const output = container.querySelector('output');
    const listeners = new AbortController();
    let pending;
    let active = true;

    form.addEventListener('submit', async (event) => {
      event.preventDefault();
      if (pending) return;
      const a = first.value === '' ? null : first.valueAsNumber;
      const b = second.value === '' ? null : second.valueAsNumber;
      pending = new AbortController();
      button.disabled = true;
      output.textContent = 'Calculando…';
      try {
        const total = await context.wu.request('ejemplo:suma', { a, b }, {
          version: '^1.0.0',
          timeout: 1000,
          signal: pending.signal,
        });
        if (active) output.textContent = `${a} + ${b} = ${total}`;
      } catch (error) {
        if (!active) return;
        output.textContent = error.code === 'WU_RPC_NO_HANDLER'
          ? 'El servicio está detenido. Inícialo y vuelve a pulsar Calcular.'
          : `No se pudo calcular: ${error.message}`;
      } finally {
        pending = undefined;
        if (active) button.disabled = false;
      }
    }, { signal: listeners.signal });

    dispose = () => {
      active = false;
      listeners.abort();
      pending?.abort();
    };
  },

  unmount(container) {
    dispose?.();
    dispose = undefined;
    container.replaceChildren();
  },
});

5. Montar ambas as apps com Astro

Esta página configura suas próprias apps e concessões. Monta primeiro o serviço e depois o cliente. Os botões permitem desmontar e montar o provedor sem recarregar o cliente: você poderá verificar que ele realmente depende de um handler disponível.

shell/src/pages/suma.astro

Ver arquivo completo

<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Una suma entre dos microapps WU</title>
    <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
  </head>
  <body>
    <main>
      <a href="/">Volver al workspace</a>
      <h1>Una suma entre dos microapps</h1>
      <p>El cliente pide el resultado y el servicio calcula la suma.</p>
      <div class="controls">
        <button id="start" disabled>Iniciar servicio</button>
        <button id="stop" disabled>Detener servicio</button>
      </div>
      <p id="status" role="status">Iniciando WU…</p>
      <div id="service"></div>
      <div id="client"></div>
    </main>
    <script>
      import { wu } from 'wu-framework';

      const start = document.querySelector<HTMLButtonElement>('#start')!;
      const stop = document.querySelector<HTMLButtonElement>('#stop')!;
      const status = document.querySelector<HTMLElement>('#status')!;
      let running = false;

      async function setService(enabled: boolean) {
        start.disabled = true;
        stop.disabled = true;
        try {
          if (enabled) await wu.mount('sumador', '#service');
          else await wu.unmount('sumador', { force: true });
          running = enabled;
          status.textContent = enabled
            ? 'Servicio disponible. Pulsa Calcular.'
            : 'Servicio detenido. Pulsa Calcular para comprobar el error.';
        } catch (error) {
          status.textContent = `No se pudo cambiar el servicio: ${error}`;
        } finally {
          start.disabled = running;
          stop.disabled = !running;
        }
      }

      try {
        await wu.init({
          sandbox: 'module',
          apps: [
            {
              name: 'sumador',
              url: `${location.origin}/mf-sumador`,
              grants: ['rpc.provide:ejemplo:suma'],
            },
            {
              name: 'cliente-suma',
              url: `${location.origin}/mf-cliente-suma`,
              grants: ['rpc.call:ejemplo:suma'],
            },
          ],
        });
        await wu.mount('sumador', '#service');
        running = true;
        await wu.mount('cliente-suma', '#client');
        stop.disabled = false;
        status.textContent = 'Servicio disponible. Pulsa Calcular.';
        start.addEventListener('click', () => { void setService(true); });
        stop.addEventListener('click', () => { void setService(false); });
      } catch (error) {
        status.textContent = `No se pudo iniciar WU: ${error}`;
      }
    </script>
  </body>
</html>

<style>
  :global(body) { margin: 0; background: #f4f7f6; color: #172b34; font: 1rem/1.6 system-ui, sans-serif; }
  main { max-width: 48rem; margin: 0 auto; padding: 2rem 1rem; }
  h1 { font-size: clamp(1.6rem, 5vw, 2.5rem); line-height: 1.2; }
  a { color: #175f56; }
  .controls { display: flex; flex-wrap: wrap; gap: .75rem; }
  button { border: 1px solid #536f74; border-radius: .5rem; padding: .6rem 1rem; color: #172b34; background: white; font: inherit; cursor: pointer; }
  button:disabled { opacity: .55; cursor: default; }
  :focus-visible { outline: 3px solid #174bac; outline-offset: 3px; }
  #client { margin-top: 1rem; }
</style>

waitForWu localiza o runtime do shell para registrar cada app com define. RPC usa a facade de mount, com identidade, versão e allowlist. Framework 4 também preserva essas opções em wu.handle; use a facade com escopo dentro de uma microapp.

6. Manter a entrada e o manifesto no build

O index.html de cada app também é a entrada do build do Vite. Os dois arquivos abaixo apontam para src/main.js. Eles substituem os do template para que desenvolvimento e produção executem o mesmo provedor e o mesmo cliente.

mf-sumador/index.html

Ver arquivo completo

<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Servicio de suma · WU</title>
  </head>
  <body><script type="module" src="/src/main.js"></script></body>
</html>

mf-cliente-suma/index.html

Ver arquivo completo

<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Cliente de suma · WU</title>
  </head>
  <body><script type="module" src="/src/main.js"></script></body>
</html>

Servir os estilos como arquivos

Astro gera uma folha CSS externa com inlineStylesheets: 'never'. Cada microapp carrega seu próprio styles.css pelo Shadow DOM. Assim, os estilos cumprem uma política style-src 'self', sem precisar de estilos inline ou alterar a CSP do servidor. Se já aplicou o contador, mantenha essa mesma opção na configuração do shell.

shell/astro.config.mjs

Ver arquivo completo

import { defineConfig } from 'astro/config';

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

mf-sumador/styles.css

Ver arquivo completo

:host { display: block; font: 1rem/1.6 system-ui, sans-serif; color: #172b34; }
section { padding: 1.5rem; background: #edf7f1; border-radius: 1rem; }
h2 { margin-top: 0; font-size: 1.25rem; }
p { margin-bottom: 0; }

mf-cliente-suma/styles.css

Ver arquivo completo

:host { display: block; font: 1rem/1.6 system-ui, sans-serif; color: #172b34; }
section { padding: 1.5rem; background: white; border: 1px solid #cbd9d9; border-radius: 1rem; }
h2 { margin-top: 0; font-size: 1.25rem; }
form { display: flex; gap: 1rem; flex-wrap: wrap; align-items: end; }
label { display: grid; gap: .25rem; }
input { box-sizing: border-box; width: 7rem; padding: .65rem; font: inherit; border: 1px solid #6e838b; border-radius: .4rem; }
button { padding: .7rem 1rem; font: inherit; color: white; background: #126153; border: 0; border-radius: .4rem; cursor: pointer; }
button:disabled { opacity: .65; cursor: wait; }
:focus-visible { outline: 3px solid #174bac; outline-offset: 3px; }
output { display: block; margin-top: 1rem; font-weight: 650; }

Há um único wu.json fonte por app, na raiz dela. O modo nativo da CLI o serve diretamente. O Vite também deve incluí-lo na saída: este plugin emite tanto o manifesto quanto styles.css sem manter cópias adicionais manualmente. Ao coletar o resultado, wu build mantém as permissões e substitui entry pelo JavaScript compilado.

Arquivos completos de dependências e build incluídos no ZIP

mf-sumador/package.json

Ver arquivo completo

{
  "name": "wu-sumador",
  "private": true,
  "type": "module",
  "engines": { "node": ">=24" },
  "scripts": { "dev": "vite --port 5003", "build": "vite build" },
  "dependencies": { "wu-framework": "4.0.0" },
  "devDependencies": { "vite": "7.3.6" }
}

mf-cliente-suma/package.json

Ver arquivo completo

{
  "name": "wu-cliente-suma",
  "private": true,
  "type": "module",
  "engines": { "node": ">=24" },
  "scripts": { "dev": "vite --port 5004", "build": "vite build" },
  "dependencies": { "wu-framework": "4.0.0" },
  "devDependencies": { "vite": "7.3.6" }
}

mf-sumador/vite.config.js

Ver arquivo completo

import { readFileSync } from 'node:fs';
import { defineConfig } from 'vite';

export default defineConfig({
  base: './',
  build: { target: 'esnext' },
  plugins: [{
    name: 'wu-assets',
    generateBundle() {
      for (const fileName of ['wu.json', 'styles.css']) {
        this.emitFile({
          type: 'asset',
          fileName,
          source: readFileSync(new URL(fileName, import.meta.url), 'utf8'),
        });
      }
    },
  }],
});

mf-cliente-suma/vite.config.js

Ver arquivo completo

import { readFileSync } from 'node:fs';
import { defineConfig } from 'vite';

export default defineConfig({
  base: './',
  build: { target: 'esnext' },
  plugins: [{
    name: 'wu-assets',
    generateBundle() {
      for (const fileName of ['wu.json', 'styles.css']) {
        this.emitFile({
          type: 'asset',
          fileName,
          source: readFileSync(new URL(fileName, import.meta.url), 'utf8'),
        });
      }
    },
  }],
});

7. Executar e reconhecer o resultado

Após copiar os arquivos, na raiz do workspace:

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

Este build inicial inclui suma.astro em shell/dist/ após copiar os arquivos com o servidor parado. Se a saída do shell já existir, a CLI 1.2.0 pode reutilizá-la ao iniciar. Com wu dev --native ativo, o watcher recompila o shell quando suas fontes mudam e então recarrega a página; não é preciso parar e compilar manualmente a cada edição. Se a compilação falhar, confira o erro no terminal.

Abra http://127.0.0.1:3000/suma/. Você deve ver Servicio de suma, Cliente de suma e os números 7 e 8.

Ação Resultado visível
Clique em Calcular O cliente mostra 7 + 8 = 15; o serviço indica que respondeu a cliente-suma.
Mude o segundo número para 3 e calcule 7 + 3 = 10.
Esvazie o primeiro número e calcule No se pudo calcular: Escribe dos números finitos.
Restaure 7 e 8, clique em Detener servicio e calcule O cliente explica que o serviço está parado e como tentar novamente.
Clique em Iniciar servicio e calcule 7 + 8 = 15 novamente.

Ao voltar à página principal, as novas apps também aparecem na navegação. Abra sumador antes de calcular em cliente-suma; essa navegação monta cada app quando você a visita. A página /suma/ segue essa ordem automaticamente.

Para verificar a versão, altere temporariamente a versão do handler para 2.0.0, recarregue /suma/ e calcule. A solicitação ^1.0.0 é rejeitada por incompatibilidade. Restaure 1.0.0 ao terminar.

8. Verificar a versão que você publicará

Pare o desenvolvimento com Ctrl+C e execute:

wu build
wu serve 4100 --mode static

Abra http://127.0.0.1:4100/suma/index.html. Repita a soma, a remoção e a nova montagem do serviço; depois, recarregue essa URL. Os mesmos resultados devem funcionar com os arquivos compilados. A saída publicável é dist/ na raiz 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 falhar

Sintoma O que verificar
/suma/ não aparece ou mostra a soma antiga Se copiou a página com o desenvolvimento parado, execute npm --prefix shell run build antes de iniciar. Durante o desenvolvimento, verifique se a recompilação automática do shell não tem erros.
O WU informa que falta uma permissão obrigatória Compare a permissão do wu.json com a concessão da app correspondente em suma.astro; na página principal, verifique wu.config.json.
O cliente diz que o serviço está parado Clique em Iniciar servicio. Na navegação principal, abra primeiro sumador.
A página funciona, mas perde seus estilos Mantenha build.inlineStylesheets: 'never' em shell/astro.config.mjs; verifique se os dois styles.css existem em dist/mf-*/ e execute wu build.
O contador Vanilla JS reaparece em produção Substitua ambos os index.html pelos desta receita; seus scripts devem carregar /src/main.js. Execute wu build novamente.
Funciona no desenvolvimento e falha em produção Verifique se copiou ambos os vite.config.js e se dist/mf-sumador/wu.json e dist/mf-cliente-suma/wu.json mantêm suas permissões com um entry de produção.
A solicitação é rejeitada pela versão O handler publica 1.0.0 e o cliente solicita ^1.0.0; eles devem ser compatíveis.

Continuar com as APIs

Você já usou init, define, mount, unmount, handle e request entre duas apps com permissões diferentes. Eventos, contratos e RPC explica as assinaturas e variantes; a referência de comunicação lista as APIs. Para adicionar descoberta de contratos, cancelamento observável e outras operações, continue com Um serviço conectado com WU.

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.