Khám Phá Bí Mật Đằng Sau Lệnh `npm run dev`: Điều Gì Thực Sự Xảy Ra?

Khi bạn gõ `npm run dev` và nhấn Enter, có vẻ như một phép màu đã xảy ra. Màn hình terminal nhấp nháy, bạn chuyển sang trình duyệt, và ứng dụng web của bạn đột nhiên xuất hiện, sẵn sàng cho bạn tương tác. Nhưng đã bao giờ bạn tự hỏi, điều gì thực sự đang diễn ra dưới lớp vỏ đó? Quy trình phức tạp nào đang biến những dòng mã nguồn của bạn thành một trải nghiệm trình duyệt sống động?

Bài viết này sẽ đưa bạn vào một hành trình sâu sắc, khám phá từng bước trong quy trình vận hành của lệnh `npm run dev`, tập trung vào các công cụ hiện đại như Vite và cơ chế Hot Module Replacement (HMR) cùng React Fast Refresh. Chúng ta sẽ cùng nhau “lặn sâu” để hiểu rõ hơn về shell, cách các tiến trình con được khởi tạo, đồ thị module, và cách mọi thứ kết hợp lại để mang đến trải nghiệm phát triển mượt mà và nhanh chóng mà bạn yêu thích.

Bước 0: Khởi Đầu Bằng Một Cú Nhấn Enter

Mọi thứ bắt đầu khi bạn gõ `npm run dev` vào terminal và nhấn Enter.

npm run dev

Ngay lập tức, shell của bạn (ví dụ: Bash, Zsh) bắt đầu tìm kiếm lệnh `npm` trong biến môi trường `PATH`. Nó sẽ tìm thấy `npm` – thường nằm trong thư mục cài đặt Node.js của `nvm` (Node Version Manager) hoặc một đường dẫn hệ thống như `/usr/local/bin/npm`. Sau khi xác định được vị trí, shell sẽ chuyển quyền điều khiển cho công cụ dòng lệnh `npm` (npm CLI), bản thân nó cũng chỉ là một script Node.js thông thường.

Công việc đầu tiên của `npm` là đọc file `package.json` trong thư mục dự án của bạn:

{
  "name": "my-project",
  "version": "1.0.0",
  "scripts": {
    "dev": "vite"
  },
  "dependencies": {
    "react": "^18.2.0",
    "vite": "^5.0.0"
  }
}

Từ trường `”scripts”`, `npm` sẽ tìm thấy lệnh được gán cho khóa `”dev”`, trong trường hợp này là `”vite”`, và chuẩn bị để thực thi nó.

Bước 1: `npm` Khởi Tạo Một Tiến Trình Con

Trước khi chạy lệnh `”vite”`, `npm` thực hiện một thủ thuật nhỏ: nó tạm thời thêm đường dẫn `./node_modules/.bin` vào biến môi trường `PATH` của tiến trình con sẽ được tạo. Điều này cực kỳ quan trọng vì nó cho phép bạn chạy các gói CLI được cài đặt cục bộ trong dự án mà không cần chỉ định đường dẫn đầy đủ.

Khi `npm` tìm thấy `”dev”: “vite”`, nó sẽ khởi tạo một tiến trình con để chạy lệnh `vite`. Nhờ việc sửa đổi `PATH` tạm thời, lệnh `”vite”` được phân giải thành `./node_modules/.bin/vite`. File này thường là một symlink (liên kết tượng trưng) trỏ đến file thực thi Vite JavaScript, nằm sâu bên trong thư mục `node_modules/vite/bin/vite.js`.

Điều này có nghĩa là, không hề có “phép màu” nào cả. Đơn giản là Node.js đang chạy một file JavaScript khác.

Bước 2: Vite Khởi Động và Tối Ưu Hóa Nhanh Chóng

Với `vite.js` được thực thi, Vite bắt đầu quá trình khởi động. Nó đọc file cấu hình của bạn (thường là `vite.config.js` hoặc `vite.config.ts`), đăng ký các plugin cần thiết, và thực hiện một bước xử lý thông minh trước khi bất kỳ file nào được phục vụ cho trình duyệt.

Thủ Thuật Pre-bundling với esbuild

Một trong những yếu tố then chốt giúp Vite cực kỳ nhanh là việc sử dụng `esbuild` – một trình đóng gói (bundler) được viết bằng Go, nổi tiếng với tốc độ đáng kinh ngạc. Vite sử dụng `esbuild` để “pre-bundle” (đóng gói trước) các dependency trong thư mục `node_modules` của bạn. Quá trình này thực hiện hai công việc chính:

1. **Chuyển đổi CommonJS sang ESM (ES Modules)**: Các trình duyệt hiện đại không hỗ trợ native cho cú pháp `require()` của CommonJS. `esbuild` sẽ viết lại các module CommonJS này thành định dạng ESM mà trình duyệt có thể hiểu được.
2. **Gộp các gói có nhiều file thành một**: Ví dụ, một thư viện như `lodash` có thể chứa hàng trăm file nhỏ. `esbuild` sẽ gộp chúng lại thành một file duy nhất. Điều này giúp trình duyệt chỉ cần thực hiện một yêu cầu HTTP duy nhất thay vì hàng trăm yêu cầu, giảm đáng kể thời gian tải.

Kết quả của quá trình pre-bundling này được lưu trữ trong thư mục `.vite/deps/`. Lần tiếp theo bạn chạy `npm run dev`, Vite sẽ kiểm tra xem có thay đổi nào trong `node_modules` hay không. Nếu không, nó sẽ bỏ qua hoàn toàn bước này, giúp khởi động lại siêu nhanh. Nếu có, cache sẽ bị xóa và quá trình pre-bundling sẽ được chạy lại.

Bước 3: Máy Chủ Phát Triển Chỉ Là Một Máy Chủ HTTP Thông Thường

Khác với các công cụ đóng gói truyền thống như Webpack Dev Server với chuỗi middleware phức tạp, Vite khởi động một máy chủ HTTP Node.js “thuần túy” trên `localhost:5173` (hoặc cổng khác nếu được cấu hình).

Khi trình duyệt yêu cầu một file (ví dụ: `App.jsx`, `main.css`), Vite thực hiện các bước sau:

1. Đọc file từ đĩa cứng.
2. Chuyển đổi file “on-the-fly” (ngay lập tức):
* JSX sang JavaScript.
* TypeScript sang JavaScript.
* File `.css` được biến đổi thành thẻ `