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.localdưới dạngVITE_PUBLIC_URLkhi 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
- Thêm khối
server.host,allowedHostsvàhmrvào config Vite. - Khởi động lại dev server.
- Chạy
npx portpreview 5173(hoặc cổng mà Vite dùng). - Mở URL HTTPS trên trình duyệt hoặc điện thoại.
- 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.