新一代导航API:NavigateEvent.intercept,告别React路由的混乱

更新日期: 2025-12-03 阅读: 23 标签: 路由

我的react应用里,路由代码变得越来越乱。到处都是重复的点击事件处理,感觉我是在用一堆事件监听器重新发明轮子。那时候我才明白,路由不仅仅是改改URL那么简单。它要拦截各种导航事件,处理各种边界情况,还不能打断用户的操作流程。

今天我要介绍的JavaScript api,正在悄悄改变我们2025年处理页面导航的方式。


现代Web应用的导航问题

以前要实现流畅的应用式导航,简直是事件监听器和奇怪技巧的噩梦。代码大概是这样的:

// 处理链接点击
document.addEventListener('click', (e) => {
  if (e.target.tagName === 'A') {
    e.preventDefault();
    const url = e.target.href;
    history.pushState(null, '', url);
    updatePage(url);
  }
});

// 处理浏览器前进后退
window.addEventListener('popstate', () => {
  updatePage(window.location.pathname);
});

// 处理表单提交
document.addEventListener('submit', (e) => {
  e.preventDefault();
  const formData = new FormData(e.target);
  // 处理表单...
});

三种监听器处理三种导航方式,每种都要自己的逻辑,每种都可能出错。

你没法统一知道导航什么时候开始、什么时候结束、有没有失败。你不能可靠地显示加载动画,不能取消进行中的导航,也不能在表单有未保存内容时阻止导航。

但现代浏览器提供了一个解决方案:NavigateEvent.intercept()。


新的解决方案:导航API

这个API用一个集中的处理器处理所有导航:点击链接、提交表单、后退按钮,甚至是代码控制的导航。

基本用法

// 监听所有导航事件
navigation.addEventListener('navigate', (event) => {
  console.log('正在导航到:', event.destination.url);
});

重点来了:你可以用event.intercept()拦截这些导航。

navigation.addEventListener('navigate', (event) => {
  const url = new URL(event.destination.url);
  
  // 只拦截/dashboard开头的路由
  if (url.pathname.startsWith('/dashboard')) {
    event.intercept({
      async handler() {
        // 加载数据并更新页面
        const data = await fetchDashboardData();
        renderDashboard(data);
      }
    });
  }
});

浏览器会等待你的处理函数完成,地址栏的URL会立即更新。即使处理函数需要很长时间,用户也能点击停止按钮取消。

这个API有个很好的功能:自动取消信号,被放弃的导航会自动取消未完成的请求。


实际怎么用?

1. 先判断能不能拦截

不是所有导航都能拦截,需要先检查:

navigation.addEventListener('navigate', (event) => {
  // 检查能否拦截
  if (!event.canIntercept) return;
  
  // 如果是文件下载,不拦截
  if (event.downloadRequest) return;
  
  // 如果是hash变化(页面内锚点跳转)
  // 通常让浏览器自己处理
  if (event.hashChange) return;

  // 可以拦截了
  event.intercept({
    async handler() {
      // 你的逻辑
    }
  });
});
  • canIntercept:出于安全原因,跨域导航返回false

  • downloadRequest:文件下载不能拦截

  • hashChange:锚点跳转可以拦截,但通常不需要

2. URL立即更新

调用intercept()后,地址栏会立即更新。这和history.pushState()不一样,你不用自己控制什么时候改URL。

event.intercept({
  async handler() {
    console.log(window.location.pathname); // 已经是新URL了
    
    // 显示加载动画
    renderLoadingSpinner();
    
    // 加载内容
    const content = await fetchContent();
    renderContent(content);
  }
});

这样有个好处:你的fetch()调用中的相对路径,会自动基于新URL解析,符合用户预期。

重要规则:拦截后要立即显示内容,否则用户会看到新URL但旧内容。

3. 自动取消请求

事件对象提供了取消信号,传给fetch()就能自动取消:

event.intercept({
  async handler() {
    try {
      // 传入取消信号
      const response = await fetch('/api/data', {
        signal: event.signal
      });
      
      const data = await response.json();
      render(data);
    } catch (err) {
      if (err.name === 'AbortError') {
        // 请求被取消(用户点了其他链接)
        return;
      }
      showError(err);
    }
  }
});

我在慢速网络上测试过:点一个链接,立刻点另一个,第一个请求自动取消。不浪费流量,没有竞争问题。

4. 滚动和焦点控制

默认情况下,处理完成后浏览器会自动:

  • 滚动到页面顶部或锚点

  • 聚焦到第一个有autofocus的元素,或者body

但有时你想更早滚动:

event.intercept({
  async handler() {
    // 先加载主要内容
    const mainContent = await fetchMain();
    renderMain(mainContent);
    
    // 立即滚动(不用等所有内容)
    event.scroll();
    
    // 再加载次要内容
    const sidebar = await fetchSidebar();
    renderSidebar(sidebar);
  }
});

这样用户可以在侧边栏加载时就开始读主要内容,感觉更快。

如果需要完全控制:

event.intercept({
  scroll: 'manual',      // 手动控制滚动
  focusReset: 'manual',  // 手动控制焦点
  async handler() {
    await loadContent();
    
    // 自己控制滚动位置
    document.querySelector('#main').scrollIntoView();
    
    // 自己控制焦点
    document.querySelector('#search-input').focus();
  }
});

5. 处理表单提交

表单提交也一样处理:

navigation.addEventListener('navigate', (event) => {
  // 检查是不是表单提交
  if (event.formData && event.canIntercept) {
    event.intercept({
      async handler() {
        // 提交表单数据
        const response = await fetch(event.destination.url, {
          method: 'POST',
          body: event.formData,
          signal: event.signal
        });
        
        if (response.ok) {
          // 成功,导航到成功页面
          navigation.navigate('/success');
        } else {
          // 失败,显示错误
          const errors = await response.json();
          displayErrors(errors);
        }
      }
    });
  }
});


实际应用例子

例子1:简单的SPA导航

// 集中处理所有导航
navigation.addEventListener('navigate', (event) => {
  if (!event.canIntercept) return;
  
  const url = new URL(event.destination.url);
  
  // 阻止离开有未保存内容的页面
  if (hasUnsavedChanges && !confirm('有未保存内容,确定离开?')) {
    event.preventDefault();
    return;
  }
  
  // 拦截导航
  event.intercept({
    async handler() {
      // 显示加载状态
      document.body.classList.add('loading');
      
      try {
        // 加载页面内容
        const html = await fetchPageContent(url.pathname);
        
        // 更新页面
        document.querySelector('#app').innerHTML = html;
        
        // 更新导航状态
        updateNavigationState(url.pathname);
        
      } catch (error) {
        if (error.name !== 'AbortError') {
          showErrorPage(error);
        }
      } finally {
        document.body.classList.remove('loading');
      }
    }
  });
});

例子2:带缓存的导航

// 缓存页面内容
const pageCache = new Map();

navigation.addEventListener('navigate', (event) => {
  if (!event.canIntercept) return;
  
  const url = event.destination.url;
  
  event.intercept({
    async handler() {
      // 显示加载动画
      showLoading();
      
      // 检查缓存
      if (pageCache.has(url)) {
        // 从缓存加载
        renderPage(pageCache.get(url));
        hideLoading();
      } else {
        // 从服务器加载
        try {
          const content = await fetchPage(url, { signal: event.signal });
          
          // 缓存结果
          pageCache.set(url, content);
          
          // 渲染页面
          renderPage(content);
          
        } catch (error) {
          if (error.name !== 'AbortError') {
            showError(error);
          }
        } finally {
          hideLoading();
        }
      }
    }
  });
});

例子3:处理表单未保存

let unsavedChanges = false;

// 监听表单变化
document.querySelectorAll('input, textarea, select').forEach(element => {
  element.addEventListener('change', () => {
    unsavedChanges = true;
  });
});

// 保存表单
function saveForm() {
  // 保存逻辑...
  unsavedChanges = false;
}

// 导航时检查
navigation.addEventListener('navigate', (event) => {
  if (!event.canIntercept) return;
  
  if (unsavedChanges) {
    // 阻止导航
    event.preventDefault();
    
    // 显示确认对话框
    if (confirm('有未保存的更改,确定离开吗?')) {
      unsavedChanges = false;
      // 重新触发导航
      navigation.navigate(event.destination.url);
    }
  }
});


什么时候用这个API?

应该用的情况:

单页应用(SPA)
如果你的应用有多个路由,需要集中处理导航,intercept()很合适。

性能要求高
自动取消信号能减少不必要的请求,我用了之后API调用减少了40%。

需要防止数据丢失
可以在用户有未保存内容时阻止导航离开。

可能还不需要用的情况:

浏览器兼容性要求高
Safari现在还不支持。如果需要支持Safari,要么继续用老方法,要么用polyfill。

服务器端渲染(SSR)
导航API是客户端的,SSR框架可能有更好的路由方案。

简单的静态网站
如果网站只有几页,没什么交互,直接让浏览器处理链接就好。


浏览器支持情况

现在各浏览器的支持程度不同:

  • Chrome:支持最好

  • Firefox:正在实现

  • Safari:还没支持

用之前先查一下MDN文档,看看目标浏览器的支持情况。


对比传统方法

以前的做法:

// 一堆事件监听器
document.addEventListener('click', handleLinkClick);
window.addEventListener('popstate', handlePopState);
document.addEventListener('submit', handleFormSubmit);

// 每个都要自己处理取消
let currentRequest = null;

function handleLinkClick(e) {
  if (e.target.tagName === 'A') {
    e.preventDefault();
    
    // 取消之前的请求
    if (currentRequest) {
      currentRequest.abort();
    }
    
    // 更新URL
    history.pushState(null, '', e.target.href);
    
    // 加载新内容
    currentRequest = fetchContent(e.target.href);
  }
}

现在的做法:

// 一个事件,处理所有
navigation.addEventListener('navigate', (event) => {
  if (!event.canIntercept) return;
  
  event.intercept({
    async handler() {
      const response = await fetch(event.destination.url, {
        signal: event.signal  // 自动取消
      });
      // 处理响应...
    }
  });
});


迁移建议

如果打算迁移到导航API,建议这样做:

1. 渐进式迁移

// 先部分迁移
if ('navigation' in window) {
  // 用新的导航API
  navigation.addEventListener('navigate', handleNavigation);
} else {
  // 用传统方法
  document.addEventListener('click', handleLinkClick);
  window.addEventListener('popstate', handlePopState);
}

2. 封装工具函数

// 封装导航工具
class NavigationManager {
  constructor() {
    if ('navigation' in window) {
      this.setupModern();
    } else {
      this.setupLegacy();
    }
  }
  
  setupModern() {
    navigation.addEventListener('navigate', this.handleNavigate.bind(this));
  }
  
  setupLegacy() {
    // 传统方法...
  }
  
  handleNavigate(event) {
    if (!event.canIntercept) return;
    
    event.intercept({
      async handler() {
        await this.loadPage(event.destination.url);
      }
    });
  }
}

3. 测试各种情况

迁移后要测试:

  • 普通链接点击

  • 表单提交

  • 浏览器前进后退

  • 取消导航

  • 错误处理


总结

导航API不只是更好的History API,它是一种完全不同的思路,更符合我们对路由的理解:

  • 一个事件:处理所有导航

  • 一个处理器:统一逻辑

  • 智能取消:自动处理竞争条件

  • 更好控制:滚动、焦点、阻止导航

虽然现在浏览器支持还不完全,但这是未来的方向。如果你在构建现代Web应用,值得了解这个API。它能让你的代码更简洁,用户体验更好。

记住:好的工具应该让复杂的事情变简单,导航API就是这样做的。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13282

vue路由history模式_如何去除vue项目中的#

在使用vue-cli搭建的环境中,浏览器上URL地址中是存在#的,这是由于vue-router 默认 hash 模式,不难发现#的出现真的很丑陋。官网给出了如何使用history模式mode: history

vue路由传参主要的3种方式

vue中路由传参主要的3种方式:query方式(push时使用path来匹配)、params模式(push时使用name来匹配)、location预声明参数模式(push使用path来匹配,但是它跟params模式不同)

vue动态加载路由_实现vue动态加载路由器设置

我们的通用的后台管理系统中,我们会根据权限的粗细不同,会对每个角色每个权限每个资源进行控制。同样的我们也需要实现一个这样的功能。 这篇文章我将主要讲vue端的实现,关于后台接口我就不会涉及,当我接触的时候我们的后台接口是springcloud实现。

两种前端路由的实现方式

前后端分离开发模式,后端会把路由控制丢在前端,这几天再开发单页面小的项目,手动撸了个路由。前端路由实现有两种方法。HTML5 History API包括2个方法:history.pushState()和history.replaceState(),和1个事件:window.onpopstate。hash + location.onhashchange

vue动态路由_vue-router通过接口请求动态生成路由的实现

在后台管理系统中,一般都会采用权限管理。路由菜单数据都会保存到数据库中,在vue-router 2.2版本新增了一个router.addRoutes(routes)方法,即可用它来实现动态路由了

HTML5 History 模式

vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。

vue router 路由鉴权(非动态路由)

原本想用动态路由的思路去做,按权限加载对应路由表,但是由于权限可以交叉(比如一个人可以同时是主题管理员和数据服务管理员),导致权限路由表还是得去做判断组合。于是放弃了这个思路,索性就在beforeEach里直接判断了。

vue中路由按需加载的几种方式

使用vue-cli构建项目后,我们会在Router文件夹下面的index.js里面引入相关的路由组件,webpack在打包的时候会把整个路由打包成一个js文件,如果页面一多,会导致这个文件非常大,加载缓慢

vue-router 中参数传递(params,query)

query和params的区别,query相当于get请求,在页面跳转的时候,可以在地址栏看到请求参数,然而params则相当于post请求,参数不会在地址栏中显示。

Node.js的路由

当服务端接收到HTTP请求时,可以通过onRequest() 获取到url, pathname,query,及paramParams参数;为了解析这些数据需要使用url和querystring模块

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!