首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >WorkBuddy 工作台按钮点了没反应:真凶不在后端,在页面是从哪打开的

WorkBuddy 工作台按钮点了没反应:真凶不在后端,在页面是从哪打开的

原创
作者头像
用户12800671
发布于 2026-10-12 09:19:12
发布于 2026-10-12 09:19:12
280
举报

我在本机跑了一个很小的 HTTP 服务,给自己做的监控面板用。面板上有一排操作按钮:停手、恢复、记一笔、刷新。某天这些按钮点了全都没反应——不报错、不转圈、页面也不刷新,就像没点一样。

诡异的是,后端日志干干净净。我用命令行直接请求那个接口,返回的是 200 和一串正确的 JSON。

下面记录我最后是怎么定位并修掉它的。结论先给:问题不在后端,而在前端用了相对路径;而相对路径打到哪台服务器,取决于这个页面是被谁打开的。

一、先把最贵的那个假设排除掉

第一反应是"服务挂了"。查进程,活着;查端口,listen 着。

第二步,绕开浏览器,直接请求接口:

代码语言:bash
复制
$ curl -s -X POST "http://127.0.0.1:8015/halt?line=L2&action=stop"
{"ok": true, "halted": true}

接口完全正常。

于是排除掉三项:接口没问题、进程没问题、参数也没问题。那"没反应"就只剩一种解释——请求根本没发出去,或者发去了别的地方。

二、分野:同一份 HTML,两种打开方式

我的面板有两条打开路径:

  1. 浏览器直接访问 http://127.0.0.1:8015/;
  2. 从客户端的内置预览面板、或者云端页面里打开同一份 HTML。

两种方式的 HTML 内容一模一样,但在浏览器眼里,它们不是同一个源(origin)。前端当时的写法是:

代码语言:js
复制
fetch('/halt?line=' + line + '&action=' + action, {method:'POST'})

/halt 是相对路径。它补全后的完整地址 = 当前页面的源 + /halt。

  • 方式 1:页面源就是 http://127.0.0.1:8015,请求打到 8015,成功。
  • 方式 2:页面源是别的(预览面板的虚拟源、或云端域名),请求打到那个源下的 /halt。那台服务器上根本没有这个接口,返回 404;就算它碰巧有,跨源也会被浏览器的同源策略拦掉。

表现就是"点了没反应":请求确实发出去了,但发错了地方,而且错误被静默吞掉了。

这一点值得单独强调:"没反应"比"报错"难查得多。 报错至少会告诉你哪里错了;没反应意味着错误发生在你没在看的那一层——Network 面板和 console。当时的失败回调里只有一句话,没人会专门点开控制台看它。

三、修法第一层:前端双路兜底

既然"当前页面源"不可控,就不要依赖它。改成先试同源,失败了再试绝对地址:

代码语言:js
复制
function wbPost(path, ok, fail, opts){
  opts = opts || {};
  var init = {method: opts.method || 'POST', cache: 'no-store'};
  if (opts.headers) init.headers = opts.headers;
  if (opts.body) init.body = opts.body;
  var urls = [path, 'http://127.0.0.1:8015' + path], i = 0;
  (function next(){
    if (i >= urls.length){
      fail('够不到本机服务(确认 8015 在跑,或双击「工作台.bat」)');
      return;
    }
    fetch(urls[i++], init)
      .then(function(r){ return r.json(); })
      .then(function(d){
        if (d && d.ok) ok(d);
        else fail((d && d.msg) || '未知原因');
      })
      .catch(function(){ next(); });   // 这一路失败,就换下一个源重试
  })();
}

有三个细节值得说:

  1. 顺序是"同源优先"。能在同一个源里解决,就别跨源,省一次 CORS 往返。
  2. 失败必须走到 fail,而且要给人一句话。前端把异常吞掉,用户只会说"点了没反应",等于把排查成本转嫁给了未来的自己。
  3. 绝对地址写死 127.0.0.1,不写 localhost。这两个名字在有些环境里解析结果不同(IPv4 / IPv6),而我的服务只 listen 在 127.0.0.1。

四、修法第二层:服务端把跨源说清楚

只改前端还不够。当页面源不是 8015 时,浏览器会先做一次跨源判定,所以服务端得明确表态:

代码语言:python
复制
def _send(self, code, body, ctype="text/html; charset=utf-8"):
    self.send_response(code)
    self.send_header("Content-Type", ctype)
    self.send_header("Content-Length", str(len(body)))
    self.send_header("Cache-Control", "no-store")
    self.send_header("Access-Control-Allow-Origin", "*")
    self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
    self.send_header("Access-Control-Allow-Headers", "Content-Type")
    self.end_headers()
    self.wfile.write(body)

def do_OPTIONS(self):
    """预检兜底:现在都是简单请求不走预检,留着防以后加自定义头。"""
    self.send_response(204)
    self.send_header("Access-Control-Allow-Origin", "*")
    self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
    self.send_header("Access-Control-Allow-Headers", "Content-Type")
    self.send_header("Content-Length", "0")
    self.end_headers()

有人会问:Allow-Origin: * 是不是放得太宽?

在这里成立,原因是服务本身只 listen 127.0.0.1,外网根本连不上这个端口;跨源放行的实际含义只是"本机的其它页面可以访问本机的这个服务"。如果这个服务是对外提供的话,就不能这么写。

do_OPTIONS 是预检兜底。当前的请求都是"简单请求",不会触发预检;但以后一旦加了自定义请求头,浏览器就会先发一个 OPTIONS,缺了这个分支就会卡在预检阶段——那又会是一次"点了没反应"。

五、第二个"没反应":按钮压根没绑事件

把上面两层修完,我发现还有一个按钮照样没反应。翻代码才看清:在网页版里,那个按钮只是渲染出来的一行文案,它根本没有绑定点击事件。 它是"看起来像按钮的文字",不是按钮。

这类问题用眼睛是看不出来的,得让脚本去核:在体检脚本里加一条规则——凡是带按钮样式的元素,都要求在事件绑定表里能找到对应项;查不到就报出来。渲染出来的样子,不等于它真的能用。

六、把"点了没反应"拆成三段

这次之后,我给自己定了条规矩:任何"点了没反应",按三段排查,每段留一个可观测点。

  1. 事件绑定了吗 —— 在控制台手动调一次处理函数,看它有没有被触发;
  2. 请求发出去了吗 —— 看 Network,确认 URL、方法、状态码;
  3. 服务端收到了吗 —— 看服务端自己的日志。

三段里必然有一段是断的。这次断在第 2 段;而第 2 段的断点由"页面是从哪打开的"决定——那是我一开始完全没有考虑到的变量。

小结

  • 相对路径的解析基准是"当前页面的源",不是"你以为的那台服务器";
  • 本地页面会被从多种入口打开,前端必须假设"源不可控";
  • 失败分支要给用户可见的提示,否则错误会被静默吞掉;
  • 只 listen 127.0.0.1 的服务放行跨源是可控的,对外服务不行;
  • "渲染出来了"不等于"绑定上了",按钮有没有事件要用脚本核。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、先把最贵的那个假设排除掉
  • 二、分野:同一份 HTML,两种打开方式
  • 三、修法第一层:前端双路兜底
  • 四、修法第二层:服务端把跨源说清楚
  • 五、第二个"没反应":按钮压根没绑事件
  • 六、把"点了没反应"拆成三段
  • 小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档