EngineeringSenior DeveloperTechnical Sessions

Session 09: Vite Deep Dive & Modern Frontend Tooling

Mục tiêu: Hiểu kiến trúc Vite, tại sao nhanh, plugin system, và thiết lập production-ready React + Vite project.


1. Vite Architecture - Tại Sao Nhanh?

Dev Server: ESBuild + Native ESM

Webpack (truyền thống):
┌──────────┐    ┌──────────────────┐    ┌──────────┐
│ Entry    │───→│  Bundle ALL      │───→│ Dev      │
│ Point    │    │  modules (slow!) │    │ Server   │
└──────────┘    └──────────────────┘    └──────────┘
                    ↑
                 Mỗi lần save: rebuild toàn bộ bundle
                 Project lớn: 30-60 giây startup

Vite:
┌──────────┐    ┌──────────────────┐    ┌──────────┐
│ Entry    │───→│ ESBuild pre-     │───→│ Dev      │
│ Point    │    │ bundle deps ONLY │    │ Server   │
└──────────┘    └──────────────────┘    │ (Koa)    │
                                        └────┬─────┘
                                             │
                              Browser request: /src/App.tsx
                                             │
                              ┌──────────────▼──────────────┐
                              │  Transform on-demand        │
                              │  (chỉ file browser yêu cầu) │
                              │  ESBuild/SWC transform      │
                              └─────────────────────────────┘

→ Startup: <500ms (bất kể project size!)
→ HMR: <50ms (chỉ update module thay đổi)

Dependency Pre-Bundling

node_modules/ (thousands of files)
      │
      ▼ ESBuild (vite optimizeDeps)
      │
  .vite/deps/ (pre-bundled, cached)
      │
  react.js          ← 1 file thay vì 1000+ internal modules
  react-dom.js
  lodash-es.js      ← CJS → ESM conversion tự động
typescript
// vite.config.ts - dependency optimization
export default defineConfig({
  optimizeDeps: {
    include: [
      'react', 'react-dom', 'react-router-dom',
      'axios',  // Force pre-bundle
    ],
    exclude: ['my-local-package'],  // Không pre-bundle
  },
});

Production Build: Rollup

Development: ESBuild (fast, no bundling)
Production:  Rollup (tree-shaking, code-splitting, minification)

┌───────────┐     ┌──────────┐     ┌──────────────────┐
│ Source     │────→│ Rollup   │────→│ dist/             │
│ Code      │     │ + Terser │     │ ├── index.html    │
│           │     │          │     │ ├── assets/       │
│           │     │          │     │ │   ├── index-abc.js  │
│           │     │          │     │ │   ├── vendor-def.js │
│           │     │          │     │ │   └── style-ghi.css │
└───────────┘     └──────────┘     └──────────────────────┘

2. HMR (Hot Module Replacement) Internals

typescript
// Vite HMR Protocol:
// 1. File thay đổi → Vite detect via chokidar
// 2. Transform lại CHỈ file đó → ESBuild
// 3. Send update qua WebSocket → browser
// 4. Browser: chỉ replace module, giữ state

// Custom HMR handling (plugin development)
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // Module mới, cập nhật references
  });

  import.meta.hot.dispose(() => {
    // Cleanup: remove event listeners, subscriptions
  });

  // Preserve state across HMR
  import.meta.hot.data.count = import.meta.hot.data.count ?? 0;
}

// ⚠️ HMR boundaries:
// - React components: auto-handled bởi @vitejs/plugin-react
// - CSS/SCSS: auto hot-replace (không cần page reload)
// - JSON: auto hot-replace
// - Static assets: full page reload

3. Vite Configuration Nâng Cao

typescript
// vite.config.ts
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react-swc';  // SWC thay Babel, ~20x faster
import path from 'path';

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');

  return {
    plugins: [
      react(),
    ],

    // Path aliases
    resolve: {
      alias: {
        '@': path.resolve(__dirname, './src'),
        '@components': path.resolve(__dirname, './src/components'),
        '@hooks': path.resolve(__dirname, './src/hooks'),
        '@api': path.resolve(__dirname, './src/api'),
      },
    },

    // Dev server
    server: {
      port: 3000,
      strictPort: true,   // Fail nếu port bận (không auto-switch)
      open: true,

      // Proxy API calls → .NET backend
      proxy: {
        '/api': {
          target: 'https://localhost:5001',
          changeOrigin: true,
          secure: false,  // Self-signed cert
          // rewrite: (path) => path.replace(/^\/api/, ''),
        },
        '/hubs': {
          target: 'https://localhost:5001',
          ws: true,       // WebSocket proxy cho SignalR
          secure: false,
        },
      },
    },

    // Build configuration
    build: {
      target: 'es2022',
      outDir: 'dist',
      sourcemap: mode === 'production' ? 'hidden' : true,
      minify: 'terser',
      terserOptions: {
        compress: {
          drop_console: mode === 'production',
          drop_debugger: true,
        },
      },

      // Code splitting
      rollupOptions: {
        output: {
          manualChunks: {
            vendor: ['react', 'react-dom', 'react-router-dom'],
            charts: ['recharts'],
            utils: ['date-fns', 'lodash-es'],
          },
        },
      },

      // Chunk size warning
      chunkSizeWarningLimit: 500, // KB
    },

    // CSS
    css: {
      modules: {
        localsConvention: 'camelCase',
      },
      preprocessorOptions: {
        scss: {
          additionalData: `@use "@/styles/variables" as *;`,
        },
      },
    },

    // Environment variables
    // Chỉ VITE_ prefix mới expose ra client
    define: {
      __APP_VERSION__: JSON.stringify(process.env.npm_package_version),
    },
  };
});

Environment Variables

bash
# .env                    → mọi environment
# .env.local              → local overrides (gitignored)
# .env.development        → dev only
# .env.production         → production only

# ✅ Exposed to client (prefix VITE_)
VITE_API_URL=https://api.myapp.com
VITE_APP_NAME=MyApp

# ❌ Server-only (no prefix)
DATABASE_URL=postgresql://...
API_SECRET=xxx
typescript
// Sử dụng - type-safe
/// <reference types="vite/client" />
interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_APP_NAME: string;
}

const apiUrl = import.meta.env.VITE_API_URL;
const isDev = import.meta.env.DEV;     // boolean
const isProd = import.meta.env.PROD;   // boolean

4. React + Vite: SPA Setup Production-Ready

Project Structure

src/
├── api/              ← API client & types
│   ├── client.ts     ← Axios/fetch wrapper
│   ├── products.ts   ← Product API functions
│   └── types.ts      ← API response types
├── components/       ← Shared UI components
│   ├── ui/           ← Primitives (Button, Input, Card)
│   └── layout/       ← Layout components
├── features/         ← Feature modules
│   ├── products/
│   │   ├── components/
│   │   ├── hooks/
│   │   └── index.ts
│   └── orders/
├── hooks/            ← Shared hooks
├── pages/            ← Route pages (flat)
│   ├── HomePage.tsx
│   ├── ProductsPage.tsx
│   └── OrdersPage.tsx
├── providers/        ← Context providers
├── routes/           ← Route configuration
│   └── index.tsx
├── styles/           ← Global styles
├── utils/            ← Helper functions
├── App.tsx
└── main.tsx

Routing with React Router

tsx
// src/routes/index.tsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom';

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    children: [
      { index: true, element: <HomePage /> },
      {
        path: 'products',
        element: <ProductsPage />,
        loader: productsLoader,   // Data loading
      },
      {
        path: 'products/:id',
        element: <ProductDetailPage />,
        loader: productDetailLoader,
      },
      {
        path: 'dashboard',
        lazy: () => import('../pages/DashboardPage'), // Code-splitting
      },
    ],
  },
]);

export function AppRouter() {
  return <RouterProvider router={router} />;
}

5. Vite Plugin System

typescript
// Custom Vite plugin anatomy
import type { Plugin } from 'vite';

function myPlugin(): Plugin {
  return {
    name: 'my-custom-plugin',      // Required: unique name

    // Build hooks (Rollup compatible)
    buildStart() { /* ... */ },
    resolveId(source) { /* ... */ },
    load(id) { /* ... */ },
    transform(code, id) {
      // Transform specific files
      if (id.endsWith('.md')) {
        return `export default ${JSON.stringify(marked(code))}`;
      }
    },

    // Vite-specific hooks
    configResolved(config) {
      // Access resolved config
    },
    configureServer(server) {
      // Add custom middleware to dev server
      server.middlewares.use('/custom', (req, res) => {
        res.end('Custom endpoint');
      });
    },
    transformIndexHtml(html) {
      // Modify index.html
      return html.replace('</head>',
        '<script>window.__BUILD_TIME__=Date.now()</script></head>');
    },
    handleHotUpdate({ file, server }) {
      // Custom HMR behavior
      if (file.endsWith('.config.json')) {
        server.ws.send({ type: 'full-reload' });
        return [];
      }
    },
  };
}

// Popular Vite plugins
// @vitejs/plugin-react-swc   → React + SWC (fastest)
// vite-plugin-svgr           → SVG as React components
// vite-plugin-pwa            → Progressive Web App
// vite-tsconfig-paths        → TypeScript path aliases
// vite-plugin-checker         → TypeScript/ESLint checking

6. Vitest - Testing Tích Hợp Với Vite

typescript
// vitest.config.ts (hoặc trong vite.config.ts)
/// <reference types="vitest" />
import { defineConfig } from 'vite';

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/test/setup.ts',
    css: true,
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'src/test/'],
    },
  },
});

// src/test/setup.ts
import '@testing-library/jest-dom';

// Test example
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { ProductCard } from '@/components/ProductCard';

describe('ProductCard', () => {
  it('renders product info', () => {
    const product = { id: '1', name: 'Widget', price: 29.99 };
    render(<ProductCard product={product} />);

    expect(screen.getByText('Widget')).toBeInTheDocument();
    expect(screen.getByText('$29.99')).toBeInTheDocument();
  });

  it('calls onAddToCart when button clicked', async () => {
    const onAddToCart = vi.fn();
    const product = { id: '1', name: 'Widget', price: 29.99 };

    render(<ProductCard product={product} onAddToCart={onAddToCart} />);
    fireEvent.click(screen.getByRole('button', { name: /add to cart/i }));

    expect(onAddToCart).toHaveBeenCalledWith('1');
  });
});

// Chạy: npx vitest          → watch mode
//        npx vitest run      → single run
//        npx vitest --ui     → web UI

7. Library Mode & SSR Mode

Library Mode

typescript
// vite.config.ts - build as library
export default defineConfig({
  build: {
    lib: {
      entry: path.resolve(__dirname, 'src/index.ts'),
      name: 'MyUILib',
      formats: ['es', 'cjs'],
    },
    rollupOptions: {
      external: ['react', 'react-dom'],  // Peer deps
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
    },
  },
});

SSR Mode

typescript
// vite.config.ts - SSR configuration
export default defineConfig({
  ssr: {
    noExternal: ['my-package'],  // Bundle this in SSR
    external: ['database-driver'],
  },
});

// Server entry
// server.ts
import { createServer as createViteServer } from 'vite';
import express from 'express';

const app = express();

const vite = await createViteServer({
  server: { middlewareMode: true },
  appType: 'custom',
});

app.use(vite.middlewares);

app.get('*', async (req, res) => {
  const { render } = await vite.ssrLoadModule('/src/entry-server.tsx');
  const html = await render(req.url);
  res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
});

Tổng Kết Session 09

ConceptKey Takeaway
ESBuild (dev)Pre-bundle deps, transform on-demand → startup <500ms
Rollup (build)Tree-shaking, code-splitting, minification
HMRModule-level hot replace, <50ms update
ProxyDev server proxy → .NET backend, WebSocket support
VitestNative Vite integration, jest-compatible API
Plugin SystemRollup hooks + Vite-specific hooks
Env VariablesVITE_ prefix → client-side, no prefix → server-only

Bài tập: Tạo React + Vite project, configure proxy đến .NET API, setup Vitest, và viết custom plugin log build time.