拓冰建站拓冰建站
首页 / 资讯中心 / 正文

JavaScript被禁用与javascript:void(0)排查替代

上周帮朋友看一个后台管理系统他发来一张截图页面上所有按钮都点不动搜索框敲了字也提交不了连日期选择器都弹不出来。我让他按 F12 打开控制台Console 面板果然飘着一行红字Refused to execute inline script because it violates the following Content Security Policy directive。同一时间他还发来另一个页面说鼠标放到某个编辑按钮上左下角会显示javascript:void(0)点了却只有页面轻轻抖一下什么都不发生。这其实是两个完全不同的问题前者是JavaScript 被禁用或被拦截后者是javascript:void(0)这种陈旧写法在现代浏览器和现代前端架构里留下的坑。它们经常一起出现因为一个页面如果本来靠 JS 干活一旦 JS 没跑起来那些挂着hrefjavascript:void(0)的空链接就会原形毕露——什么都没做。这篇内容我打算把两条线都讲清楚普通用户遇到请启用 JavaScript该去哪里开、怎么开、开了没用怎么办前端开发者又该怎么写代码才能既不依赖javascript:void(0)又能在 JS 真的挂掉时给用户留一条活路。不管你是只会点鼠标的普通用户还是天天写addEventListener的人都能在这里找到可以直接抄的操作路径。1. 页面按钮点了没反应先判断是不是 JavaScript 被拦住了在动手改设置之前得先确认问题到底出在哪一层。很多人一看到页面卡住就去重装浏览器实际上九成的页面不动跟浏览器装得好不好没关系而是脚本根本没被执行。判断方法不复杂关键是要按顺序排除别乱猜。1.1 JavaScript 被禁用其实有四种完全不同的来源我习惯把JS 没跑分成四类因为每一类的解决路径完全不同混在一起排查会浪费大量时间。第一类是用户手动关闭。浏览器设置里确实有 JavaScript 开关有些人是早年为了省流量减少弹窗关掉的之后忘了换了很多网站都觉得这网站真烂。这类问题最好解决把开关拨回去就行。第二类是浏览器扩展或内容拦截器。像 NoScript、部分广告拦截工具、隐私保护类扩展默认策略就是先拦掉所有脚本白名单里的才放行。用户自己根本没关过 JavaScript但扩展替他关了。这种情况最迷惑人因为浏览器设置里明明显示允许。第三类是嵌入容器层面的关闭。你打开的可能不是浏览器而是某个 App 内置的网页视图。iOS 上WKWebView的脚本开关由宿主 App 控制WKWebpagePreferences的allowsContentJavaScriptiOS 14 之后取代了老的javaScriptEnabled如果 App 把它关了页面里的 JS 一行都不会执行Android 上同类容器跟随系统 WebView 的策略。这类问题用户侧几乎无法通过设置解决只能换用系统浏览器交叉验证。原生和 JavaScript 互相调用OC 与 JS 互调、JSBridge 这一类在 App 里很常见一旦宿主禁了 JS桥接方法全都调不通页面看起来就是死的。第四类是代码自己把自己拦住了也就是内容安全策略CSP。这种情况严格说不是JavaScript 被禁用但表现一模一样脚本文件能下载、能解析就是不给执行。控制台会明确报Refused to execute inline script或者Refused to run the JavaScript URL。这是服务端配置问题用户改设置一点用都没有。注意先分清是哪一类再动手。用户被扩展拦住的问题你去改浏览器设置是白费功夫服务端 CSP 写死的问题用户重装十次浏览器也一样。1.2 三分钟自检确认浏览器到底有没有在执行 JavaScript我平时用一套固定的自检流程三分钟能定性。打开页面按 F12macOS 上是 OptionCommandI唤出开发者工具切到 Console 面板直接敲一行11回车。如果返回2说明 JS 在执行问题不在禁用而在你的代码逻辑或某个资源加载失败如果提示类似Warning: Dont paste code into the console却始终不给结果或者返回 undefined那基本可以确定脚本执行被拦了。第二步看 Network 面板。刷新页面按 JS 类型过滤观察那些.js文件的状态码。如果是 200 但页面还是不动重点看 Console 有没有 CSP 报错如果是 404 或 403那是部署问题文件压根没上去或者被权限挡住了如果列表里干脆没有.js请求说明 HTML 里的script标签本身就没被解析出来——这种情况我见过好几次最后发现是标签写成了自闭合的script srca.js /HTML 不像 XML那个斜杠不闭合标签后面一大段 DOM 都被当成脚本文本吞掉了。第三步做交叉验证开一个无痕窗口无痕模式默认不加载扩展访问同一个页面。如果无痕下正常、普通窗口下不正常那九成是扩展在捣乱反之如果两种模式都不正常就往浏览器设置、宿主容器和 CSP 方向查。第四步看页面上有没有那种启用 JavaScript 才能继续的提示很多站点会写 please enable javascript to continue. 这类文案。这个提示通常放在noscript里它显示出来基本等于官方盖章浏览器确实没执行脚本。但要留个心眼noscript只在脚本没执行时渲染脚本 404 或者语法错误导致整段没跑完它同样会显示所以它证明的是脚本没跑不一定是用户禁用了。2. 各个平台开启 JavaScript 的实操路径确认是被禁用之后接下来就是怎么开。不同平台、不同浏览器的入口差别很大而且这些年不少浏览器把开关越藏越深甚至直接移除了。我把常见的都列出来你可以直接对号入座。2.1 桌面浏览器Chrome、Edge、Firefox、Safari 逐一说清Chrome 是最常见的。最快的办法是在地址栏直接输入chrome://settings/content/javascript回车就能跳到 JavaScript 设置页选择网站可以使用 JavaScript。走菜单的话是右上角三点 → 设置 → 左侧隐私和安全 → 网站设置 → 往下找到JavaScript。这里还有一层更细的控制下面有个不允许使用 JavaScript的列表如果某个站点被你不小心加进去了在这个列表里删掉它。另外还可以做单站设置——在目标网站页面点地址栏左侧的图标那把锁或者调节图标→ 网站设置 → 找到 JavaScript → 改成允许。这个单站开关优先级高于全局设置很多我明明开了全局还是不行的情况就是这里被单独设成了阻止。Edge 的逻辑和 Chrome 基本一致地址栏输入edge://settings/content/javascript或者走设置 → Cookie 和网站权限 → JavaScript。同样支持全局开关加单站例外。Firefox 的入口在设置 → 隐私与安全往下滚到权限区域勾选启用 JavaScript。如果你习惯直接改配置地址栏输入about:config搜索javascript.enabled双击切换成true。需要提醒的是Firefox 的扩展生态里脚本拦截类工具特别多如果设置里是勾选状态还是不生效先去扩展管理页临时禁用全部扩展试一次。Safari 桌面版的情况不太一样现代版本已经不提供关闭 JavaScript 的图形开关脚本默认始终启用。如果你在 Safari 上遇到脚本不执行排查方向应该换成三个一是设置 → 扩展看有没有安装拦截类扩展二是设置 → 隐私看内容拦截器三是先在高级里勾上显示网页开发者功能然后看开发菜单里是不是有人勾选过停用 JavaScript这类开发选项。这个开发菜单的开关很容易被误触后遗忘我见过不止一次。2.2 手机端和 App 内置浏览器入口更分散iOS 上打开设置 → 找到Safari 浏览器 → 往下滑到高级 → 里面有 JavaScript 开关绿色是开。注意 iOS 上这个开关是跟随系统 Safari 的第三方浏览器比如各种基于 WebKit 的浏览器有自己的设置项需要单独看。Android 上 Chrome 的路径是右上角三点 → 设置 → 网站设置 → JavaScript。Android 上还有个容易被忽略的点——如果你想给某个站点单独放行在网站设置里进JavaScript再找添加例外或者站点级权限列表。App 内置浏览器各种社交、电商、资讯类 App 里点开链接后出现的那个页面通常不提供 JavaScript 开关它跟随宿主 App 和系统 WebView 的策略。这类环境出问题的概率反而更高可能是宿主 App 出于安全考虑限制了脚本也可能是某次 WebView 内核更新引入的兼容问题。我的建议是遇到这种环境异常先把链接复制出来用系统浏览器打开。如果系统浏览器正常、App 内不正常那就是容器问题用户侧无解只能反馈给 App 方。2.3 开关明明打开了为什么还是不行这是最气人的情况之一。历史上我就撞过好几次总结下来几个高频原因。扩展优先级更高。大部分脚本拦截扩展的工作方式是在浏览器执行脚本之前先注入自己的拦截逻辑它的判断发生在浏览器设置之后。所以设置里显示允许扩展照样能拦。验证方法就是无痕窗口或者临时禁用全部扩展。被统一配置的策略覆盖。一些公司统一管理的电脑会通过策略下发浏览器配置Chrome 里可以在地址栏输入chrome://policy查看当前生效的策略项搜索 JavaScript 相关的条目。如果看到默认设置被指定为禁用、或者某个域名被列入了阻止名单那你手动改设置是改不动的界面上的开关可能是灰的也可能改完刷新就变回去。这种情况只能找管理设备的同事。服务端 CSP 把脚本挡了。这一条我要多讲两句因为它最容易被误判成JS 被禁用。内容安全策略是服务端通过 HTTP 响应头下发的规则比如script-src限制了允许加载脚本的来源。常见报错有两类一类是内联脚本被拒报Refused to execute inline script另一类是javascript:伪协议被拒报Refused to run the JavaScript URL。有意思的是很多人以为javascript:void(0)是空的不做事不会触发安全策略实际上现代浏览器把它当作脚本 URL 来处理在严格 CSP 下一样会被拦只不过因为它是空的被拦了你也看不出来。语法错误导致整块脚本没执行。如果某个.js文件第一行就有语法错误整个文件都不会执行后面的功能全部失效表现和JS 被禁用一模一样。控制台里一定有红字别只看页面。3. 前端开发者视角JS 被禁用时页面应该长什么样前面讲的是用户怎么自救接下来这部分是给写代码的人看的。坦白说现在很多前端页面对 JavaScript 的依赖已经到了没它就没有一切的程度从数据请求、路由跳转、表单校验到 3D 渲染比如用纯 JavaScript 配合 WebGPU 做高斯泼溅这类计算密集型的场景全都跑在 JS 上。但这种设计意味着一旦脚本没跑起来用户看到的就是一片空白或者一堆点不动的按钮。这不该是默认状态。3.1 noscript 的正确用法和几个常见误用noscript是 HTML 里唯一一个专门用来处理脚本没执行场景的元素。它的语义是只有当脚本没有执行时里面的内容才参与渲染。用法上分两处一处放在head里一处放在body里。放 head 里的典型用法是给整页降级noscript style .js-only { display: none !important; } .no-js-tip { display: block !important; } /style /noscript这样即使脚本没跑你也能用纯 CSS 把依赖 JS 的模块隐藏掉把提示块显示出来。注意 img 的加载行为在 noscript 里受 CSP 影响早期有安全考虑禁止 noscript 内加载图片所以提示尽量用文字和样式别依赖图片资源。放 body 里的用法最常见就是一句醒目的提示告诉用户去哪开。文案别写得太技术写请开启浏览器 JavaScript 后重试通常在浏览器的设置 - 网站设置里这种带路径的话比单纯一句 please enable javascript to continue. 有用得多。我见过两个常见的误用。一是把 noscript 当成检查 JS 是否被禁用的手段然后据此判断用户的浏览器能力——这是错的noscript 反映的是脚本有没有执行脚本加载失败、CSP 拦截、语法错误都会触发它。二是把关键内容塞在 noscript 里指望被搜索引擎收录——实际上大部分搜索引擎是执行脚本的noscript里的内容权重并不高把它当 SEO 手段不靠谱。3.2 渐进增强让表单和链接在没 JS 时也能用这块是我最想强调的。原生 HTML 本身就自带很多能力问题在于我们用 JS 把它们覆盖掉了。拿表单来说form action/search methodget加上button typesubmit这是浏览器原生支持的提交方式不依赖任何脚本。但很多项目的写法是按钮写成button typebutton点击事件绑一个 JS 函数函数里拼参数、发请求、渲染结果。这个方案在 JS 正常时当然更流畅但 JS 一挂按钮就彻底没反应。比较稳妥的做法是保留原生提交作为兜底用 JS 去preventDefault()接管——也就是默认能工作脚本在的时候才做增强。原生表单提交和前端脚本接管这两种模式的区别本质上就是默认可用和默认不可用的区别选择哪种决定了你的页面在极端情况下的下限。链接也是同理。一个功能按钮如果它做的是跳转到某个路由用a href/detail?id1是天然可用的右键还能在新标签页打开如果用a hrefjavascript:void(0) onclickgo()那既不可复制链接也不能新标签打开脚本一挂就彻底废掉。这里的关键判断是这个交互的本质是导航还是动作是导航就用 a 标签配真实 href是动作提交、删除、弹窗就用 button。可点击的元素分不清自己是链接还是按钮是无障碍和可用性问题的共同根源。3.3 资源加载和执行顺序的检查手段想在运行时确认脚本资源到底加载成没成有几个便宜好用的办法。document.readyState能看出文档处于哪个阶段loading / interactive / complete。如果你在 loading 阶段就去找 DOM 节点肯定找不到。window.performance.getEntriesByType(resource)能列出页面加载的所有资源包含名称、耗时、传输大小。排查某个 JS 没加载时我经常直接在控制台跑这一句然后按耗时倒序看明显缺失的文件一眼就能发现。针对单个资源用onload和onerror是更直接的做法const s document.createElement(script); s.src /assets/main.js; s.onload () console.log(脚本加载完成); s.onerror () console.error(脚本加载失败); document.head.appendChild(s);动态插入脚本默认是异步执行的如果你有顺序依赖记得手动管理执行顺序别指望它们按插入顺序跑。图片的判断稍微特殊一点img.complete为 true 且naturalWidth大于 0 才算真正解码成功只判断 complete 有时候会被缓存里的坏图骗过去。执行顺序上script默认是阻塞解析的加defer会等文档解析完按顺序执行加async则是下载完就执行、顺序不定。凡是依赖 DOM 的初始化逻辑要么放defer要么监听DOMContentLoaded别指望写在 head 里就能安全拿到节点。事件监听这块也有个细节滚动、触摸这类高频事件如果处理函数里不做重活记得加上{ passive: true }能让滚动更跟手只触发一次的监听器用{ once: true }省得自己手动移除。4. 把 javascript:void(0) 这件事讲透现在说回另一半标题。javascript:void(0)几乎是我在代码审查里见到最多的一行历史遗留它的存在有历史原因但在今天基本是负资产。要理解它得先理解void运算符和javascript:伪协议这两件事。4.1 void 运算符和 javascript: 伪协议的真实行为void是 JavaScript 里的一个一元运算符作用很简单对它后面的表达式求值然后丢掉结果返回undefined。所以void 0、void(0)、void hello的结果都是undefined写 0 只是因为它最短。那为什么是void 0而不是直接写undefined这是 ES5 之前的历史包袱。在很早的语言版本里undefined只是全局对象上的一个普通属性是可以被重新赋值的——有人写过undefined 1这种代码之后所有判空逻辑全崩。所以库作者们发明了void 0这个套路保证无论环境多脏都能拿到真正的 undefined。ES5 之后全局undefined变成了只读属性两者在语义上等价了但压缩工具比如 Terser依然会把代码里的undefined替换成void 0因为三个字符比九个字符短能省一点体积。你在压缩后的产物里看到满屏void 0就是这个原因。再说javascript:伪协议。它允许在 URL 位置直接写一段脚本浏览器在导航时会在当前文档上下文里执行它。关键在于执行结果的处理如果这段脚本返回一个字符串浏览器会把这个字符串当成新文档内容替换掉当前页面。这一条是无数事故的源头。比如你写a hrefjavascript:doSomething()而doSomething恰好返回了一个字符串比如它最后一行是return 操作成功用户一点整个页面就会被操作成功四个字覆盖掉。而javascript:void(0)的价值就在于它的返回值一定是 undefined浏览器不会拿它当文档内容页面保持不变看起来就是什么都没发生。顺带说一句这类写在 URL 里的代码还有另一种用途就是把一小段脚本存成浏览器书签点一下就执行——比如把视频旋转 90 度观察画面这类小工具本质上就是这个机制的应用。不过现代浏览器对地址栏里手动粘贴的javascript:开头内容会做剥离处理防止被诱导执行所以这类书签只能存起来用直接粘贴到地址栏往往不生效。4.2 为什么当年大家都这么写代价是什么hrefjavascript:void(0)在十几年前是主流做法原因很实在那时候大家普遍用a标签做所有可点击的东西因为 a 标签自带鼠标手型、自带键盘可聚焦样式也省事。但a必须有个 href不写 href 的话在旧浏览器里就没有链接样式写了href#又会跳到页面顶部并且往地址栏塞一个井号于是javascript:void(0)就成了既保留链接外观、又什么都不做的最省事写法。代价是显而易见的。第一个代价是语义错误一个执行动作的东西被标成了链接屏幕阅读器会念成链接用户期待点开一个新页面结果什么都没发生。第二个代价是可访问性缺失真正的按钮天生支持 Enter 和空格触发链接只支持 Enter空格键在链接上默认是滚动页面用鼠标的用户毫无感知用键盘的用户会觉得这个按钮坏了一半。第三个代价是导航能力丢失右键不能在新标签页打开不能复制链接地址不能长按预览用户少了一大半操作自由度。第四个代价是它和现代安全策略冲突前面说过在配置了严格 CSP 的站点上这类伪协议 URL 会直接被拒绝执行控制台留下报错而因为它是空的功能上你也看不出任何异常只是有一段报错一直在那里。第五个代价是排查困难一个挂着javascript:void(0)的元素从 DOM 上完全看不出它该干什么到底是什么事件在处理它得顺着onclick或者事件委托一路找维护成本远高于一个明确的button>button typebutton classbtn>document.addEventListener(click, (e) { const btn e.target.closest([data-action]); if (!btn) return; const action btn.dataset.action; const handlers { save: () console.log(执行保存), cancel: () console.log(执行取消) }; handlers[action]?.(); });这段用了事件委托好处是动态插入的按钮不用重新绑定>document.querySelectorAll(a[data-internal]).forEach(a { a.addEventListener(click, (e) { if (e.metaKey || e.ctrlKey || e.shiftKey || e.button ! 0) return; e.preventDefault(); router.push(a.getAttribute(href)); }); });那个前置判断很重要用户按住 Command 或者 Ctrl 点链接意图就是新标签打开这时候你不能拦拦了就是跟用户对着干。如果确实需要一个不在 DOM 层面表达 href 的按钮型链接那就用a rolebutton tabindex0同时补上键盘事件el.addEventListener(keydown, (e) { if (e.key Enter || e.key ) { e.preventDefault(); doAction(); } });注意必须preventDefault()不然空格键会顺便把页面往下滚一屏体验很怪。5.3 字符串调函数的场景和安全边界有些老项目里能见到用字符串调用函数的写法比如根据配置里的函数名动态执行。原理是window上的属性可以直接用中括号访问所以window[handleSave]()是能跑起来的。热词里提到的通过字符串调用函数说的就是这类模式。这个写法能不用就不用。原因是它把函数暴露在全局任何注入进来的一段脚本都能直接调用你的业务方法风险很高同时函数名拼错了不会报找不到函数只会报window.someFn is not a function排查时容易绕远路再加上全局命名冲突两个模块起了同名函数谁覆盖谁全看加载顺序。比较稳妥的替代是用一张显式的映射表只把需要暴露的方法放进去其他一律不给const actions Object.freeze({ save: () { /* ... */ }, cancel: () { /* ... */ } }); const run (name, ...rest) actions[name]?.(...rest); run(save);这里的剩余参数...rest用来透传附加参数配合可选链?.保证名字不存在时静默失败而不是抛错。至于eval和new Function在配置了 CSP 的站点上会被unsafe-eval规则直接拒绝执行所以这类方案在现代安全要求下基本没有生存空间。顺便说一句字符串处理。很多同学在拼提示文案或者请求 URL 时喜欢一路用这本身没问题但可读性在参数多了以后会急剧下降模板字符串和数组join是更清楚的选择。性能上不用纠结现代引擎对字符串拼接的优化已经很到位可读性才是这个场景里真正值得优化的东西。6. 常见问题与排查技巧速查最后把前面散落的排查经验收一收形成一套固定的动作顺序。下次再遇到照着走就行。6.1 我自己的固定排查顺序第一步确认脚本到底有没有执行。控制台敲11比看任何提示都准。第二步看控制台的红字别只看页面的表现。CSP 报错、语法错误、跨域错误这三类都会表现为功能不工作但修法完全不同。第三步看 Network 里脚本的状态码。200 说明文件到位404 是部署问题被 CSP 拦的话请求可能根本没发出去。第四步无痕窗口交叉验证。区分扩展问题和环境问题这一步五秒钟就能定性性价比最高。第五步换浏览器交叉验证。如果换了浏览器就好了问题在你的脚本用了某个浏览器不支持的语法。我遇到过那种只写了可选链和空值合并就用上了压缩打包的项目在没有做语法降级的构建配置下老版本浏览器直接整个文件解析失败报的还是最难找的那种语法错误。第六步检查是否为嵌入容器。页面在某个 App 里打开时出问题先复制链接用系统浏览器验证一次能把问题范围砍掉一半。6.2 问题与解决方案对照表现象最可能的原因处理方式页面显示请启用 JavaScript浏览器设置里被关闭或扩展拦截按平台开启开关无痕窗口验证扩展控制台报 Refused to execute inline script服务端 CSP 未允许内联脚本把内联脚本外置为文件或调整 CSP 配置控制台报 Refused to run the JavaScript URLCSP 不允许javascript:伪协议把javascript:void(0)换成 button 或真实链接所有.js请求都是 404构建产物没发布或路径拼错检查静态资源目录和部署流水线页面上完全看不到.js请求script标签写法有误或被吞掉检查是否误用自闭合写法检查标签嵌套某些浏览器直接报语法错误使用了未降级的新语法检查构建目标的浏览器兼容配置公司电脑上怎么改设置都没用设备统一下发的浏览器策略在chrome://policy查看生效策略App 内打不开、系统浏览器正常WebView 容器限制了脚本执行反馈给 App 方或引导用户用外部浏览器按钮可聚焦但按空格没反应用了 a 标签但没处理键盘事件改用 button或补 keydown 处理 Enter 和空格点击后地址栏多了个井号href#触发了 hash 变化用 button或阻止默认行为点击后页面被一段文字替换javascript:URL 的脚本有返回值改用事件监听绑定表单里按回车提交了意外内容按钮未声明typebutton显式补上 type 属性6.3 几条不太写在文档里的经验第一条noscript不是万能的它能告诉你脚本没跑但不能告诉你为什么没跑。真正的原因是靠控制台和 Network 面板查出来的别指望加个 noscript 就算做完降级了。第二条现代前端框架构建出来的页面在 JS 被禁用时基本等于白屏这是架构决定的不是谁写错了。如果你的项目需要面向可能开着脚本拦截的用户群唯一靠谱的做法是关键路径用服务端渲染出可用的 HTML或者至少给一个不依赖脚本的轻量版本入口别指望前端补丁能救。第三条改造老项目时别一次性把所有javascript:void(0)都换掉。这类元素往往被旧的 JavaScript 代码用属性选择器绑着事件你只改 HTML 不改绑定逻辑按钮会直接失效。稳妥的做法是先把样式钩子和事件绑定解耦再一个一个替换替换一个验证一个。第四条给自己的项目加一条静态扫描规则禁止hrefjavascript:和href#出现在提交的代码里。现代工程链路里配一条正则扫描的成本很低远比上线后被人反馈按钮点了没反应要划算。最后分享一个小技巧。判断一个按钮到底是链接还是动作我有个特别简单的自我提问法这个元素用户会不会希望右键在新标签页打开如果会它就是链接用 a 配 href如果不会那它就是按钮用 button。这个问题问三遍基本就不会再写出javascript:void(0)了。我当年第一次被人这么问的时候才发现自己写了三年的链接其实一个都不该是链接。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门