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
{
"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
{
"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
{
"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
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
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
<!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
<!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
<!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
import { defineConfig } from 'astro/config';
export default defineConfig({
server: { port: 4321 },
build: { inlineStylesheets: 'never' },
});
mf-sumador/styles.css
: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
: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
{
"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
{
"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
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
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.