两个工具,一个问题:你的开发服务器需要通过 HTTPS 可达。mkcert 给你一个本地受信的 TLS 证书,让 https://localhost:3000 直接能用。隧道给你一个由真实 CA 颁发的真实证书支撑的公网 HTTPS URL。它们不是竞争对手——解决不同的问题——但人们总把它们当成竞争对手。
简短答案
- 只需要在自己机器、自己浏览器里用 HTTPS?用 mkcert。
- 需要外部服务(Stripe、GitHub、Auth0、一部手机)能到达你的开发服务器?用隧道。
- 都要?都跑。它们不冲突。
三个要点就是整篇文章。其余只是为什么。
mkcert:30 秒内本地受信证书
mkcert 把一个本地证书颁发机构装进你操作系统的信任库,并从中签发证书。浏览器视该证书为受信,因为 CA 受信。没有你的连接不是私密连接的警告,没有 --ignore-certificate-errors 标志,不用自己折腾自签证书。
# 每台机器一次
mkcert -install
# 每个项目一次
mkcert localhost 127.0.0.1
# 现在你有了 localhost.pem 和 localhost-key.pem
把它们接进你的开发服务器(Vite、Next.js、Express、Django runserver_plus 都支持 TLS 参数),你就有了带真实证书的 https://localhost:3000。
但有个坑:只有你的机器信任那个 CA。你队友的浏览器会警告。你的手机不会信任,除非手动安装 CA 根证书。
隧道:真实 HTTPS,真实公网 URL
localhost 隧道把流量从一个公网 HTTPS URL 转发到你的本地端口。证书由真实 CA(Let's Encrypt 等)签发,每台设备上的每个浏览器都信任。
npx portpreview 3000
隧道在云端网关处理 TLS 终止。你的本地服务器可以保持纯 HTTP——公网 URL 是 HTTPS,那才是每个外部服务接触到的面。
并排对比
| 需求 | mkcert | 隧道 |
|---|---|---|
| 本地浏览器里的 HTTPS | 是 | 是(通过公网 URL) |
| 浏览器里的 service worker / 安全 cookie 测试 | 是 | 是 |
| 外部提供商能到达你 | 否 | 是 |
| 手里的手机能到达你 | 否(除非装 CA) | 是 |
| OAuth 提供商接受该 URL | 有时(Google:是;很多:否) | 是 |
| Stripe / GitHub / Twilio 能向它发 webhook | 否 | 是 |
| 配置时间 | 每台机器 30 秒 | 每会话一条命令 |
| 离线可用 | 是 | 否 |
| 飞行模式下可用 | 是 | 否 |
人们的常见误区
试图用 mkcert 做 webhook 测试
Stripe 无法信任你机器的本地 CA。无论证书在你浏览器里看起来多受信——Stripe 在另一个网络。任何来自公网的入站流量都需要隧道。
为单人纯浏览器的 HTTPS 用隧道
如果你只想让 service worker 工作,或在自己浏览器里设置一个安全 cookie,mkcert 更快且离线可用。别为一个证书文件就能解决的事烧掉一个隧道会话。
选定一个工具并硬把另一个用例塞进去
两个都跑没问题。我们大部分设置用 mkcert 做日常浏览器端工作,测试 webhook 或 OAuth 回调时用隧道。它们在 package.json 里无冲突共存。
那 Caddy 或 nginx 呢?
是的,你可以在开发服务器前跑带自动 HTTPS 的 Caddy,这也行——本质上是“多几步加一个反向代理的 mkcert”。对大多数本地开发,mkcert 更简单。对涉及多个本地服务的更复杂路由,Caddy 物有所值。
我们的真实设置
我们工作的一个仓库在 dev 脚本里有 mkcert 用于 localhost 端 HTTPS,还有一个单独的 tunnel npm 脚本,当有人需要 webhook 或移动端测试时运行 portpreview。隧道 URL 通过环境变量传入,所以 OAuth 重定向 URI 很容易替换。接好花了 20 分钟,此后我们再没想过本地 TLS。
关于 OAuth 专用的隧道设置,见如何在本地测试 OAuth 回调。关于与队友或设计师分享预览,分享你的本地开发服务器。加入 PortPreview 等候名单,获得这套设置的隧道一侧。