简介:针对 Vue 开发中常见的 net :ERR_CONNECTION_REFUSED 报错,这份 PDF 提供了简明的排查与修复思路,适合在本地开发环境频繁切换网络时遇到连接失败问题前端开发者参考。文档从错误产生原因入手,指出 dev 脚本中的 --host 0.0.0.0 需随网络环境调整为固定 IP,并给出完整的 webpack-dev-server 配置示例,帮助读者快速恢复开发服务器访问。同时,文档还补充了 Vue 响应式系统的典型坑点:数组下标赋值、数组长度变化以及对象属性动态增删无法被自动检测,并整理了 Vue.set、this.$set、Object.assign 三种更新数据的可行方案,对理解 Vue 数据绑定机制也有帮助。该资源为单份 PDF,大小约 120KB,内容紧凑,聚焦实战排错;已有 81554 人学习下载,适合需要迅速定位开发环境或数据更新异常的前端学习者。
1. 先看“拒绝”两个字:ERR_CONNECTION_REFUSED 不等于你代码写错
看到浏览器控制台冒出vue net :ERR_CONNECTION_REFUSED,第一反应通常是怀疑自己动坏了哪行代码,把网络请求、代理、响应拦截器改来改去。但这条报错真正想说的是:你的请求包发出去之后,目标端口上压根没有进程在监听,或者监听着的进程直接把这个 TCP 连接拒了。也就是说,问题多半不在前端逻辑里,而在“请求到底有没有送到一个活着的服务上”。
从 TCP 层面理解会发现这件事很朴素。浏览器要跟服务器建立连接,得先完成一次三次握手,而握手的前提是目标 IP 和端口上有程序在listen()。如果那个端口是空的,内核直接回一个 RST 包,浏览器就把这次失败翻译成ERR_CONNECTION_REFUSED。所以排查方向应该从“我是不是写错了请求”转向“服务在不在、监听在哪、网络通不通”。
这篇笔记适合三种人:刚把项目克隆下来npm run dev就遇到报错的新手,前后端联调时后端坚持“服务已经起了”的前端开发者,以及部署到远程服务器后从外部访问不通的倒霉蛋。下面按一条我从经验里压出来的排查链路来写,照着顺序走,基本能把根因钉死。
2. 三条命令定位 ERR_CONNECTION_REFUSED 出在链路哪一段
2.1 先别急着改代码,核对浏览器里请求的实际 URL、端口与协议
排查的第一步不是敲命令,而是睁大眼睛看浏览器的 Network 面板。按下 F12 打开开发者工具,切到 Network 选项卡,找到那行标红或直接显示ERR_CONNECTION_REFUSED的请求,看它的完整 URL。这个过程我一般会检查四个点:域名是不是localhost,端口号是不是后端正在监听的那个,协议是http还是https,路径前缀里有没有多出来的/api或写错的版本号。
这里藏着很多“低级但高频”的坑。最常见的是页面上明明跑在http://localhost:5173,但配置里把后端地址写成了https://localhost:3000,协议都对不上;还有一类是环境变量覆盖——项目里用.env.development定义了VITE_API_BASE_URL,但本机存在一个不进版本库的.env.local,它把接口地址指向了另一个环境的端口,本地后端根本没起。注意 Vue 的.env相关文件改动后必须重启 dev server,光靠热更新不会重新加载环境变量。
如果 Network 面板里请求太多看不过来,可以直接在 Console 里输入fetch('/api/ping')或者fetch('http://localhost:3000/api/ping'),观察这条手动请求的结果,把“页面代码的锅”和“网络链路的锅”快速切分开。这一步做完,你至少能回答“请求到底打到了哪里”这个最基本的问题。
2.2 服务端到底起没起:先问 curl,再问 telnet
地址核对完之后,直接用命令行探测目标端口。我一般先用curl看完整交互过程,再用telnet只验证 TCP 层通不通。下面这条命令是排查时最常用的一招:
# 用 curl 直接打后端接口,-v 输出完整握手过程 curl -v http://127.0.0.1:3000/api/ping # 加上 -m 限制超时时间,避免一直卡住 curl -v -m 10 http://127.0.0.1:3000/api/ping # 如果只关心 TCP 通不通,用 telnet 快速探测 telnet 127.0.0.1 3000curl -v会打印出Trying 127.0.0.1:3000...、Connected to 127.0.0.1或Connection refused这些关键行。看到Connection refused,说明内核回绝了连接,目标端口上没有监听进程,或者有防火墙规则主动发了 RST;看到Operation timed out,说明包发出去后石沉大海,多半是防火墙丢包;看到Connected,说明 TCP 已经建立,问题不在连接层,而在后端应用层没有正确响应。
telnet适合做更纯粹的端口探测:能进入一个黑屏光标界面说明端口通了,立刻按Ctrl+]再输入quit退出。Windows 10 以上默认不带 telnet 客户端,可以用 PowerShell 里的Test-NetConnection 127.0.0.1 -Port 3000替代。这一步的价值在于把“连不上”细分成“拒绝”和“没回应”——这两个现象对应的排查方向完全不同,值得在动手前先分清。
2.3 再看端口和进程:别让“假监听”骗了你
curl 显示连接被拒后,下一步就是查端口上到底有没有进程、进程是不是处于真正的监听状态。这条命令在不同系统上写法不一样,但目的相同:
# Linux / macOS:查看哪个进程在监听 3000 端口 lsof -i :3000 # 没有 lsof 时用 netstat,macOS 注意加 -v netstat -anv | grep 3000 # Windows PowerShell 用 netstat netstat -ano | findstr :3000看输出时有一个极易踩的坑:进程存在不等于正在监听。lsof -i :3000返回的结果里要认准LISTEN状态,如果看到的是CLOSE_WAIT、SYN_SENT、TIME_WAIT,那说明该端口上根本没有接受新连接的服务。另一个常见场景是端口被别的进程占用了——比如你先起了一个 Node 服务占着 3000,再启动后端时它自动切到了 3001,而后端代理配置还指向 3000,你会看到一个典型现象:后端日志显示服务正常启动,浏览器却在报连接被拒。
提示:改完
vite.config.js、.env或后端监听配置后,必须重启对应进程。dev server 的热更新不会重载服务进程的网络配置,这是很多人反复改配置却依然报错的原因。
3. 前后端联调的代理黑洞:devServer 把请求转发给了没人的端口
3.1 为什么 Vue 项目里这个报错九成来自代理配置
Vue 开发模式跑起来的是一个 dev server,页面里的请求走相对路径/api,由 dev server 按代理规则转发到后端真实地址。这套机制的初衷是避免开发时反复处理跨域,但也引入了一个新的出错点:代理目标一旦写错,浏览器这边看到的永远是一条连接被拒。
典型的翻车现场是这样:后端同学说“服务在 3000 端口”,前端配置里也写着target: 'http://localhost:3000',但后端起服务时因为端口被占用,实际上跑在 3001。前端页面请求/api/user,dev server 把请求转发到localhost:3000,那里空空如也,浏览器直接报ERR_CONNECTION_REFUSED。更隐蔽的是后端进程挂掉后,代理目标指向一个曾经存在过的端口,短时间内的报错往往让人误以为是后端代码刚改坏了。
判断是不是代理问题有个很实用的方法:直接访问后端地址,比如在浏览器打开http://localhost:3000/api/ping。如果能正常返回 JSON,而通过页面请求就报连接被拒,那问题基本锁定在 dev server 的代理配置上。另一个线索是 dev server 的终端输出——Vite 在代理失败时通常会打出一行明显的错误日志,包含Proxy error字样。
3.2 检查并修正 vite 的 server.proxy 配置
找到问题指向代理后,打开vite.config.js,重点检查server.proxy这一段。下面是一个我经常用来给前端同事演示的最小可用配置:
// vite.config.js export default { server: { port: 5173, proxy: { // 把以 /api 开头的请求转发给后端 '/api': { target: 'http://127.0.0.1:3000', // 后端真实地址,必须与后端监听一致 changeOrigin: true, // 改写请求头中的 Host 为 target,多数后端需要 pathRewrite: { '^/api': '' }, // 去掉 /api 前缀,按后端路由决定 secure: false // 后端是 https 且证书自签时设 false } } } }这份配置里每一项都值得较真。target里的端口号要与后端进程实际监听端口一致,建议优先写127.0.0.1而不是localhost,理由后面专门讲;changeOrigin控制是否把请求头里的 Host 字段改成 target 的域名,有些后端框架会校验 Host,不开启就返回 403;pathRewrite决定/api/users到达后端时是保留成/api/users还是去掉前缀变成/users,完全取决于后端路由有没有统一挂/api前缀。
如果你用的还是老的 Vue CLI 工程,对应的文件是vue.config.js,字段名为devServer.proxy,配置结构基本一致。改完配置后必做的一件事:重启 dev server,再执行一次 2.2 里的 curl,确认代理转发路径上的端口真的有进程在监听。
3.3 三个必查参数:端口、Host 头、路径前缀
代理配置里最容易出问题的就是下面三个参数,我把它们整理成一张速查表:
| 参数 | 控制什么 | 检查方法 | 典型故障 |
|---|---|---|---|
target端口 | 转发到后端的哪个端口 | lsof -i :端口看监听 | 后端跑在 3001,代理写 3000 |
changeOrigin | 是否改写 Host 头 | 看后端日志收到的 Host 值 | 后端校验 Host,返回 403 或拒绝连接 |
pathRewrite | 是否去掉/api前缀 | 看后端日志收到的请求路径 | 后端有/api前缀但被重写掉,返回 404 |
排查时我习惯于“后端日志做裁判”:让后端同事把请求日志打印出来,前端发起一次请求,看后端到底有没有收到、收到时路径长什么样。如果后端完全没收到请求,是代理转发目标的问题;如果收到了但路径不对,是重写规则的问题;如果收到了但直接 4xx,那是应用层路由或鉴权的问题。这一步能避免你在三个参数之间反复横跳。
4. 服务“起了但连不上”的四个隐藏开关:监听地址、IPv6、防火墙、安全组
4.1 监听地址的坑:127.0.0.1 与 0.0.0.0 的区别
后端进程确实活着,端口也确实有 LISTEN,但请求还是被拒——这时候要怀疑监听地址。Node.js 里app.listen(3000)默认会监听所有可用地址,但很多框架和手动配置里会明确绑定127.0.0.1,导致只有本机能连。比如下面这两种写法,行为完全不同:
// 写法 A:只允许本机回环地址访问 app.listen(3000, '127.0.0.1'); // 写法 B:监听所有网卡,局域网和远程主机都能访问 app.listen(3000, '0.0.0.0');用lsof -i :3000查看时,监听地址会直接显示出来:127.0.0.1:3000说明只服务本机,*:3000或0.0.0.0:3000说明对所有网卡开放。这个坑在两种场景下最容易咬人:一是远程服务器部署,二是手机真机调试。你手机和电脑连着同一个 Wi-Fi,前端用局域网 IP 访问 dev server,但后端只监听了127.0.0.1,真机请求一到就被拒。
有个真实场景让我印象很深:某人开发一个跨平台应用,后端服务在电脑上跑得好好的,手机通过局域网 IP 访问页面,所有接口全部报连接拒绝,电脑上却一切正常。查了半天,后端确实监听了127.0.0.1,改成0.0.0.0并重启后问题消失。注意,如果后端前面还挂着一层 Nginx 或网关做反向代理,那后端刻意只监听本机反而是合理的,不要为了“能连上”盲目改成0.0.0.0。
4.2 IPv6 参与探测:localhost 解析到 ::1 会怎么骗你
localhost这个域名本身就有坑。很多系统上它同时解析到127.0.0.1(IPv4)和::1(IPv6),具体走哪个取决于应用的解析顺序。如果后端进程只监听了 IPv4 的127.0.0.1,而浏览器或 curl 解析localhost后选择了::1,就会出现一个非常困惑的现象:curl http://127.0.0.1:3000成功,curl http://localhost:3000却报连接拒绝。
我自己遇到过一次类似情况:某图像处理 Demo 的后端用 Node 启动,浏览器访问localhost:3000时偶尔报ERR_CONNECTION_REFUSED,刷新几次又好了。后来发现是 Node 服务在其中一个瞬间只绑定了 IPv4,而浏览器恰好在那个时刻走了 IPv6 回环地址。这类问题不好复现,但排查起来并不复杂:
# 强制 curl 走 IPv4,绕过 localhost 的 IPv6 解析 curl -4 http://localhost:3000/api/ping # 强制 curl 走 IPv6,看是否也通 curl -6 http://localhost:3000/api/ping如果-4通而默认不通,说明localhost解析到了::1但服务不监听 IPv6。解决方案是统一使用127.0.0.1作为代理和后端地址,或者让后端服务明确监听0.0.0.0(注意这通常同时覆盖 IPv4 和 IPv6 的监听)。这个坑隐蔽在于:你看到报错是“连接被拒”,但问题本质是 IP 协议栈不一致,和端口、进程都无关。
4.3 防火墙与安全软件:怎么验证是不是它们丢的包
如果本地 curl 能通、浏览器也通,但换了台机器或者手机访问就报错,那大概率是防火墙或安全软件在中间拦截。Linux、Windows、macOS 各有各的防火墙入口,但验证思路一致:先用一条命令看当前状态,再临时放行端口测试,最后把规则固化下来。
# Ubuntu / Debian 系:查看 ufw 状态和规则 sudo ufw status # CentOS / RHEL 系:查看 firewalld sudo firewall-cmd --list-all # 临时放行 3000 端口(ufw 示例) sudo ufw allow 3000/tcp判断防火墙拦截有个经验法则:被丢弃的包通常表现为Operation timed out,而不是Connection refused。因为防火墙直接丢包时,客户端收不到任何回应,只能等超时;而端口没监听时,内核会主动回 RST,客户端立刻收到拒绝。如果现象是超时,优先怀疑防火墙和安全软件,配合上一步的 curl 输出可以很快定性。
Windows 上还要额外注意第三方安全软件,它们自带“网络防护”或“入站拦截”,有时候会静默拦截本机回环之外的连接。验证方法很笨但有效:临时退出安全软件,再用手机或另一台机器访问一次。如果恢复访问,把开发用的端口加入白名单;如果仍然不通,继续查下一层。
4.4 远程服务器上被安全组挡住的“二次拒绝”
部署到云服务器后,即使后端监听0.0.0.0、系统防火墙放行,依然可能从外部连不上,这时候要查云平台的安全组。很多人忽略了一点:云服务器实际上经过两道防火墙——实例绑定的安全组,以及系统内部的防火墙。安全组写在虚拟化层,入站流量先过它,再进系统。安全组没放行时,外部访问的表现通常也是超时或拒绝,和系统防火墙几乎一样。
排查顺序建议这样:在服务器本机执行curl http://127.0.0.1:3000确认服务正常,再从本机执行telnet 服务器公网IP 3000验证外部连通。如果本机通、外部不通,安全组嫌疑最大。这时候打开云平台控制台,找到实例绑定的所有安全组,检查入站规则里有没有放行 3000 端口。注意一个细节:一个实例可以绑定多个安全组,规则取的是并集,任何一个安全组的默认拒绝规则都可能拦掉流量。
改完安全组后过一两分钟再测,规则下发有延迟。如果仍然不通,把系统防火墙的对应端口也放行一遍。我见过不少情况是安全组放行了、但系统里firewalld或ufw还拦着,两道关卡必须同时打开。真机调试场景也走同样的链路,只是把“远程服务器”换成了开发机本身,检查逻辑不变。
5. 避坑:Vue 项目里最容易翻车的 5 个排查现场
5.1 改了后端代码没重启:热更新只救前端,不救后端进程
现象:后端同学改完代码说“已经生效了”,前端刷新页面还是ERR_CONNECTION_REFUSED。原因:Vite 的热更新只管前端模块,后端 Node 进程改了监听端口、路由前缀之后,旧进程还占着原来的端口和配置。解决:改完后端必须重启进程,重启前用lsof -i :端口确认旧进程已经被干掉,否则新进程会因端口占用起不来,或者悄悄换到另一个端口。
5.2 https 页面请求 http 后端:表面是连接拒绝,实则是混合内容
现象:页面部署在一台 https 服务器上,请求http://127.0.0.1:3000时报错,且错误信息里有时带证书相关提示。原因:浏览器对 https 页面发起 http 请求有混合内容拦截策略,再加上证书信任问题,最终表现可能就是连接失败。解决:本地开发时保持页面和后端协议一致,都走 http;如果后端必须 https,把secure: false配置上,同时确保代理目标里的证书能被本机信任,自签证书尤其容易在这类场景里埋雷。
5.3 进程假死:端口还开着,但新连接全部被拒
现象:lsof -i :3000显示进程在监听,curl 却报拒绝或一直转圈。原因:进程可能被调试器暂停、事件循环被同步操作阻塞,或者连接数达到上限,内核不再接受新连接。解决:先看进程状态,ps aux | grep node观察 CPU 和状态;其次重启进程,重启后立刻 curl 验证;最后才考虑是不是需要换端口。这类问题最花时间的部分是“进程活着”这个假象会误导你绕开正确的排查方向。
5.4 浏览器缓存与 Service Worker:你看到的报错不是最新代码
现象:代码明明改好了,刷新页面还报同样的连接拒绝,清掉缓存后报错消失或变化。原因:PWA 项目的 Service Worker 把旧页面和旧请求缓存下来了,离线时请求被 SW 拦截后走了错误逻辑,表现为莫名其妙的连接失败。解决:打开 DevTools 的 Application 面板,找到 Service Workers,点击 Unregister 注销,再点 Clear storage 清空站点数据;调试时记得勾选 Network 面板的 Disable cache,避免缓存再次干扰判断。
5.5 端口号抄错、环境变量覆盖了 baseURL:最“低级”却最高频
现象:后端明明监听 3000,前端请求打向 3001,或者.env里定义的地址被另一个文件覆盖。原因:.env.local的优先级高于.env.development,而且.env.local一般不进版本库,别人拉代码根本看不到这个文件存在。解决:在页面代码里把最终拼接好的请求地址打印到 Console,核对端口与路径;查项目根目录下是否存在.env.local、.env.development.local这类文件;让后端同事贴一份实际监听的端口号做比对。我排查时习惯让当事人把最终发出去的 URL 直接贴过来,比自己猜快得多。
6. 把浏览器报错变成命令行答案:curl 复现与三层排障顺序
6.1 从浏览器一键复制出真实请求
既然报错在浏览器里,那就让浏览器告诉我们它到底发了什么。DevTools 的 Network 面板里,右键点击那条失败的请求,选择 Copy as cURL,然后粘到终端里执行。这样做的好处是:请求头、Cookie、查询参数、请求体全部原样保留,比手打一条测试请求更接近真实故障现场。
# 从浏览器复制出的 curl 命令,-v 会打印完整连接过程 curl -v 'http://127.0.0.1:3000/api/ping' \ -H 'Content-Type: application/json' \ --data-raw '{"ping":1}'-v是这里最关键的参数,它输出里的Trying、Connected、Connection refused直接告诉你 TCP 层发生了什么。-H后面的请求头不要随便删,有些后端会校验 Content-Type 或自定义头,删掉后行为可能变化,反而误导判断。
6.2 三种输出的判读表
curl 的输出虽然每个字都不一样,但核心结果基本可以归成三类:
| curl 输出关键字 | 含义 | 下一步方向 |
|---|---|---|
connect ECONNREFUSED/Connection refused | 端口无监听或防火墙主动拒绝 | 查进程监听、lsof、系统防火墙 |
Operation timed out | 数据包被丢弃,无任何回应 | 查防火墙、安全组、网络路由 |
Connected但无响应或 4xx/5xx | TCP 已建立,应用层异常 | 查后端日志、路由、鉴权逻辑 |
这三类判读基本覆盖了ERR_CONNECTION_REFUSED的绝大多数场景。一旦自己在心里能快速把报错归到这三类里,排查就变成套路活了,不会跟着感觉瞎改配置。
6.3 以后再遇到这个报错,我的执行顺序
现在把我的固定排查顺序完整交代一遍,按这个顺序走基本不会漏:先问一句“后端进程还活着吗”,用lsof -i :端口看监听;接着在页面 Console 打印最终请求 URL,核对端口和路径;然后从终端 curl 一下后端本机地址,区分拒绝还是超时;再查监听地址是127.0.0.1还是0.0.0.0;之后检查 dev server 代理配置和环境变量;最后清缓存、注销 Service Worker。全程不碰前端业务代码,除非以上全部通过。
我自己在本地跑项目时的习惯是开三个终端:一个跑后端,一个跑前端,一个专门敲 curl 和 lsof。后端一重启,先 curl 一下再切回浏览器刷新,省得在页面上反复手滑。这个习惯让我少踩了无数次“以为改了代码其实没重启”的血泪坑。看完这篇,如果你手头正好有报错现场,先别急着改代码,按链路从浏览器那端往外量一遍,答案通常就在某个端口或某行配置上。希望帮到你。
本文还有配套的精品资源,点击获取