什么是ngrok

ngrok 是一款内网穿透工具

特性/场景 描述
核心功能 内网穿透,将本地服务暴露到公网
主要优势 简单易用、无需公网IP、无需配置路由器、提供HTTPS、自带请求监控工具
典型用户 软件开发人员、测试工程师、DevOps工程师
核心使用场景 1. 本地开发调试
2. Webhook测试(极其重要)
3. 演示预览
4. 移动端测试
5. 临时访问内网服务

注册帐号获取Token

打开ngrok控制台进行帐号注册,注册完毕之后会进入控制台,切换到Authtoken页面即可查看分配的token。
image.png
免费版的账户可以使用

  • 1个随机域名
  • 20000次https请求,每分钟4000次
  • 1GB网络传输流量
    启动隧道之后可以在控制台查看接入的端点
    image.png

ngrok 支持多种方式的使用

命令行工具

可以通过多种方式来进行全局的命令行工具安装

$ brew install ngrok
$ npm install ngrok -g

安装之后设置token,就是注册时获取的authtoken

$ ngrok config add-authtoken $YOUR_AUTHTOKEN

然后就可以启动内网穿透了,比如我本地启动了5173端口

$ ngrok http http://localhost:5173

启动之后可以看到已经分配了一个地址并重定向到了本地5173端口
image.png

访问这个链接之后可以看到跟本地访问的效果一致
image.png

SDK 调用

ngrok也支持 SDK 的方式调用,将隧道的连接放进业务中控制。以NodeJS SDK 为例,我们可以使用以下API来进行处理

验证

await ngrok.authtoken(token);

将控制台中生成的token传入authtoken方法用于验证身份,以便后续连接。

连接

const url = await ngrok.connect();
const url = await ngrok.connect(port);

创建隧道连接,可以指定端口,不指定时默认80端口进行转发。
也可以传入一个配置来精细控制连接的方式:

  • proto: ‘http’, // http|tcp|tls, 默认 http
  • addr: 8080, // 转发的端口, 默认 80
  • basic_auth: ‘user:pwd’, // 隧道鉴权方式
  • subdomain: ‘son’, // 子域名
  • authtoken: ‘12345’, // token
  • region: ‘us’, // 连接服务器地域 (us, eu, au, ap, sa, jp, in), 默认 us
  • configPath: ‘~/git/project/ngrok.yml’, // 配置文件目录
  • binPath: path => path.replace(‘app.asar’, ‘app.asar.unpacked’), // 自定义bin目录
  • onStatusChange: status => {}, // 连接状态变化回调
  • onLogEvent: data => {}, // ngrok标准输出

断开连接

await ngrok.disconnect(url); // 停止指定地址
await ngrok.disconnect(); // 停止所有
await ngrok.kill(); // 杀掉ngrok进程

使用以上API在退出程序时断开连接,释放进程。

例如我们可以通过NodeJS SDK来实现一个Vite插件,在Vite DevServer启动时建立隧道连接。

import type { Plugin, ViteDevServer } from 'vite'
import { connect, authtoken, disconnect } from 'ngrok'

interface NgrokPluginOptions {
    // ngrok 认证令牌,可以从环境变量 NGROK_AUTHTOKEN 中获取
    authToken?: string
    // 是否启用 ngrok,默认为 true
    enabled?: boolean
    // ngrok 配置选项
    ngrokOptions?: {
        // 自定义子域名
        subdomain?: string
        // 区域设置 (us, eu, ap, au, sa, jp, in)
        region?: 'us' | 'eu' | 'ap' | 'au' | 'sa' | 'jp' | 'in'
        // 基本认证
        auth?: string
        // 主机头重写
        host_header?: string
        // 绑定tls
        bind_tls?: boolean | string
        // 检查证书
        inspect?: boolean
    }
}

let ngrokUrl: string | null = null
export default function vitePluginNgrok(options: NgrokPluginOptions = {}): Plugin {
    const {
        authToken = process.env.NGROK_AUTHTOKEN,
        enabled = true,
        ngrokOptions = {}
    } = options
    
    return {
        name: 'vite-plugin-ngrok',
        apply: 'serve', // 只在开发模式下应用
        configureServer(server: ViteDevServer) {
            if (!enabled) {
                return
            }
            
            // 在服务器启动后设置 ngrok
            server.middlewares.use('/__ngrok_status', (_req, res) => {
                res.setHeader('Content-Type', 'application/json')
                res.end(JSON.stringify({
                    url: ngrokUrl,
                    status: ngrokUrl ? 'connected' : 'disconnected'
                }))
            })
            
            // 监听服务器启动事件
            server.httpServer?.on('listening', async () => {
            await setupNgrok(server, authToken, ngrokOptions)
            })
            
            // 服务器关闭时断开 ngrok
            server.httpServer?.on('close', async () => {
                if (ngrokUrl) {
                    try {
                        await disconnect()
                        console.log('\n🔌 ngrok 隧道已断开')
                    } catch (error) {
                        console.error('断开 ngrok 时出错:', error)
                    }
                }
            })
        }
    }
}

async function setupNgrok(
    server: ViteDevServer,
    authToken?: string,
    ngrokOptions: NgrokPluginOptions['ngrokOptions'] = {}
) {
    try {
        // 从 httpServer 获取实际监听的地址信息
        const address = server.httpServer?.address()
        if (!address) {
            console.error('❌ 无法获取服务器地址信息')
            return
        }
        
        const port = typeof address === 'string' ? parseInt(address) : address.port
        const host = typeof address === 'string' ? 'localhost' : address.address === '::1' ? 'localhost' : address.address
        
        console.log(`\n🚀 正在启动 ngrok 隧道...`)
        console.log(`📍 本地地址: http://${host}:${port}`)
        
        // 如果提供了认证令牌,先设置认证
        if (authToken) {
            try {
                await authtoken(authToken)
            console.log('✅ ngrok 认证令牌已设置')
            } catch (error) {
                console.warn('⚠️ 设置 ngrok 认证令牌失败:', error)
            }
        }
        
        // 连接到 ngrok
        const url = await connect({
            addr: port,
            ...ngrokOptions
        })
        
        ngrokUrl = url
        console.log(`\n🌐 ngrok 隧道已建立!`)
        console.log(`┌─────────────────────────────────────────────────┐`)
        console.log(`│ 本地地址: http://${host}:${port}${' '.repeat(Math.max(0, 29 - host.length - port.toString().length))}│`)
        console.log(`│ 公网地址: ${url}${' '.repeat(Math.max(0, 37 - url.length))}│`)
        console.log(`└─────────────────────────────────────────────────┘`)
        console.log(`\n💡 您可以通过公网地址访问您的应用`)
        console.log(`📊 ngrok 状态: http://127.0.0.1:4040 (ngrok 检查界面)`)
        console.log(`🔗 插件状态: http://${host}:${port}/__ngrok_status\n`)
    } catch (error) {
        console.error('\n❌ 启动 ngrok 隧道失败:')
        console.error(error)
        console.log('\n💡 提示:')
        console.log(' 1. 确保已安装 ngrok: https://ngrok.com/download')
        console.log(' 2. 设置认证令牌: ngrok authtoken <your_token>')
        console.log(' 3. 或通过环境变量设置: NGROK_AUTHTOKEN=<your_token>')
        console.log(' 4. 或在插件配置中传入 authToken 参数\n')
    }
}

在启动开发服务器时可以看到日志中已经打印出连接地址
image.png
我们可以通过127.0.0.1:4040地址查看连接状态
image.png

总结

总而言之,ngrok 是一个功能强大且开发者友好的工具,它解决了“如何让外界安全、方便地访问本地服务”这一核心痛点,是现代软件开发流程中不可或缺的利器。


前端小白