History对象

history对象概述

history对象保存了浏览器的历史记录,提供了导航功能。

基本属性

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

// 滚动恢复行为
console.log(history.scrollRestoration);  // "auto" 或 "manual"

// 当前状态
console.log(history.state);  // pushState设置的状态对象

滚动恢复

// 自动恢复滚动位置(默认)
history.scrollRestoration = "auto";

// 手动管理滚动位置
history.scrollRestoration = "manual";

window.addEventListener("load", function() {
    // 手动恢复滚动位置
    const scrollY = sessionStorage.getItem("scrollY");
    if (scrollY) {
        window.scrollTo(0, parseInt(scrollY));
    }
});

window.addEventListener("beforeunload", function() {
    sessionStorage.setItem("scrollY", window.scrollY);
});

历史记录导航

在历史记录中前进和后退。

前进后退

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

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

// 移动指定步数
history.go(-1);  // 后退一步(等同于back)
history.go(1);   // 前进一步(等同于forward)
history.go(-2);  // 后退两步
history.go(2);   // 前进两步
history.go(0);   // 刷新当前页面

// 检查是否可以前进/后退
// 没有直接API,但可以尝试
function canGoBack() {
    return history.length > 1;
}

导航按钮实现

// 后退按钮
const backButton = document.querySelector("#back");
backButton.addEventListener("click", function() {
    if (history.length > 1) {
        history.back();
    } else {
        location.href = "/";
    }
});

// 前进按钮
const forwardButton = document.querySelector("#forward");
forwardButton.addEventListener("click", function() {
    history.forward();
});

history.pushState

添加新的历史记录条目。

基本用法

// history.pushState(state, title, url)

// 添加历史记录
history.pushState(
    { page: 1 },           // 状态对象
    "Page 1",              // 标题(目前被忽略)
    "/page/1"              // URL
);

// 相对URL
history.pushState({ page: 2 }, "", "page/2");

// 完整URL(必须同源)
history.pushState({ page: 3 }, "", "/page/3");

// 查询字符串
history.pushState({ page: 4 }, "", "?page=4");

// Hash
history.pushState({ section: "intro" }, "", "#intro");

状态对象

// 状态对象可以是任何可序列化的值
history.pushState({ page: 1, sort: "name" }, "", "?page=1&sort=name");

// 获取状态
console.log(history.state);  // { page: 1, sort: "name" }

// 复杂状态
history.pushState({
    user: { id: 1, name: "东巴文" },
    filters: { status: "active", role: "admin" },
    timestamp: Date.now()
}, "", "/users/1");

SPA路由示例

// 简单的SPA路由
class Router {
    constructor() {
        this.routes = {};
        this.init();
    }
    
    init() {
        // 监听popstate事件
        window.addEventListener("popstate", (e) => {
            this.handleRoute(location.pathname);
        });
        
        // 处理初始路由
        this.handleRoute(location.pathname);
    }
    
    register(path, handler) {
        this.routes[path] = handler;
    }
    
    navigate(path, state = {}) {
        history.pushState(state, "", path);
        this.handleRoute(path);
    }
    
    handleRoute(path) {
        const handler = this.routes[path] || this.routes["/404"];
        if (handler) {
            handler(history.state);
        }
    }
}

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

router.register("/", () => showHomePage());
router.register("/about", () => showAboutPage());
router.register("/users", () => showUsersPage());

// 导航
router.navigate("/about");

history.replaceState

替换当前历史记录条目。

基本用法

// history.replaceState(state, title, url)

// 替换当前历史记录
history.replaceState(
    { page: 1 },
    "",
    "/page/1"
);

// 与pushState的区别
// pushState: 添加新记录,可以后退
// replaceState: 替换当前记录,不能后退到之前

使用场景

// 场景1:登录后替换URL
// 登录页URL: /login?return=/dashboard
function handleLoginSuccess() {
    const returnUrl = new URLSearchParams(location.search).get("return") || "/";
    history.replaceState({}, "", returnUrl);
    // 用户不能后退到登录页
}

// 场景2:清理URL参数
// URL: /search?q=js&page=1
function cleanUrl() {
    const url = new URL(location.href);
    url.searchParams.delete("temp");
    history.replaceState(history.state, "", url);
}

// 场景3:更新状态但不添加历史
function updatePage(page) {
    const url = new URL(location.href);
    url.searchParams.set("page", page);
    history.replaceState({ page }, "", url);
    loadData(page);
}

popstate事件

历史记录变化时触发的事件。

基本用法

window.addEventListener("popstate", function(e) {
    console.log("历史记录变化");
    console.log("状态:", e.state);
    console.log("当前URL:", location.href);
    
    // 根据状态更新页面
    if (e.state && e.state.page) {
        showPage(e.state.page);
    }
});

触发时机

// popstate在以下情况触发:
// 1. 点击浏览器后退按钮
// 2. 点击浏览器前进按钮
// 3. history.back()
// 4. history.forward()
// 5. history.go()

// popstate不会在以下情况触发:
// 1. history.pushState()
// 2. history.replaceState()
// 3. 页面加载

window.addEventListener("popstate", function(e) {
    console.log("popstate触发");
});

history.pushState({}, "", "/new");  // 不触发
history.back();                      // 触发

完整SPA示例

class SPARouter {
    constructor() {
        this.routes = new Map();
        this.currentRoute = null;
        this.init();
    }
    
    init() {
        // 监听popstate
        window.addEventListener("popstate", (e) => {
            this.dispatch(location.pathname, e.state);
        });
        
        // 监听链接点击
        document.addEventListener("click", (e) => {
            const link = e.target.closest("a[href]");
            if (link && this.isInternalLink(link)) {
                e.preventDefault();
                this.navigate(link.getAttribute("href"));
            }
        });
        
        // 初始路由
        this.dispatch(location.pathname, history.state);
    }
    
    isInternalLink(link) {
        return link.hostname === location.hostname &&
               !link.getAttribute("href").startsWith("#") &&
               !link.getAttribute("target");
    }
    
    route(path, handler) {
        this.routes.set(path, handler);
        return this;
    }
    
    navigate(path, state = {}) {
        history.pushState(state, "", path);
        this.dispatch(path, state);
    }
    
    replace(path, state = {}) {
        history.replaceState(state, "", path);
        this.dispatch(path, state);
    }
    
    dispatch(path, state) {
        const handler = this.routes.get(path);
        if (handler) {
            handler(state);
            this.currentRoute = path;
        } else {
            // 404处理
            const notFound = this.routes.get("*");
            if (notFound) notFound(state);
        }
    }
}

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

router
    .route("/", () => renderHome())
    .route("/about", () => renderAbout())
    .route("/users", (state) => renderUsers(state))
    .route("*", () => render404());

// 导航
router.navigate("/users", { page: 1 });

History API应用

实际开发中的应用场景。

无刷新页面切换

// Tab切换
const tabs = document.querySelectorAll(".tab");
const contents = document.querySelectorAll(".content");

tabs.forEach(tab => {
    tab.addEventListener("click", function() {
        const id = this.dataset.tab;
        
        // 更新UI
        tabs.forEach(t => t.classList.remove("active"));
        contents.forEach(c => c.classList.remove("active"));
        this.classList.add("active");
        document.getElementById(id).classList.add("active");
        
        // 更新URL
        history.pushState({ tab: id }, "", `#${id}`);
    });
});

// 恢复状态
window.addEventListener("popstate", function(e) {
    if (e.state && e.state.tab) {
        const tab = document.querySelector(`[data-tab="${e.state.tab}"]`);
        if (tab) tab.click();
    }
});

// 初始化
if (location.hash) {
    const tab = document.querySelector(`[data-tab="${location.hash.slice(1)}"]`);
    if (tab) tab.click();
}

分页导航

// 分页
function loadPage(page) {
    fetch(`/api/data?page=${page}`)
        .then(res => res.json())
        .then(data => {
            renderData(data);
            
            // 更新URL
            const url = new URL(location.href);
            url.searchParams.set("page", page);
            history.pushState({ page }, "", url);
        });
}

// 监听后退/前进
window.addEventListener("popstate", function(e) {
    if (e.state && e.state.page) {
        loadPage(e.state.page);
    }
});

// 初始化
const currentPage = new URLSearchParams(location.search).get("page") || 1;
loadPage(currentPage);

表单状态保存

// 保存表单状态
const form = document.querySelector("form");

function saveFormState() {
    const formData = new FormData(form);
    const state = Object.fromEntries(formData);
    history.replaceState({ form: state }, "");
}

// 输入时保存
form.addEventListener("input", debounce(saveFormState, 500));

// 恢复表单状态
function restoreFormState(state) {
    if (state && state.form) {
        Object.entries(state.form).forEach(([name, value]) => {
            const field = form.elements[name];
            if (field) field.value = value;
        });
    }
}

// 页面加载时恢复
restoreFormState(history.state);

// 后退时恢复
window.addEventListener("popstate", function(e) {
    restoreFormState(e.state);
});

面包屑导航

// 维护导航历史
const navigationHistory = [];

function navigate(path, title) {
    navigationHistory.push({
        path: location.pathname,
        title: document.title
    });
    
    history.pushState({ history: navigationHistory }, "", path);
    document.title = title;
    
    updateBreadcrumbs();
}

function updateBreadcrumbs() {
    const breadcrumbs = document.querySelector(".breadcrumbs");
    const history = history.state?.history || [];
    
    breadcrumbs.innerHTML = history
        .map(item => `<a href="${item.path}">${item.title}</a>`)
        .join(" > ");
}

下一步

掌握了History对象后,让我们继续学习:

  1. 其他BOM对象 - 学习Navigator和Screen
  2. 客户端存储 - 学习Cookie和Storage
  3. 回调函数 - 学习异步编程基础

东巴文(db-w.cn) - 让编程学习更简单

🎯 东巴文寄语:History API是构建单页应用(SPA)的核心技术,掌握pushState、replaceState和popstate事件,可以实现无刷新页面导航。在 db-w.cn,我们帮你深入理解前端路由原理!