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.
ssrLoadModulecarrega módulos no servidor com HMR incluso.ssrTransformconverte código para SSR. Proxy do Vite redireciona chamadas de API. WSS tunneling para HMR atrás de proxies.