Tất cả bài viết
ViteHMRlocal developmentlocalhost tunneling

Vite + tunnel: HMR thật sự hoạt động

Tunnel một dev server Vite vốn chạy tốt — trừ hai thứ sẽ làm bạn vấp ở lần đầu. Trang tải được nhưng HMR thì không. Hoặc trang không tải gì cả vì Vite từ chối hostname tunnel của bạn. Cả hai đều là config một dòng khi bạn biết phải nhìn đâu.

Vấn đề 1: "Blocked request. This host is not allowed."

Vite 5+ đã thêm bảo vệ host-check theo mặc định. Nếu bạn mở URL tunnel và thấy Blocked request. This host is not allowed, đó là phần kiểm tra này từ chối hostname *.portpreview.dev của bạn.

Sửa trong vite.config.ts:

export default defineConfig({
  server: {
    host: true,                            // listen on 0.0.0.0
    allowedHosts: ['.portpreview.dev'],   // accept any subdomain
  },
});

Dấu chấm đầu biến nó thành khớp theo hậu tố. Bạn có thể đặt hostname tunnel cụ thể ở đó nếu muốn nghiêm ngặt, nhưng cho phát triển hằng ngày thì hậu tố tiện hơn — URL phiên tunnel xoay vòng.

Vấn đề 2: HMR tải xong rồi âm thầm chết

HMR chạy trên WebSocket. Khi Vite phục vụ tại localhost:5173 nhưng trình duyệt tải từ https://abc123.portpreview.dev, client HMR cố kết nối tới URL mà server bảo nó — chính là cái cục bộ. Tunnel không chuyển tiếp cái đó, nên WebSocket thất bại và HMR âm thầm dừng.

Sửa config HMR:

export default defineConfig({
  server: {
    host: true,
    allowedHosts: ['.portpreview.dev'],
    hmr: {
      clientPort: 443,            // browser connects on 443 (HTTPS)
      protocol: 'wss',            // secure WebSocket through the tunnel
    },
  },
});

Giờ client HMR kết nối tới wss://abc123.portpreview.dev (cổng 443), tunnel chuyển tiếp upgrade, và các chỉnh sửa lan đi như mong đợi. Nếu bạn chỉ cần reload tĩnh và không quan tâm HMR qua tunnel, có thể bỏ qua — nhưng bạn sẽ bấm cmd-R rất nhiều.

Vì sao muốn tunnel Vite ngay từ đầu

Ba lý do thực tế:

  • Kiểm thử di động. Mở URL trên điện thoại, thấy cùng một build với HMR. Tốt hơn chia sẻ màn hình hay emulator theo từng thiết bị. Xem kiểm thử di động với tunnel để biết mẫu đầy đủ.
  • Chia sẻ công việc đang làm. Gửi link cho designer hoặc PM, họ thấy nhánh của bạn trực tiếp. Không có bước deploy.
  • Luồng OAuth và webhook cần HTTPS. Frontend do Vite phục vụ nói chuyện với Stripe (chỉ redirect), Auth0 hoặc tương tự. mkcert vs tunnel bàn về lựa chọn này.

SvelteKit, Astro, SolidStart đều dùng Vite

Các config trên áp dụng cho mọi framework dùng Vite làm dev server. vite.config.js của SvelteKit có cùng hình dạng. Astro có astro.config.mjs riêng với một sub-object vite — đặt cùng các tùy chọn server vào đó. SolidStart, Nuxt 3 (qua Vite) và Qwik City: cùng mẫu.

Build production là chuyện khác

Tất cả ở trên là config của dev server. Khi bạn chạy vite preview hoặc phục vụ một bundle đã build, không cái nào quan trọng vì không có HMR và không có middleware host-check. Lúc đó tunnel chỉ đang chuyển tiếp các file tĩnh.

Một vài điều nhỏ

  • CORS host nghiêm ngặt. Nếu frontend gọi một API ở origin khác (cổng localhost khác, hoặc một tunnel API riêng), hãy đặt CORS ở phía API. Vite không proxy theo mặc định trừ khi bạn bảo nó qua server.proxy.
  • App dùng nhiều WebSocket. HMR của Vite là một WebSocket. Nếu app của bạn dùng một WebSocket khác cho trạng thái game, chat hay dữ liệu trực tiếp, cái đó riêng biệt và cũng cần cấu hình trong code client để dùng URL tunnel.
  • URL tunnel trong file env. Chúng tôi đặt URL tunnel đang hoạt động vào .env.local dưới dạng VITE_PUBLIC_URL khi frontend cần biết địa chỉ công khai của chính nó. import.meta.env.VITE_PUBLIC_URL đọc nó ở client.

Từng bước

  1. Thêm khối server.host, allowedHostshmr vào config Vite.
  2. Khởi động lại dev server.
  3. Chạy npx portpreview 5173 (hoặc cổng mà Vite dùng).
  4. Mở URL HTTPS trên trình duyệt hoặc điện thoại.
  5. Chỉnh một component. Xác nhận HMR cập nhật trang mà không reload toàn bộ.

Nếu HMR không cập nhật, mở console trình duyệt — Vite log lại nỗ lực kết nối WebSocket và bạn sẽ thấy URL nó đã thử. Điều đó cho biết thay đổi hmr.clientPort/protocol đã có hiệu lực hay chưa.

Tham gia danh sách chờ PortPreview để có tunnel giữ nguyên các nâng cấp WebSocket theo mặc định.

Câu hỏi thường gặp

Vì sao Vite chặn URL tunnel của tôi với "host is not allowed"?
Vite 5+ đã thêm một host-check từ chối hostname lạ theo mặc định. Thêm allowedHosts: ['.portpreview.dev'] (hoặc domain tunnel của bạn) vào config server để chấp nhận request từ tunnel.
Làm sao để HMR của Vite hoạt động qua tunnel?
Đặt hmr.clientPort: 443 và hmr.protocol: 'wss' trong vite.config.ts. Điều này bảo client HMR kết nối qua HTTPS/WSS của tunnel thay vì cố chạm tới localhost trực tiếp, vốn thất bại từ bên ngoài máy bạn.
Cái này có dùng được cho SvelteKit, Astro và Nuxt không?
Có. Mọi framework dùng Vite làm dev server đều nhận cùng config server.host, allowedHosts và hmr. Astro lồng nó dưới vite trong astro.config.mjs; các framework khác đặt vào file config Vite thông thường.