JavaScript History API 完全指南

掌握浏览器历史管理:pushState、replaceState、popstate 事件与 SPA 路由实现

JavaScript History API 完全指南

History API 允许操作浏览器会话历史记录。本文详解其用法和 SPA 路由实现。

基础概念

History 对象

// 历史记录长度
console.log(history.length);

// 当前状态对象
console.log(history.state);

// 滚动恢复行为
history.scrollRestoration = 'manual'; // 或 'auto'

基本导航

// 后退一页
history.back();

// 前进一页
history.forward();

// 跳转到指定位置
history.go(-2); // 后退两页
history.go(1);  // 前进一页
history.go(0);  // 刷新当前页

核心方法

pushState

// 添加历史记录
history.pushState(state, title, url);

// 参数说明
// state: 与新历史记录关联的状态对象
// title: 新页面标题(大多数浏览器忽略)
// url: 新的 URL(可选,必须同源)

// 示例
history.pushState(
  { page: 1, query: 'javascript' },
  '',
  '/search?q=javascript&page=1'
);

// 状态对象大小限制(通常 640KB)
const largeState = { data: new Array(100000).fill('x') };
try {
  history.pushState(largeState, '', '/large');
} catch (e) {
  console.error('状态对象过大:', e);
}

replaceState

// 替换当前历史记录
history.replaceState(state, title, url);

// 示例:更新当前页面状态
history.replaceState(
  { ...history.state, scrollPosition: window.scrollY },
  '',
  location.href
);

// 用于修正 URL
if (location.pathname === '/old-path') {
  history.replaceState(null, '', '/new-path');
}

popstate 事件

// 监听历史导航
window.addEventListener('popstate', (event) => {
  console.log('导航发生');
  console.log('状态:', event.state);
  
  // 根据状态更新页面
  if (event.state) {
    updatePage(event.state);
  }
});

// 注意:pushState/replaceState 不触发 popstate
// 只有用户点击后退/前进按钮或调用 history.back()/forward()/go() 时触发

SPA 路由实现

基础路由器

class Router {
  constructor() {
    this.routes = new Map();
    this.currentRoute = null;
    
    window.addEventListener('popstate', (e) => {
      this.handleRoute(location.pathname, e.state);
    });
  }
  
  // 注册路由
  register(path, handler) {
    this.routes.set(path, handler);
    return this;
  }
  
  // 导航到路径
  navigate(path, state = {}) {
    if (path === location.pathname) return;
    
    history.pushState(state, '', path);
    this.handleRoute(path, state);
  }
  
  // 替换当前路由
  replace(path, state = {}) {
    history.replaceState(state, '', path);
    this.handleRoute(path, state);
  }
  
  // 处理路由
  handleRoute(path, state) {
    // 精确匹配
    if (this.routes.has(path)) {
      this.currentRoute = path;
      this.routes.get(path)(state);
      return;
    }
    
    // 参数路由匹配
    for (const [pattern, handler] of this.routes) {
      const params = this.matchRoute(pattern, path);
      if (params) {
        this.currentRoute = path;
        handler({ ...state, params });
        return;
      }
    }
    
    // 404 处理
    if (this.routes.has('*')) {
      this.routes.get('*')(state);
    }
  }
  
  // 路由参数匹配
  matchRoute(pattern, path) {
    const patternParts = pattern.split('/');
    const pathParts = path.split('/');
    
    if (patternParts.length !== pathParts.length) {
      return null;
    }
    
    const params = {};
    
    for (let i = 0; i < patternParts.length; i++) {
      if (patternParts[i].startsWith(':')) {
        params[patternParts[i].slice(1)] = pathParts[i];
      } else if (patternParts[i] !== pathParts[i]) {
        return null;
      }
    }
    
    return params;
  }
  
  // 初始化
  init() {
    this.handleRoute(location.pathname, history.state);
  }
}

// 使用
const router = new Router();

router
  .register('/', () => showHome())
  .register('/about', () => showAbout())
  .register('/users/:id', ({ params }) => showUser(params.id))
  .register('*', () => show404())
  .init();

带中间件的路由器

class AdvancedRouter {
  constructor() {
    this.routes = [];
    this.middlewares = [];
    this.beforeHooks = [];
    this.afterHooks = [];
    
    window.addEventListener('popstate', (e) => {
      this.resolve(location.pathname, e.state);
    });
  }
  
  // 添加中间件
  use(middleware) {
    this.middlewares.push(middleware);
    return this;
  }
  
  // 导航守卫
  beforeEach(hook) {
    this.beforeHooks.push(hook);
    return this;
  }
  
  afterEach(hook) {
    this.afterHooks.push(hook);
    return this;
  }
  
  // 注册路由
  route(path, handler, meta = {}) {
    const regex = this.pathToRegex(path);
    this.routes.push({ path, regex, handler, meta });
    return this;
  }
  
  // 路径转正则
  pathToRegex(path) {
    const pattern = path
      .replace(/\//g, '\\/')
      .replace(/:(\w+)/g, '(?<$1>[^/]+)')
      .replace(/\*/g, '.*');
    return new RegExp(`^${pattern}$`);
  }
  
  // 导航
  async navigate(path, state = {}) {
    const from = { path: location.pathname, state: history.state };
    const to = { path, state };
    
    // 执行前置守卫
    for (const hook of this.beforeHooks) {
      const result = await hook(to, from);
      if (result === false) return;
      if (typeof result === 'string') {
        return this.navigate(result, state);
      }
    }
    
    history.pushState(state, '', path);
    await this.resolve(path, state);
    
    // 执行后置守卫
    for (const hook of this.afterHooks) {
      await hook(to, from);
    }
  }
  
  // 解析路由
  async resolve(path, state) {
    // 执行中间件
    const context = { path, state, params: {} };
    
    for (const middleware of this.middlewares) {
      await middleware(context);
    }
    
    // 匹配路由
    for (const route of this.routes) {
      const match = path.match(route.regex);
      if (match) {
        context.params = match.groups || {};
        context.meta = route.meta;
        await route.handler(context);
        return;
      }
    }
  }
  
  init() {
    this.resolve(location.pathname, history.state);
  }
}

// 使用
const router = new AdvancedRouter();

// 中间件
router.use(async (ctx) => {
  console.log('访问:', ctx.path);
  ctx.startTime = Date.now();
});

// 导航守卫
router.beforeEach(async (to, from) => {
  if (to.path.startsWith('/admin') && !isLoggedIn()) {
    return '/login';
  }
});

router.afterEach((to, from) => {
  // 页面访问统计
  analytics.pageView(to.path);
});

// 路由
router
  .route('/', homeHandler)
  .route('/users/:id', userHandler, { requiresAuth: true })
  .init();

链接拦截

class LinkInterceptor {
  constructor(router) {
    this.router = router;
    this.init();
  }
  
  init() {
    document.addEventListener('click', (e) => {
      const link = e.target.closest('a');
      if (!link) return;
      
      // 检查是否应该拦截
      if (this.shouldIntercept(link)) {
        e.preventDefault();
        this.router.navigate(link.pathname + link.search);
      }
    });
  }
  
  shouldIntercept(link) {
    // 外部链接
    if (link.origin !== location.origin) return false;
    
    // 新标签页打开
    if (link.target === '_blank') return false;
    
    // 下载链接
    if (link.hasAttribute('download')) return false;
    
    // 明确要求不拦截
    if (link.dataset.noIntercept !== undefined) return false;
    
    // 特殊协议
    if (!/^https?:$/.test(link.protocol)) return false;
    
    return true;
  }
}

// 使用
new LinkInterceptor(router);

状态管理

页面状态持久化

class PageStateManager {
  constructor() {
    this.stateKey = 'pageState';
    
    window.addEventListener('popstate', (e) => {
      this.restore(e.state);
    });
    
    window.addEventListener('beforeunload', () => {
      this.save();
    });
  }
  
  // 保存状态
  save() {
    const state = {
      scrollX: window.scrollX,
      scrollY: window.scrollY,
      formData: this.getFormData(),
      timestamp: Date.now()
    };
    
    history.replaceState(
      { ...history.state, [this.stateKey]: state },
      ''
    );
  }
  
  // 恢复状态
  restore(historyState) {
    const state = historyState?.[this.stateKey];
    if (!state) return;
    
    // 恢复滚动位置
    requestAnimationFrame(() => {
      window.scrollTo(state.scrollX, state.scrollY);
    });
    
    // 恢复表单数据
    if (state.formData) {
      this.restoreFormData(state.formData);
    }
  }
  
  getFormData() {
    const forms = document.querySelectorAll('form[data-persist]');
    const data = {};
    
    forms.forEach((form, index) => {
      const formData = new FormData(form);
      data[index] = Object.fromEntries(formData.entries());
    });
    
    return data;
  }
  
  restoreFormData(data) {
    const forms = document.querySelectorAll('form[data-persist]');
    
    forms.forEach((form, index) => {
      if (data[index]) {
        Object.entries(data[index]).forEach(([name, value]) => {
          const input = form.elements[name];
          if (input) input.value = value;
        });
      }
    });
  }
}

const stateManager = new PageStateManager();

导航历史栈

class NavigationStack {
  constructor(maxSize = 50) {
    this.maxSize = maxSize;
    this.stack = [];
    this.currentIndex = -1;
    
    // 初始化
    this.push(location.pathname, history.state);
    
    window.addEventListener('popstate', (e) => {
      this.onPopState(e);
    });
  }
  
  push(path, state) {
    // 移除当前位置之后的记录
    this.stack = this.stack.slice(0, this.currentIndex + 1);
    
    // 添加新记录
    this.stack.push({ path, state, timestamp: Date.now() });
    this.currentIndex++;
    
    // 限制大小
    if (this.stack.length > this.maxSize) {
      this.stack.shift();
      this.currentIndex--;
    }
  }
  
  onPopState(event) {
    const path = location.pathname;
    
    // 判断是前进还是后退
    if (this.currentIndex > 0 && 
        this.stack[this.currentIndex - 1]?.path === path) {
      this.currentIndex--;
    } else if (this.currentIndex < this.stack.length - 1 &&
               this.stack[this.currentIndex + 1]?.path === path) {
      this.currentIndex++;
    }
  }
  
  canGoBack() {
    return this.currentIndex > 0;
  }
  
  canGoForward() {
    return this.currentIndex < this.stack.length - 1;
  }
  
  getHistory() {
    return [...this.stack];
  }
  
  getCurrent() {
    return this.stack[this.currentIndex];
  }
}

const navStack = new NavigationStack();

实际应用场景

无限滚动分页

class InfiniteScrollWithHistory {
  constructor(options) {
    this.options = {
      container: document.body,
      loadMore: async () => [],
      itemsPerPage: 20,
      ...options
    };
    
    this.page = this.getPageFromUrl();
    this.items = [];
    
    this.init();
  }
  
  getPageFromUrl() {
    const params = new URLSearchParams(location.search);
    return parseInt(params.get('page')) || 1;
  }
  
  updateUrl(page) {
    const url = new URL(location.href);
    url.searchParams.set('page', page);
    
    history.replaceState(
      { page, scrollY: window.scrollY },
      '',
      url.toString()
    );
  }
  
  async init() {
    // 加载初始数据
    for (let i = 1; i <= this.page; i++) {
      const items = await this.options.loadMore(i);
      this.items.push(...items);
    }
    
    this.render();
    
    // 恢复滚动位置
    if (history.state?.scrollY) {
      window.scrollTo(0, history.state.scrollY);
    }
    
    // 监听滚动
    this.setupInfiniteScroll();
    
    // 监听历史导航
    window.addEventListener('popstate', (e) => {
      if (e.state?.page) {
        this.page = e.state.page;
        window.scrollTo(0, e.state.scrollY || 0);
      }
    });
  }
  
  setupInfiniteScroll() {
    const observer = new IntersectionObserver(async (entries) => {
      if (entries[0].isIntersecting) {
        this.page++;
        const newItems = await this.options.loadMore(this.page);
        this.items.push(...newItems);
        this.render();
        this.updateUrl(this.page);
      }
    });
    
    observer.observe(document.querySelector('#load-more-trigger'));
  }
  
  render() {
    // 渲染列表
  }
}

模态框历史管理

class ModalHistoryManager {
  constructor() {
    this.modals = new Map();
    this.activeModals = [];
    
    window.addEventListener('popstate', (e) => {
      this.handlePopState(e);
    });
  }
  
  register(id, modal) {
    this.modals.set(id, modal);
  }
  
  open(id, data = {}) {
    const modal = this.modals.get(id);
    if (!modal) return;
    
    // 添加历史记录
    history.pushState(
      { modalId: id, modalData: data },
      '',
      `${location.pathname}?modal=${id}`
    );
    
    this.activeModals.push(id);
    modal.show(data);
  }
  
  close(id) {
    const modal = this.modals.get(id);
    if (!modal) return;
    
    // 后退历史
    if (this.activeModals.includes(id)) {
      history.back();
    }
  }
  
  handlePopState(event) {
    const state = event.state;
    
    // 关闭当前模态框
    if (this.activeModals.length > 0) {
      const currentModalId = this.activeModals.pop();
      const modal = this.modals.get(currentModalId);
      modal?.hide();
    }
    
    // 如果是打开模态框的状态
    if (state?.modalId) {
      const modal = this.modals.get(state.modalId);
      if (modal) {
        this.activeModals.push(state.modalId);
        modal.show(state.modalData);
      }
    }
  }
}

// 使用
const modalManager = new ModalHistoryManager();

modalManager.register('userProfile', {
  show: (data) => document.getElementById('userModal').classList.add('active'),
  hide: () => document.getElementById('userModal').classList.remove('active')
});

// 打开模态框
document.querySelector('.open-modal').addEventListener('click', () => {
  modalManager.open('userProfile', { userId: 123 });
});

标签页导航

class TabNavigation {
  constructor(container) {
    this.container = container;
    this.tabs = container.querySelectorAll('[data-tab]');
    this.panels = container.querySelectorAll('[data-panel]');
    
    this.init();
  }
  
  init() {
    // 从 URL 恢复状态
    const hash = location.hash.slice(1);
    if (hash) {
      this.activate(hash, false);
    }
    
    // 监听标签点击
    this.tabs.forEach(tab => {
      tab.addEventListener('click', (e) => {
        e.preventDefault();
        const tabId = tab.dataset.tab;
        this.activate(tabId, true);
      });
    });
    
    // 监听历史导航
    window.addEventListener('popstate', () => {
      const hash = location.hash.slice(1);
      this.activate(hash || this.tabs[0].dataset.tab, false);
    });
  }
  
  activate(tabId, updateHistory = true) {
    // 更新标签状态
    this.tabs.forEach(tab => {
      tab.classList.toggle('active', tab.dataset.tab === tabId);
    });
    
    // 更新面板显示
    this.panels.forEach(panel => {
      panel.classList.toggle('active', panel.dataset.panel === tabId);
    });
    
    // 更新 URL
    if (updateHistory) {
      history.pushState({ tab: tabId }, '', `#${tabId}`);
    }
  }
}

// 使用
new TabNavigation(document.querySelector('.tabs-container'));

最佳实践总结

History API 最佳实践:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   状态管理                                          │
│   ├── 保持状态对象精简                             │
│   ├── 序列化敏感数据                               │
│   ├── 合理使用 replaceState                        │
│   └── 处理状态恢复                                 │
│                                                     │
│   路由设计                                          │
│   ├── 使用语义化 URL                               │
│   ├── 支持直接访问                                 │
│   ├── 处理 404 情况                                │
│   └── 实现路由守卫                                 │
│                                                     │
│   用户体验                                          │
│   ├── 保持后退按钮可用                             │
│   ├── 恢复滚动位置                                 │
│   ├── 避免重复历史记录                             │
│   └── 处理外部链接                                 │
│                                                     │
└─────────────────────────────────────────────────────┘
方法用途触发 popstate
pushState添加历史记录
replaceState替换当前记录
back/forward/go导航历史

掌握 History API,构建流畅的单页应用导航体验。