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
| Concept | Key Takeaway |
|---|---|
| ESBuild (dev) | Pre-bundle deps, transform on-demand → startup <500ms |
| Rollup (build) | Tree-shaking, code-splitting, minification |
| HMR | Module-level hot replace, <50ms update |
| Proxy | Dev server proxy → .NET backend, WebSocket support |
| Vitest | Native Vite integration, jest-compatible API |
| Plugin System | Rollup hooks + Vite-specific hooks |
| Env Variables | VITE_ 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.