vercel-labs/portless
摘要
Portless 是一款开发者工具,它用稳定、带名称的 .localhost 域名取代端口号,用于本地开发环境。它提供 HTTPS 支持与 HTTP/2 协议,并为 Next.js 和 Vite 等框架进行自动配置。
查看缓存全文
缓存时间: 2026/09/02 17:44
vercel-labs/portless
来源:https://github.com/vercel-labs/portless
portless
将端口号替换为稳定的、命名式的 .localhost URL,用于本地开发。既适用于人类开发者,也适用于自动化工具。
- "dev": "next dev" # http://localhost:3000
+ "dev": "portless run next dev" # https://myapp.localhost
安装
全局安装(推荐):
npm install -g portless
或作为项目开发依赖安装:
npm install -D portless
portless 目前版本低于 1.0。当按项目安装时,不同的协作者可能使用不同版本。状态目录的格式可能在版本间发生变更,这可能需要重新运行
portless trust。
运行你的应用
portless myapp next dev # -> https://myapp.localhost
默认启用带 HTTP/2 的 HTTPS。首次运行时,portless 会生成一个本地 CA,将其添加到系统信任库,并绑定 443 端口(在 macOS/Linux 上使用 sudo 自动提权)。
使用 --no-tls 选项可运行纯 HTTP。
当你运行应用时,代理会自动启动。一个随机端口(4000-4999)会通过 PORT 环境变量分配。大多数框架(Next.js、Express、Nuxt 等)会自动识别该变量。
对于忽略 PORT 的框架(Vite、VitePlus、Astro、React Router、Angular、Expo、React Native),portless 会自动注入正确的 --port 标志,必要时还会注入匹配的 --host 标志。注入会通过一个以框架名称或已知运行器开头的包脚本命令生效(例如 "dev": "vite"、"dev": "bunx vite")。只有框架的服务器命令(dev、serve、preview、start、单独的 vite 或 vite [root])会接收这些标志;不提供服务的命令,如 vite build、vite optimize、vp test 或 astro check,则会被忽略并保持不变。
Expo 连接模式(--localhost、--lan、--tunnel)会被保留,同时仍会注入分配的端口。
portless 无法分类的脚本也会被忽略:例如在其不跟踪标志语法的 CLI 子命令前的标志(vp --mode dev build)。当向脚本追加标志可能无效时,portless 也会保持脚本不变:复合命令(&&、|、;)、末尾注释(#)、其自身的 -- 选项终止符、环境变量前缀(NODE_ENV=production vite)、委派给另一个脚本("dev": "npm run dev:vite")或脚本名前的运行器标志(bun run --bun dev)。这些脚本使用自己的端口,因此你需要在脚本中自行设置端口。
自动启动时,portless 会复用最近一次代理运行的配置(端口、TLS、TLD),因此重启或系统重启不会静默地恢复为默认设置。显式环境变量(PORTLESS_PORT、PORTLESS_HTTPS 等)始终具有最高优先级。
portless 将每用户状态存储在 ~/.portless。当代理在 sudo 下运行时,它会从调用用户的主目录解析此路径,以便代理和非特权应用进程共享相同的路由注册。
在非交互式环境(无 TTY 或 CI=1)下,portless 会退出并显示描述性错误信息,而不是进行提示,因此像 turborepo 这样的任务运行器和 CI 脚本会提前失败并给出清晰的消息。
配置
直接运行 portless 即可开箱即用。它会通过代理运行 package.json 中的 "dev" 脚本,并根据包名、git 根目录或当前目录推断应用名称:
portless # -> 运行 "dev" 脚本, https://<name>.localhost
使用可选的 portless.json 文件覆盖默认设置:
{ "name": "myapp" }
portless # -> 运行 "dev" 脚本, https://myapp.localhost
脚本默认为 "dev"。如果未在配置中设置名称,则从 package.json 推断。
Monorepo(单仓多包)
位于仓库根目录的一个 portless.json 文件可覆盖所有工作区包。Portless 从 pnpm-workspace.yaml 或 package.json(npm、yarn、 bun)的 "workspaces" 字段中发现包:
{
"apps": {
"apps/web": { "name": "myapp" },
"apps/api": { "name": "api.myapp" }
}
}
portless # 从仓库根目录:启动所有具有 "dev" 脚本的工作区包
cd apps/web && portless # 仅启动一个包
apps 映射是可选的,仅在需要覆盖名称时使用。未列出的包仍会通过其 package.json 推断名称进行自动发现。如果没有 apps 映射,主机名遵循 .<project>.localhost 的约定。项目名称来自工作区包中最常见的 npm 作用域(例如 @myorg/web 和 @myorg/api 会产生 myorg),回退到工作区根目录名称。如果一个包的简称与项目名称匹配,它将获得不带重复的 .localhost。
配置字段
| 字段 | 类型 | 默认值 | 描述 |
|---|---|---|---|
name | string | 推断 | 基础应用名称。工作树前缀仍会应用。 |
script | string | "dev" | 要运行的 package.json 脚本名称。 |
appPort | number | 自动 | 子进程使用的固定端口。 |
proxy | boolean | 自动 | 是否通过代理路由。自动检测。 |
apps | object | 覆盖工作区包的设置,按相对路径为键。 | |
turbo | boolean | true | 设置为 false 以使用直接生成进程而非 turborepo。 |
package.json 中的 “portless” 键
你可以在 package.json 中添加一个 "portless" 键,而不是单独的 portless.json 文件。字符串值是设置名称的简写:
{ "name": "@myorg/web", "portless": "myapp" }
对象值支持所有每应用字段(name、script、appPort、proxy):
{
"name": "@myorg/web",
"portless": {
"name": "myapp",
"script": "dev:app"
}
}
package.json 中的 "portless" 键优先于 portless.json 的应用条目,但会被 CLI 标志覆盖。
–script 标志
在单次调用中覆盖默认脚本:
portless --script start # 运行 "start" 而非 "dev"
portless --script test # 运行 "test" 而非 "dev"
Turborepo
要将 portless 与 turborepo 一起使用,请将 portless 作为 dev 脚本,并将实际命令放在单独的脚本中:
{
"scripts": {
"dev": "portless",
"dev:app": "next dev"
},
"portless": {
"name": "myapp",
"script": "dev:app"
}
}
Turbo 运行每个包的 dev 脚本,该脚本调用 portless。Portless 读取配置,检测包管理器,并通过代理运行 pnpm run dev:app(或 yarn/bun/npm)。无需更改 turbo.json。根目录下的 pnpm dev 会像往常一样通过 turbo 运行。没有安装 portless 的人可以直接运行 pnpm run dev:app。
在 package.json 中使用
你仍然可以在 package.json 脚本中使用 portless:
{
"scripts": {
"dev": "portless run next dev"
}
}
有了 portless.json,你可以简化为:
{
"scripts": {
"dev": "next dev"
}
}
然后运行 portless 或 portless run 通过代理。
当你按下 Ctrl+C 时,portless 会转发中断信号并等待命令的进程树退出。再次按下 Ctrl+C 会转发另一个中断信号。任何剩余的后代进程在短暂宽限期后将被终止。
子域名
使用子域名组织服务:
portless api.myapp pnpm start # -> https://api.myapp.localhost
portless docs.myapp next dev # -> https://docs.myapp.localhost
默认情况下,只有明确注册的子域名会被路由(严格模式)。在启动代理时使用 --wildcard 允许任何已注册路由的子域名回退到该应用(例如 tenant1.myapp.localhost 路由到 myapp 应用,无需额外注册)。
Git 工作树(Worktrees)
portless run 会自动检测 git 工作树。在关联的工作树中,分支名会作为子域名前缀添加,因此每个工作树都会获得自己的 URL,无需任何配置更改:
# 主工作树(无前缀)
portless run next dev # -> https://myapp.localhost
# 在 "fix-ui" 分支上的关联工作树
portless run next dev # -> https://fix-ui.myapp.localhost
使用 --name 在保留工作树前缀的同时覆盖推断的基础名称:
portless run --name myapp next dev # -> https://fix-ui.myapp.localhost
只需在 package.json 中放置 portless run,它就无处不在。主检出使用普通名称,每个工作树获得一个唯一的子域名。没有冲突,无需 --force。
自定义顶级域名(TLD)
默认情况下,portless 使用 .localhost,这在大多数浏览器中会自动解析为 127.0.0.1。如果你喜欢不同的 TLD(例如 .test),请使用 --tld:
portless proxy start --tld test
portless myapp next dev # -> https://myapp.test
代理会自动同步 /etc/hosts 文件以包含路由主机名(包括 .test),因此这些域名可以在你的机器上解析。
重复使用 --tld 可从一个代理在多个 TLD 下提供相同的应用名称:
portless proxy start --tld localhost --tld test
portless myapp next dev # -> https://myapp.localhost
# -> https://myapp.test
当配置了多个 TLD 时,PORTLESS_URL 使用第一个 TLD。PORTLESS_TLD 也接受逗号分隔的列表,例如 PORTLESS_TLD=localhost,test。
推荐:.test(IANA 保留,无冲突风险)。避免使用 .local(与 mDNS/Bonjour 冲突)和 .dev(Google 拥有,通过 HSTS 强制使用 HTTPS)。
多段 TLD
--tld 值接受小写的 DNS 名称(一个或多个点分隔的标签,无末尾点),因此你可以使用你拥有的域名作为“TLD”。这使得本地 URL 与生产环境具有相同的结构,从而使 OAuth 重定向 URI、跨子域名 Cookie 和基于主机的路由在两种环境中以相同方式工作:
portless proxy start --tld dev.example.com
portless myapp next dev # -> https://myapp.dev.example.com
每个标签必须遵循 DNS 规则:小写字母、数字和内部连字符,每个标签最多 63 个字符,总计最多 253 个字符。完整主机名(app.TLD)也受 253 个字符的 DNS 限制。
代理会自动同步 /etc/hosts 文件以包含已注册的主机名,因此 myapp.dev.example.com 会在你的机器上解析为 127.0.0.1。
这是一个仅限回环地址的设置:在非局域网模式下,代理仅绑定到 127.0.0.1 和 ::1(见下文),因此自定义 TLD 只能从运行代理的机器访问。从其他设备访问代理需要局域网模式(--lan),但局域网模式在 .local TLD 下提供服务,并且会忽略自定义的 --tld,因此目前无法将两者结合使用。
严格的 OAuth 提供商(Google、Apple)会拒绝 .localhost 和 .test 重定向 URI,但会接受真实域名,因此 https://myapp.dev.example.com/api/auth/callback/google 可以用作重定向 URI。
工作原理
flowchart TD
Browser["浏览器 -> myapp.localhost"] --> Proxy["portless proxy (端口 80 或 443)"]
Proxy --> App1[":4123 myapp"]
Proxy --> App2[":4567 api"]
- 启动代理:在你运行应用时自动启动,或使用
portless proxy start显式启动 - 运行应用:
portless <name> <script>分配一个空闲端口并向代理注册 - 通过 URL 访问:
https://<name>.localhost通过代理路由到你的应用
在非局域网模式下,代理及其 HTTP 重定向监听器仅绑定到 IPv4 和 IPv6 回环地址,即 127.0.0.1 和 ::1。它们不接受通过 LAN、VPN 或其他网络接口的连接。
HTTP/2 + HTTPS
默认启用带 HTTP/2 的 HTTPS。浏览器将 HTTP/1.1 限制为每个主机 6 个连接,这会对提供许多未打包文件(Vite、Nuxt 等)的开发服务器造成瓶颈。HTTP/2 在单个连接上复用所有请求。
WebSockets 在两个协议版本上都能工作,因此开发服务器的 HMR(Next.js、Vite 等)可以通过代理工作:HTTP/1.1 Upgrade 请求会被原样转发,而在 HTTP/2 连接上打开的 WebSockets 使用扩展的 CONNECT(RFC 8441)。
首次运行时,portless 会生成一个本地 CA 并将其添加到你的系统信任存储中。没有浏览器警告。无需手动设置。
# 使用你自己的证书(例如从 mkcert)
portless proxy start --cert ./cert.pem --key ./key.pem
# 禁用 HTTPS(端口 80 上的纯 HTTP)
portless proxy start --no-tls
# 如果你在首次运行时跳过了信任提示,稍后信任 CA
portless trust
在 Linux 上,portless trust 支持 Debian/Ubuntu、Arch、Fedora/RHEL/CentOS 和 openSUSE(通过 update-ca-certificates 或 update-ca-trust)。在 Windows 上,它使用 certutil 将 CA 添加到系统信任存储。在 WSL 上,它会更新 Linux 信任存储和 Windows 当前用户根存储,以便 Windows 浏览器信任 portless HTTPS 证书。
在操作系统启动时启动
将代理安装为操作系统启动服务,以便重启后即可使用干净的 HTTPS URL,无需从终端启动代理:
portless service install
portless service install --lan
portless service install --wildcard
PORTLESS_STATE_DIR=~/.portless-lan PORTLESS_LAN=1 portless service install
portless service status
portless service uninstall
除非提供了安装选项或 PORTLESS_* 环境变量,否则服务使用 portless 默认设置:端口 443 上的 HTTPS,名称为 .localhost。service install 接受你在 proxy start 中会用到的代理选项,包括 --port、--no-tls、--lan、--ip、--tld、--wildcard、--cert 和 --key。
使用 --state-dir <path> 或 PORTLESS_STATE_DIR=<path> 选择服务状态和日志的写入位置。所选的服务配置会被写入 launchd、systemd 或任务计划程序,并在重启后复用。
portless service status 报告已安装的端口、HTTPS 模式、TLD、局域网模式、通配符模式和状态目录。
macOS 和 Linux 安装一个由 root 拥有的服务,以便在启动时绑定端口 443。Windows 安装一个以 SYSTEM 身份运行的任务计划程序启动任务。安装和卸载可能需要管理员权限。portless clean 会自动移除该服务。
局域网模式
portless proxy start --lan
portless proxy start --lan --https
portless proxy start --lan --ip 192.168.1.42
--lan 显式地将代理绑定到 IPv4 和 IPv6 的未指定地址,即 0.0.0.0 和 ::,并切换到 mDNS 发现。这使得服务在 .local 上对同一网络中的设备可用。
Portless 会自动检测你的局域网 IP 并自动跟随 Wi-Fi/IP 变化,但你可以使用 --ip <ip> 或导出 PORTLESS_LAN_IP 来固定另一个地址。
在你的 shell 中设置 PORTLESS_LAN=1(0/1 布尔值),使局域网模式成为代理启动时的默认模式。Portless 通过 proxy.lan 记住局域网模式,因此如果你停止一个局域网代理并再次启动它,它将保持在局域网模式。除非被显式标志或环境变量覆盖,所有代理设置(端口、TLS、TLD、局域网)都会被持久化并在自动启动时复用。使用 PORTLESS_LAN=0 进行一次启动以切换回 .localhost 模式。
如果代理已经以不同的显式局域网/TLS/TLD 设置运行,portless 会发出警告并要求你先停止它。
局域网模式依赖于 portless 已经生成的系统 mDNS 工具:macOS 自带 dns-sd,而 Linux 使用 avahi-utils 中的 avahi-publish-address(通过 sudo apt install avahi-utils 或你的发行版等效方式安装)。如果命令缺失或你的网络不可达,portless proxy start --lan 会打印相关错误并退出。
框架注意事项
-
Next.js:将你的
.local主机名添加到allowedDevOrigins:// next.config.js module.exports = { allowedDevOrigins: ["myapp.local", "*.myapp.local"], }; -
Expo / React Native:portless 始终注入
--port。React Native 还会获得--host 127.0.0.1。在非局域网模式下,Expo 获得--host localhost,但在局域网模式下,portless 会让 Metro 使用其默认的局域网主机行为,而不是强制--host或HOST。
Tailscale 共享
在你的 Tailscale (https://tailscale.com) 网络上与团队成员共享你的开发服务器:
portless myapp --tailscale next dev # -> https://myapp.localhost (本地)
# -> https://devbox.yourteam.ts.net (tailnet)
每个 --tailscale 应用都根挂载到自己的 Tailscale HTTPS 端口上,因此不需要框架的 basePath 配置。第一个应用获得端口 443,后续应用获得 8443、8444 等。
portless myapp --tailscale next dev # -> https://devbox.ts.net
portless api --tailscale pnpm start # -> https://devbox.ts.net:8443
使用 --funnel 将你的开发服务器暴露到公共互联网。
相似文章
vercel/next.js
Next.js 是由 Vercel 构建的开源 Web 框架,扩展了 React,用于快速构建全栈应用。
Show HN: Port Zero – 如何学会不再担心并爱上 PORT=0
Port Zero 是一款免费、开源的工具,它为开发服务器分配稳定的本地域名而非端口号,从而消除端口冲突,并支持轻松的多分支开发和云隧道功能。
@rammcodes: 天哪... 你现在可以仅用 VS Code 将 localhost 分享给任何人!VS Code 内置了端口功能,…
VS Code 内置了端口功能,允许用户无需第三方工具即可将 localhost 暴露到互联网,方便进行演示和测试的分享。
Routeup – 稳定的本地 HTTPS URL 和可选公共隧道
Routeup 为本地开发应用提供稳定且受浏览器信任的 HTTPS 名称,支持可选公共隧道,无需端口转发或路由器配置。
@interjc: 这来自 CF 的工具 https://try.cloudflare.com 很令人印象深刻,localhost:3000 也可以分享给网民查看。
Cloudflare Quick Tunnels 允许开发者通过单个命令将 localhost 暴露到互联网,提供安全、加密的 URL,无需账户或 DNS 配置。