kb.erickguedes.com
Vite: Build Tool do Futuro

SSR e Recursos Avançados

Aula 4 de 4

SSR (Server-Side Rendering)

Vite suporta SSR de forma flexível, permitindo integração com qualquer framework.

Configuração SSR

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    ssr: true, // Build para SSR
    rollupOptions: {
      input: './src/entry-server.tsx'
    }
  },
  ssr: {
    // Módulos a serem externalizados (não bundled para SSR)
    external: ['react', 'react-dom/server'],
    noExternal: ['@my-company/*'] // Forçar bundle de certos módulos
  }
});

Estrutura SSR

src/
├── entry-client.tsx      # Entry para cliente
├── entry-server.tsx      # Entry para servidor
├── App.tsx
└── routes.ts
// src/entry-client.tsx
import { hydrateRoot } from 'react-dom/client';
import App from './App';

hydrateRoot(document.getElementById('root')!, <App />);
// src/entry-server.tsx
import { renderToString } from 'react-dom/server';
import App from './App';

export function render(url: string) {
  return renderToString(<App url={url} />);
}

SSR Load Module

// server.js (servidor Node customizado)
import { createServer } from 'vite';

const HOST = 'http://localhost:3000';

async function startServer() {
  const vite = await createServer({
    server: { middlewareMode: true },
    appType: 'custom'
  });

  const app = express();

  // Usar Vite como middleware de dev
  app.use(vite.middlewares);

  // Handler SSR
  app.use('*', async (req, res) => {
    const url = req.originalUrl;

    try {
      // Carregar módulo SSR via Vite
      const { render } = await vite.ssrLoadModule('/src/entry-server.tsx');
      const html = render(url);

      // Template HTML
      const template = await vite.transformIndexHtml(url, `
        <!DOCTYPE html>
        <html>
          <head><title>SSR App</title></head>
          <body>
            <div id="root">${html}</div>
            <script type="module" src="/src/entry-client.tsx"></script>
          </body>
        </html>
      `);

      res.status(200).set({ 'Content-Type': 'text/html' }).end(template);
    } catch (e) {
      vite.ssrFixStacktrace(e);
      res.status(500).end(e.message);
    }
  });

  app.listen(3000);
}

startServer();

SSR Transform

// Uso avançado de SSR transform
const code = `
  import { useState } from 'react';
  export function Counter() {
    const [count, setCount] = useState(0);
    return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
  }
`;

// Transformar código para SSR
const result = await vite.ssrTransform(code, 'virtual:module.tsx');
console.log(result.code);

SSR External

// vite.config.ts
export default defineConfig({
  ssr: {
    // Externalizar tudo que for Node-specific
    external: [
      'react',
      'react-dom',
      'react-dom/server',
      'next/navigation',
      'next/server'
    ],

    // Forçar bundle de módulos que deveriam ser externalizados
    noExternal: [
      /@my-company\//,
      'some-esm-only-package'
    ],

    // Targets para SSR build
    target: 'node', // 'node' | 'webworker'
  },

  build: {
    ssr: true,
    ssrManifest: true, // Gerar manifesto SSR para produção
    rollupOptions: {
      input: './src/entry-server.tsx'
    }
  }
});

Framework Integration

React SSR

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    ssr: './src/entry-server.tsx'
  }
});
// src/entry-server.tsx
import { renderToPipeableStream } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom/server';
import App from './App';

export function render(url: string, options?: { onShellReady?: () => void }) {
  const stream = renderToPipeableStream(
    <StaticRouter location={url}>
      <App />
    </StaticRouter>,
    {
      bootstrapScripts: ['/src/entry-client.tsx'],
      onShellReady() {
        options?.onShellReady?.();
      }
    }
  );

  return stream;
}

Vue SSR

import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  build: {
    ssr: './src/entry-server.ts'
  }
});
// src/entry-server.ts
import { renderToString } from 'vue/server-renderer';
import { createApp } from './app';

export async function render(url: string) {
  const { app } = createApp();
  const html = await renderToString(app);
  return { html };
}

Svelte SSR

import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
  plugins: [svelte()],
  build: {
    ssr: './src/entry-server.ts'
  }
});

Production SSR

// server-prod.js
import express from 'express';
import { readFileSync } from 'fs';

const app = express();

// Servir arquivos estáticos
app.use('/assets', express.static('dist/client/assets'));

// Ler template HTML do build
const template = readFileSync('dist/client/index.html', 'utf-8');

// Importar entry server (pré-bundlado)
const { render } = await import('./dist/server/entry-server.js');

app.use('*', async (req, res) => {
  const url = req.originalUrl;
  const html = render(url);

  const finalHtml = template.replace('<!--ssr-outlet-->', html);
  res.status(200).set({ 'Content-Type': 'text/html' }).end(finalHtml);
});

app.listen(3000);
// vite.config.ts (produção)
export default defineConfig({
  build: {
    // Build cliente
    outDir: 'dist/client',
    // Build servidor SSR
    ssr: 'dist/server',
    ssrManifest: true,
    rollupOptions: {
      input: {
        client: './src/entry-client.tsx',
        server: './src/entry-server.tsx'
      }
    }
  }
});

Proxy (server.proxy)

// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      // Proxy simples
      '/api': 'http://localhost:8080',

      // Com opções
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        secure: false,

        // Rewrite path
        rewrite: (path) => path.replace(/^\/api/, ''),

        // Headers customizados
        headers: {
          'X-Proxy-By': 'Vite'
        },

        // Cookie domain rewrite
        cookieDomainRewrite: {
          '.backend.com': 'localhost'
        }
      },

      // WebSocket
      '/ws': {
        target: 'ws://localhost:8080',
        ws: true
      },

      // Regex pattern
      '^/api/.*': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  }
});

WebSocket HMR Tunneling

// vite.config.ts
export default defineConfig({
  server: {
    // Configuração HMR para ambientes atrás de proxy/reverse proxy
    hmr: {
      protocol: 'wss',      // WebSocket Secure
      host: 'meusite.com',   // Host público
      port: 443,             // Porta padrão HTTPS
      path: '/hmr/',         // Path customizado
      clientPort: 443        // Forçar porta do cliente
    }
  }
});

// Alternativa: desabilitar HMR
server: {
  hmr: false
}
# Tunneling HMR via SSH (desenvolvimento remoto)
ssh -L 3000:localhost:3000 -R 3001:localhost:3001 user@server

Lab: Exercício - SSR Completo

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  ssr: {
    external: ['react', 'react-dom']
  }
});
// src/entry-server.tsx
import { renderToString } from 'react-dom/server';
import App from './App';

export function render() {
  return renderToString(<App />);
}
// server.js
import express from 'express';
import { createServer } from 'vite';

async function start() {
  const app = express();
  const vite = await createServer({
    server: { middlewareMode: true },
    appType: 'custom'
  });

  app.use(vite.middlewares);

  app.use('*', async (req, res) => {
    const { render } = await vite.ssrLoadModule('/src/entry-server.tsx');
    const appHtml = render();

    const html = await vite.transformIndexHtml(req.url, `
      <!DOCTYPE html>
      <html>
        <head><title>SSR React</title></head>
        <body>
          <div id="root">${appHtml}</div>
          <script type="module" src="/src/entry-client.tsx"></script>
        </body>
      </html>
    `);

    res.end(html);
  });

  app.listen(3000, () => console.log('SSR rodando em http://localhost:3000'));
}

start();
npm run dev
# SSR funciona tanto em dev quanto em produção

Vite SSR é flexível e agnóstico a framework. ssrLoadModule carrega módulos no servidor com HMR incluso. ssrTransform converte código para SSR. Proxy do Vite redireciona chamadas de API. WSS tunneling para HMR atrás de proxies.