保哥笔记

button按钮刷新页面的8种JavaScript写法实战

保哥写前端这些年,被问得最多的一个看似简单却暗藏玄机的问题,就是怎么用一个 button 按钮触发页面刷新。表面上看 location.reload() 一行代码就能搞定,但真正写进项目里,你会遇到强刷与软刷、GET表单与POST表单刷新、跨浏览器兼容、防止表单重复提交等一连串细节。

这一篇保哥把自己常用的八种刷新写法整理出来,并附上每种写法的适用场景、坑点、性能对比和个人推荐顺序,最后给出一套生产级别的代码模板和常见踩坑案例。

八种主流的 button 刷新页面写法概览

先把所有写法一次性铺开,方便你对照查阅。下面这段代码包含了八种常见做法,对应的场景在后文会逐一展开:

<!-- 写法一:最标准、推荐首选 -->
<input type="button" value="刷新">

<!-- 写法二:将 location 自身赋值给 location -->
<input type="button" value="刷新">

<!-- 写法三:使用 location.assign 传入当前地址 -->
<input type="button" value="刷新">

<!-- 写法四:execCommand 调用 refresh(IE 已弃用) -->
<input type="button" value="刷新">

<!-- 写法五:window.navigate(仅 IE 支持) -->
<input type="button" value="刷新">

<!-- 写法六:location.replace 不会留下历史记录 -->
<input type="button" value="刷新">

<!-- 写法七:window.open 在当前窗口打开自身 -->
<input type="button" value="刷新">

<!-- 写法八:execWB(22,1) 通过 WebBrowser 控件刷新(仅 IE) -->
<input type="button" value="刷新">

保哥强调一句:八种写法里只有第一、第三、第六、第七这四种是现代浏览器(Chrome、Edge、Firefox、Safari)都支持的,其余四种(写法二、四、五、八)要么依赖已经退场的IE,要么在严格模式下有隐患,生产代码请优先使用前四种

写法一 location.reload 的强刷与软刷

location.reload() 是最常见也最语义化的刷新写法,但它有一个鲜为人知的参数:

<button type="button">软刷新</button>
<button type="button">强制刷新(绕过缓存)</button>

传入 true 表示无视本地缓存,从服务器重新拉取所有资源,相当于按下 Ctrl+F5。注意 reload(true) 这个布尔参数虽然在 MDN 文档里被标记为非标准,但 Chrome、Firefox、Safari 直到目前都仍然支持,保哥在生产中用了七八年没遇到兼容问题。如果你需要严格遵循标准,可以改用下面的写法绕一圈:

function hardReload() {
  const url = new URL(location.href);
  url.searchParams.set('_t', Date.now());
  location.replace(url.toString());
}

通过给URL拼一个时间戳参数,让浏览器认为这是一个全新的资源,从而绕过缓存。这种做法在严格按照W3C规范的项目里更安全。如果你的页面已经使用了 query string 做业务逻辑,注意时间戳参数名要避免冲突——保哥习惯用 _t_r 这种带下划线前缀的临时参数名。

写法三和写法六 assign 与 replace 的关键差异

这两个方法都能把当前地址重新加载一次,但对浏览器历史栈的影响完全不同。

// assign 会在历史栈里压一条新记录
location.assign(location.href);

// replace 会把当前历史记录覆盖掉
location.replace(location.href);

保哥的判断标准是这样的:如果用户点刷新之后还希望按浏览器后退按钮回到当前页之前的状态,用 assign;如果你做的是表单提交成功页或支付结果页,不希望用户后退回来再次触发提交,用 replace。后者还有一个隐藏好处——它在某些浏览器里不会触发是否重新提交表单的弹窗。

性能层面两者基本没有差异,都会触发完整的网络请求。差别仅在历史栈和某些边缘行为(比如 referrer 头、前进后退按钮可用性)。保哥踩过一次坑:在支付回执页用了 assign,用户支付成功后点刷新,再按后退键又回到了支付页,重复支付了一次。从此之后所有"提交成功页"统一改用 replace

写法七 window.open 在 SPA 中的特殊用途

window.open(location.href, '_self') 看上去像是开一个新窗口然后变成自己,听着绕,但它在 SPA(单页应用)里有一个非常实用的场景:彻底卸载 React、Vue 的运行时状态。

function fullReset() {
  // 清掉本地状态,再用 _self 重新装载页面
  sessionStorage.clear();
  window.open(location.href, '_self');
}

location.reload() 在 SPA 里也能刷,但有些前端路由库会拦截 reload 事件,导致刷新行为被劫持。保哥见过 vue-router 加 history mode 的项目里,location.reload 被 service worker 拦截后只刷新了部分组件状态,造成"刷新了但又没完全刷新"的诡异 bug。window.open(..., '_self') 是浏览器原生导航,绕过了所有 JS 路由钩子,保哥在排查刷新无效的诡异 bug 时经常拿它当兜底。

另一个少有人知的用途是在 iframe 里触发父窗口刷新:

// 在 iframe 内部调用,刷新父窗口
window.parent.location.reload();

// 跨域 iframe 无法直接访问父级 location,要用 postMessage
window.parent.postMessage({ type: 'reload' }, '*');
// 父窗口监听
window.addEventListener('message', (e) => {
  if (e.data && e.data.type === 'reload') location.reload();
});

已经过时的 IE 专属写法不要再用

写法二、写法四、写法五、写法八都和 IE 浏览器有不解之缘。

location = location 利用了 JS 引擎对 location 属性赋值会重新加载的特性。它在 Chrome 里也能跑,但严格模式下会被 lint 工具标红,并且代码可读性极差,新人 review 完全看不懂意图。保哥的代码规范里直接把这种写法列入 ESLint 黑名单。

document.execCommand('Refresh') 来自 IE 的 contentEditable 时代,现在 execCommand 整体都被 W3C 标记为 deprecated,连富文本编辑都不推荐用它了,更别说刷新页面。Chrome 在 119 版本之后逐步停止支持大部分 execCommand 命令,依赖它的代码会逐渐失效。

window.navigate 是 IE 的私有 API,从 Edge 切换到 Chromium 内核之后已经彻底消失。如果你的代码里还有这一行,等于在新 Edge 里直接报 ReferenceError 中断后续脚本执行。

document.all.WebBrowser.ExecWB(22,1) 这一句更夸张,它依赖嵌入在页面里的 ActiveX WebBrowser 控件,只有 IE 才支持,并且需要降低安全设置才能跑。保哥的建议非常直接:如果你的代码库里还有这四种写法,请尽快迁移到 location.reload()。批量替换一行 sed 就能搞定:

find . -name "*.html" -o -name "*.js" | xargs grep -l "ExecWB\|window.navigate\|execCommand('Refresh')\|location = location" \
  | xargs sed -i.bak \
  -e "s/document.all.WebBrowser.ExecWB(22,1)/location.reload()/g" \
  -e "s/window.navigate(\([^)]*\))/location.assign(\1)/g" \
  -e "s/document.execCommand('Refresh')/location.reload()/g" \
  -e "s/location = location/location.reload()/g"

跑完之后逐个 diff 确认,没问题再删 .bak 文件。保哥用这个方法在一个 IE 时代的老项目里一次性清理掉了 200 多处过时写法。

用 button 刷新时常被忽视的安全细节

看上去人畜无害的刷新按钮,在以下几种场景里会埋雷。

第一个雷是 POST 表单的"是否重新提交"弹窗。如果当前页面是通过 POST 请求渲染的,调用 location.reload() 时浏览器会弹出确认框。解决办法是后端在 POST 处理完成后用 303 重定向到一个 GET URL(即 PRG 模式:Post-Redirect-Get),前端再刷这个 GET URL 就不会弹框了。这是后端 + 前端配合的事,纯前端无法绕开浏览器的安全机制。

第二个雷<button> 默认的 type。原生 <button> 标签如果没有显式声明 type="button",在表单里的默认行为是 type="submit",点击它不仅会刷新页面,还会顺带提交表单:

<!-- 错误:会触发表单提交 -->
<form action="/submit" method="post">
  <button>刷新</button>
</form>

<!-- 正确:明确 type="button" -->
<form action="/submit" method="post">
  <button type="button">刷新</button>
</form>

这个坑保哥见过太多次。最严重的一次是某电商后台的"刷新订单列表"按钮被嵌在筛选表单里,没写 type="button",结果点一次刷新会把所有筛选条件重新提交一遍,把列表重置回默认状态。用户疯狂吐槽"刷新按钮把我的筛选条件搞没了",前端排查了两天才定位到这一行。

第三个雷是事件委托。如果你用 jQuery 的 $(document).on('click', '#refresh', ...) 绑定了刷新逻辑,刷新本身会卸载所有 JS,绑定关系会重建,看似没问题。但如果你绑定时不小心写成 $('#refresh').on(...) 又叠加了内联 onclick,就会触发两次刷新,部分浏览器会进入刷新风暴。Chrome 有专门的"redirect loop detection"机制,触发后页面直接 ERR_TOO_MANY_REDIRECTS。

第四个雷是 onbeforeunload 拦截。如果当前页面注册了 window.addEventListener('beforeunload', fn) 用来"防止用户误关页面",刷新动作也会被这个钩子拦下来弹出确认框。要在你的刷新按钮里临时关闭这个拦截:

function safeReload() {
  window.onbeforeunload = null; // 关掉拦截
  location.reload();
}

性能与缓存对比

八种写法在性能层面差异其实很小,因为最终都走的是浏览器导航 API。但缓存策略上有一些细微差别,保哥整理成下表:

如果你的页面在 CDN 后面(CloudFlare、阿里云 CDN 等),请优先选择"带时间戳"的方案,否则即便客户端发了 no-cache 请求头,CDN 也可能直接返回边缘缓存,导致看到的还是旧内容。

保哥的推荐选型与代码模板

保哥日常项目里只用下面这两段模板,其它写法看到就改:

<!-- 普通刷新 -->
<button type="button" class="btn-refresh">
  刷新
</button>

<!-- 防缓存的硬刷新 -->
<button type="button" class="btn-refresh" id="hardReload">
  强制刷新
</button>
<script>
document.getElementById('hardReload').addEventListener('click', () => {
  const u = new URL(location.href);
  u.searchParams.set('_t', Date.now());
  location.replace(u.toString());
});
</script>

这两段模板覆盖了 95% 的场景:日常软刷用第一段,需要绕过 CDN 或浏览器缓存的支付回执页、报表页用第二段。剩下 5% 的特殊场景(嵌入在 iframe 里、跨源 postMessage 触发刷新等)单独再讨论。

在 React 项目里保哥的写法是:

function RefreshButton() {
  const handleClick = () => {
    window.onbeforeunload = null;
    window.location.reload();
  };
  return <button type="button"
}

在 Vue 3 项目里:

<template>
  <button type="button" @click="reload">刷新</button>
</template>
<script setup>
const reload = () => {
  window.onbeforeunload = null;
  location.reload();
};
</script>

常见问题解答

location.reload 会不会丢失表单里已经填好的内容?

看浏览器策略。Chrome 和 Firefox 默认会尝试恢复 input、textarea 的内容(基于 bfcache 和 form autofill),但 select、checkbox 不一定能保持选中。如果是关键表单,保哥建议把数据先写入 sessionStorage,刷新后回填,别指望浏览器替你兜底。具体做法:在 beforeunload 事件里序列化表单数据存 sessionStorage,DOMContentLoaded 时反序列化回填。这种模式保哥在大型 SaaS 后台里用了多年,用户体验比指望浏览器原生恢复要稳得多。

在 React 或 Vue 项目里用内联 行不行?

能跑,但不推荐。React 提倡用合成事件 onClick={() => location.reload()},Vue 用 @click="reload",这样能享受到框架的事件代理和测试便利。直接写在 HTML 里的内联事件会绕过框架的虚拟 DOM 体系,部分场景下会引发警告(React 会提示 use onClick instead of onclick),并且 SSR 渲染时可能出问题。如果你用 TypeScript 严格模式,内联 onclick 也无法享受类型检查的红利。

刷新按钮能不能加防抖,避免用户连点?

可以,而且应该加:

let refreshing = false;
document.querySelector('#refresh').addEventListener('click', () => {
  if (refreshing) return;
  refreshing = true;
  location.reload();
});

虽然刷新会卸载脚本,理论上没法连点,但用户在刷新尚未发起请求的那一瞬间多次点击,有些机型(特别是低端安卓)会触发两次导航,导致服务器被多余请求打到。加一个布尔锁是廉价且有效的做法。如果你想更彻底地防抖,可以配合 button 的 disabled 属性使用,点击后立即把按钮置灰。

location.reload(true) 真的能绕过 CDN 缓存吗?

绕不过强 CDN 缓存。reload(true) 影响的是浏览器自身缓存以及发往源站的请求头(会带 Cache-Control: no-cachePragma: no-cache),但是否被 CDN 尊重取决于 CDN 配置。CloudFlare 默认会忽略客户端的 no-cache 请求头,阿里云 CDN 可以在控制台配置是否尊重。保哥的经验是:要稳定绕过 CDN,唯一可靠的办法就是给 URL 加随机查询参数,让 CDN 当成新资源去回源。

移动端浏览器对这些写法的支持有差异吗?

主流移动浏览器(Chrome Android、Safari iOS、UC、QQ 浏览器、微信内置 WebView)对前四种写法都完全支持。比较坑的是某些定制 ROM 或国产 WebView,比如华为鸿蒙的部分版本对 location.replace 在 history.length 为 1 时有 bug,调用后页面卡住。保哥的兜底做法是在移动端统一只用 location.reload(),避开历史栈相关 API。微信 WebView 还有一个特殊行为:在 PWA 模式下 reload 会触发 service worker 重新注册,部分场景下会导致首屏白屏 200 到 500 毫秒,需要在 sw 里做缓存预热。

刷新按钮做成 a 标签 href="#" 行不行?

不推荐。javascript: 协议在严格 CSP(Content Security Policy)下会被拦截,并且 SEO 友好性差(爬虫不会执行 JS),还会被某些安全扫描工具标记为可疑代码。保哥的标准做法是 <button type="button"><a href="#">。前者语义更清晰,无障碍读屏软件也能正确朗读"按钮"而不是"链接"。

刷新按钮的无障碍设计要注意什么?

三个要点:1)必须用 <button> 元素而不是 <div> 模拟,前者天然支持键盘 Enter/Space 触发;2)加 aria-label="刷新页面" 让屏幕阅读器准确朗读功能;3)刷新过程中(比如带 loading 动画)加 aria-busy="true" 提示辅助技术当前正在加载。完整示例:<button type="button" aria-label="刷新页面" aria-busy="false">刷新</button>

SPA 应用里刷新会丢失路由状态吗?

会,因为刷新会让 SPA 从入口重新初始化。如果你用 React Router、Vue Router 这类 history mode 路由,需要在服务端配置 fallback 到 index.html,否则刷新非根路径会 404。状态层面,Redux/Vuex/Pinia 的 store 都会被清空,需要靠 redux-persist、pinia-plugin-persistedstate 之类的库把关键状态持久化到 localStorage。保哥的最佳实践是:用户登录态、当前路由 query、表单未提交内容三类必须持久化,其它非关键状态允许丢失。