历史记录API

历史记录API概述

历史记录API(History API)是HTML5提供的接口,允许Web应用与浏览器历史记录进行交互。通过这个API,可以在不刷新页面的情况下修改浏览器地址栏URL,实现单页应用(SPA)的路由功能。

东巴文(db-w.cn) 认为:历史记录API是现代单页应用的核心技术之一,让Web应用具备了类似原生应用的流畅体验。

历史记录API特点

核心特点

特点 说明
无刷新导航 修改URL而不刷新页面
历史管理 添加、替换、遍历历史记录
状态保存 可以保存状态对象
前进后退 支持浏览器前进后退按钮

History对象

// History对象属性
console.log(window.history.length);  // 历史记录数量

// History对象方法
window.history.back();      // 后退
window.history.forward();   // 前进
window.history.go(-1);      // 后退一步
window.history.go(1);       // 前进一步
window.history.go(0);       // 刷新当前页面

pushState方法

基本用法

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

// 参数说明
// state: 状态对象,可以存储任意数据
// title: 标题(目前大多数浏览器忽略此参数)
// url: 新的URL(必须同源)

// 示例
history.pushState(
    { page: 1, name: '首页' },
    '首页',
    '/home'
);

完整示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>pushState示例</title>
</head>
<body>
    <nav>
        <a href="/home" data-page="home">首页</a>
        <a href="/about" data-page="about">关于</a>
        <a href="/contact" data-page="contact">联系</a>
    </nav>
    
    <div id="content"></div>
    
    <script>
        const content = document.getElementById('content');
        
        // 页面内容
        const pages = {
            home: '<h1>首页</h1><p>欢迎访问首页</p>',
            about: '<h1>关于</h1><p>这是关于页面</p>',
            contact: '<h1>联系</h1><p>这是联系页面</p>'
        };
        
        // 导航点击事件
        document.querySelectorAll('nav a').forEach(link => {
            link.addEventListener('click', function(e) {
                e.preventDefault();
                
                const page = this.dataset.page;
                const url = this.getAttribute('href');
                
                // 添加历史记录
                history.pushState({ page }, '', url);
                
                // 更新内容
                content.innerHTML = pages[page];
            });
        });
        
        // 处理前进后退
        window.addEventListener('popstate', function(e) {
            if (e.state) {
                content.innerHTML = pages[e.state.page];
            }
        });
        
        // 初始加载
        content.innerHTML = pages.home;
    </script>
</body>
</html>

replaceState方法

基本用法

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

// 示例:替换当前记录
history.replaceState(
    { page: 'new-page' },
    '新页面',
    '/new-page'
);

pushState vs replaceState

// pushState: 添加新记录
// 历史记录: [page1, page2, page3] -> [page1, page2, page3, page4]
history.pushState({ page: 4 }, '', '/page4');

// replaceState: 替换当前记录
// 历史记录: [page1, page2, page3] -> [page1, page2, page4]
history.replaceState({ page: 4 }, '', '/page4');

东巴文点评pushState添加新记录,用户可以后退;replaceState替换当前记录,用户不能后退到之前的页面。

popstate事件

事件触发

// 监听历史记录变化
window.addEventListener('popstate', function(e) {
    console.log('状态对象:', e.state);
    console.log('当前URL:', location.href);
    
    // 根据状态更新页面
    if (e.state) {
        updatePage(e.state);
    }
});

// 注意:pushState和replaceState不会触发popstate事件
// popstate事件只在浏览器前进后退时触发

完整路由示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>历史记录API路由示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        
        nav {
            background: #667eea;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
        }
        
        nav a {
            color: white;
            text-decoration: none;
            margin-right: 15px;
            padding: 5px 10px;
            border-radius: 3px;
            transition: background 0.3s;
        }
        
        nav a:hover,
        nav a.active {
            background: rgba(255, 255, 255, 0.2);
        }
        
        #content {
            padding: 20px;
            background: #f5f5f5;
            border-radius: 5px;
            min-height: 300px;
        }
        
        .page {
            animation: fadeIn 0.3s;
        }
        
        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }
        
        .info {
            margin-top: 20px;
            padding: 10px;
            background: #fff3cd;
            border-radius: 5px;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <h1>历史记录API路由示例</h1>
    
    <nav>
        <a href="/" data-route="home">首页</a>
        <a href="/about" data-route="about">关于</a>
        <a href="/products" data-route="products">产品</a>
        <a href="/contact" data-route="contact">联系</a>
    </nav>
    
    <div id="content"></div>
    
    <div class="info">
        <p>当前URL: <span id="currentUrl"></span></p>
        <p>历史记录数: <span id="historyLength"></span></p>
    </div>
    
    <script>
        // 路由配置
        const routes = {
            home: {
                title: '首页',
                content: `
                    <div class="page">
                        <h2>欢迎来到首页</h2>
                        <p>这是一个使用History API实现的单页应用路由示例。</p>
                        <ul>
                            <li>点击导航链接查看不同页面</li>
                            <li>使用浏览器前进后退按钮</li>
                            <li>观察URL变化</li>
                        </ul>
                    </div>
                `
            },
            about: {
                title: '关于',
                content: `
                    <div class="page">
                        <h2>关于我们</h2>
                        <p>东巴文(db-w.cn)致力于提供高质量的编程教程。</p>
                        <p>我们的使命是让编程学习更简单、更有趣!</p>
                    </div>
                `
            },
            products: {
                title: '产品',
                content: `
                    <div class="page">
                        <h2>产品列表</h2>
                        <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px;">
                            <div style="padding: 15px; background: white; border-radius: 5px; text-align: center;">
                                <div style="font-size: 48px;">📚</div>
                                <h3>HTML教程</h3>
                                <p>从基础到进阶</p>
                            </div>
                            <div style="padding: 15px; background: white; border-radius: 5px; text-align: center;">
                                <div style="font-size: 48px;">🎨</div>
                                <h3>CSS教程</h3>
                                <p>美化你的网页</p>
                            </div>
                            <div style="padding: 15px; background: white; border-radius: 5px; text-align: center;">
                                <div style="font-size: 48px;"></div>
                                <h3>JavaScript教程</h3>
                                <p>让网页动起来</p>
                            </div>
                        </div>
                    </div>
                `
            },
            contact: {
                title: '联系',
                content: `
                    <div class="page">
                        <h2>联系我们</h2>
                        <form style="max-width: 400px;">
                            <div style="margin-bottom: 15px;">
                                <label>姓名:</label><br>
                                <input type="text" style="width: 100%; padding: 8px; margin-top: 5px;">
                            </div>
                            <div style="margin-bottom: 15px;">
                                <label>邮箱:</label><br>
                                <input type="email" style="width: 100%; padding: 8px; margin-top: 5px;">
                            </div>
                            <div style="margin-bottom: 15px;">
                                <label>消息:</label><br>
                                <textarea style="width: 100%; padding: 8px; margin-top: 5px;" rows="4"></textarea>
                            </div>
                            <button type="button" style="padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer;">
                                发送
                            </button>
                        </form>
                    </div>
                `
            }
        };
        
        // 路由类
        class Router {
            constructor() {
                this.routes = routes;
                this.contentEl = document.getElementById('content');
                this.init();
            }
            
            init() {
                // 监听导航点击
                document.querySelectorAll('nav a[data-route]').forEach(link => {
                    link.addEventListener('click', (e) => {
                        e.preventDefault();
                        const route = e.target.dataset.route;
                        this.navigate(route);
                    });
                });
                
                // 监听历史记录变化
                window.addEventListener('popstate', (e) => {
                    if (e.state) {
                        this.render(e.state.route);
                    } else {
                        this.render('home');
                    }
                });
                
                // 初始渲染
                const initialRoute = this.getRouteFromURL();
                this.navigate(initialRoute, true);
            }
            
            navigate(route, replace = false) {
                const url = route === 'home' ? '/' : `/${route}`;
                const state = { route };
                
                if (replace) {
                    history.replaceState(state, '', url);
                } else {
                    history.pushState(state, '', url);
                }
                
                this.render(route);
            }
            
            render(route) {
                const routeConfig = this.routes[route];
                
                if (routeConfig) {
                    document.title = routeConfig.title + ' - 东巴文';
                    this.contentEl.innerHTML = routeConfig.content;
                    
                    // 更新导航激活状态
                    this.updateNavActive(route);
                    
                    // 更新信息显示
                    this.updateInfo();
                }
            }
            
            updateNavActive(route) {
                document.querySelectorAll('nav a[data-route]').forEach(link => {
                    if (link.dataset.route === route) {
                        link.classList.add('active');
                    } else {
                        link.classList.remove('active');
                    }
                });
            }
            
            updateInfo() {
                document.getElementById('currentUrl').textContent = location.href;
                document.getElementById('historyLength').textContent = history.length;
            }
            
            getRouteFromURL() {
                const path = location.pathname;
                const route = path.substring(1) || 'home';
                return this.routes[route] ? route : 'home';
            }
        }
        
        // 初始化路由
        const router = new Router();
    </script>
</body>
</html>

状态管理

状态对象

// 保存复杂状态
history.pushState({
    page: 'products',
    category: 'electronics',
    filters: {
        price: '100-500',
        brand: 'apple'
    },
    scrollPosition: 500
}, '', '/products/electronics');

// 读取状态
window.addEventListener('popstate', function(e) {
    if (e.state) {
        const { page, category, filters, scrollPosition } = e.state;
        
        // 恢复页面状态
        loadPage(page, category);
        applyFilters(filters);
        window.scrollTo(0, scrollPosition);
    }
});

状态序列化

// 状态对象会被序列化,注意限制
const state = {
    id: 123,
    name: '产品名称',
    timestamp: Date.now()
};

// 保存状态
history.pushState(state, '', '/product/123');

// 注意:状态对象有大小限制(通常640KB)
// 避免保存大量数据或循环引用

东巴文点评:状态对象应该只保存必要的信息,大数据应该存储在IndexedDB或服务器。

综合示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>历史记录API综合示例 - 东巴文</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 1000px;
            margin: 0 auto;
            padding: 20px;
            background: #f5f5f5;
        }
        
        h1 {
            text-align: center;
            color: #333;
        }
        
        .section {
            margin: 20px 0;
            padding: 20px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        }
        
        .section h2 {
            margin-top: 0;
            color: #667eea;
        }
        
        /* 导航样式 */
        .main-nav {
            display: flex;
            gap: 10px;
            padding: 15px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 10px;
            margin-bottom: 20px;
        }
        
        .main-nav a {
            color: white;
            text-decoration: none;
            padding: 10px 20px;
            border-radius: 5px;
            transition: background 0.3s;
        }
        
        .main-nav a:hover,
        .main-nav a.active {
            background: rgba(255, 255, 255, 0.2);
        }
        
        /* 内容区域 */
        .main-content {
            background: white;
            padding: 30px;
            border-radius: 10px;
            min-height: 400px;
            animation: fadeIn 0.3s;
        }
        
        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }
        
        /* 分页样式 */
        .pagination {
            display: flex;
            justify-content: center;
            gap: 10px;
            margin-top: 20px;
        }
        
        .pagination button {
            padding: 8px 16px;
            border: 1px solid #ddd;
            background: white;
            cursor: pointer;
            border-radius: 5px;
            transition: all 0.3s;
        }
        
        .pagination button:hover {
            background: #667eea;
            color: white;
            border-color: #667eea;
        }
        
        .pagination button.active {
            background: #667eea;
            color: white;
            border-color: #667eea;
        }
        
        /* 产品列表 */
        .product-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
            gap: 20px;
        }
        
        .product-card {
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 15px;
            text-align: center;
            transition: all 0.3s;
        }
        
        .product-card:hover {
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            transform: translateY(-5px);
        }
        
        .product-icon {
            font-size: 64px;
            margin-bottom: 10px;
        }
        
        .product-name {
            font-weight: bold;
            margin-bottom: 5px;
        }
        
        .product-price {
            color: #667eea;
            font-weight: bold;
            font-size: 18px;
        }
        
        /* 历史记录信息 */
        .history-info {
            display: flex;
            gap: 20px;
            padding: 15px;
            background: #f9f9f9;
            border-radius: 5px;
            margin-top: 20px;
        }
        
        .history-info div {
            flex: 1;
            text-align: center;
        }
        
        .history-info strong {
            display: block;
            margin-bottom: 5px;
            color: #667eea;
        }
        
        /* 标签页样式 */
        .tabs {
            display: flex;
            border-bottom: 2px solid #ddd;
            margin-bottom: 20px;
        }
        
        .tab {
            padding: 10px 20px;
            cursor: pointer;
            border-bottom: 2px solid transparent;
            margin-bottom: -2px;
            transition: all 0.3s;
        }
        
        .tab:hover {
            color: #667eea;
        }
        
        .tab.active {
            color: #667eea;
            border-bottom-color: #667eea;
        }
        
        .tab-content {
            display: none;
        }
        
        .tab-content.active {
            display: block;
        }
        
        /* 按钮样式 */
        .btn {
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            transition: all 0.3s;
        }
        
        .btn-primary {
            background: #667eea;
            color: white;
        }
        
        .btn-primary:hover {
            background: #5568d3;
        }
        
        .btn-secondary {
            background: #6c757d;
            color: white;
        }
        
        .btn-secondary:hover {
            background: #5a6268;
        }
    </style>
</head>
<body>
    <h1>历史记录API综合示例</h1>
    
    <!-- 导航 -->
    <nav class="main-nav">
        <a href="/" data-route="home">首页</a>
        <a href="/products" data-route="products">产品</a>
        <a href="/tabs" data-route="tabs">标签页</a>
        <a href="/about" data-route="about">关于</a>
    </nav>
    
    <!-- 主内容 -->
    <div id="mainContent" class="main-content"></div>
    
    <!-- 历史记录信息 -->
    <div class="history-info">
        <div>
            <strong>当前URL</strong>
            <span id="currentUrl"></span>
        </div>
        <div>
            <strong>历史记录数</strong>
            <span id="historyLength"></span>
        </div>
        <div>
            <strong>当前状态</strong>
            <span id="currentState"></span>
        </div>
    </div>
    
    <script>
        // 应用状态
        const appState = {
            products: {
                page: 1,
                perPage: 4,
                total: 12
            },
            tabs: {
                activeTab: 'tab1'
            }
        };
        
        // 产品数据
        const products = [
            { id: 1, name: 'HTML教程', price: 99, icon: '📚' },
            { id: 2, name: 'CSS教程', price: 99, icon: '🎨' },
            { id: 3, name: 'JavaScript教程', price: 149, icon: '⚡' },
            { id: 4, name: 'Vue教程', price: 199, icon: '💚' },
            { id: 5, name: 'React教程', price: 199, icon: '💙' },
            { id: 6, name: 'Node.js教程', price: 179, icon: '💚' },
            { id: 7, name: 'Python教程', price: 149, icon: '🐍' },
            { id: 8, name: '数据库教程', price: 129, icon: '🗄️' },
            { id: 9, name: 'Git教程', price: 79, icon: '📦' },
            { id: 10, name: '算法教程', price: 199, icon: '🧮' },
            { id: 11, name: '网络安全教程', price: 169, icon: '🔒' },
            { id: 12, name: 'AI教程', price: 249, icon: '🤖' }
        ];
        
        // 路由配置
        const routes = {
            home: {
                title: '首页',
                render: renderHome
            },
            products: {
                title: '产品',
                render: renderProducts
            },
            tabs: {
                title: '标签页',
                render: renderTabs
            },
            about: {
                title: '关于',
                render: renderAbout
            }
        };
        
        // 渲染首页
        function renderHome() {
            return `
                <div style="text-align: center; padding: 50px 0;">
                    <h2 style="font-size: 32px; margin-bottom: 20px;">欢迎来到东巴文</h2>
                    <p style="font-size: 18px; color: #666; margin-bottom: 30px;">
                        这是一个使用History API实现的单页应用示例
                    </p>
                    <div style="display: flex; justify-content: center; gap: 20px;">
                        <button class="btn btn-primary" onclick="router.navigate('products')">
                            浏览产品
                        </button>
                        <button class="btn btn-secondary" onclick="router.navigate('about')">
                            了解更多
                        </button>
                    </div>
                    
                    <div style="margin-top: 50px; padding: 30px; background: #f9f9f9; border-radius: 10px;">
                        <h3>History API 功能演示</h3>
                        <ul style="text-align: left; max-width: 400px; margin: 20px auto;">
                            <li>✅ 无刷新导航</li>
                            <li>✅ URL地址更新</li>
                            <li>✅ 浏览器前进后退支持</li>
                            <li>✅ 状态保存与恢复</li>
                            <li>✅ 分页状态保持</li>
                        </ul>
                    </div>
                </div>
            `;
        }
        
        // 渲染产品页
        function renderProducts(state = {}) {
            const page = state.page || appState.products.page;
            const perPage = appState.products.perPage;
            const totalPages = Math.ceil(products.length / perPage);
            
            const startIndex = (page - 1) * perPage;
            const pageProducts = products.slice(startIndex, startIndex + perPage);
            
            return `
                <h2>产品列表</h2>
                <div class="product-grid">
                    ${pageProducts.map(product => `
                        <div class="product-card">
                            <div class="product-icon">${product.icon}</div>
                            <div class="product-name">${product.name}</div>
                            <div class="product-price">¥${product.price}</div>
                        </div>
                    `).join('')}
                </div>
                
                <div class="pagination">
                    <button onclick="changePage(${page - 1})" ${page === 1 ? 'disabled' : ''}>
                        上一页
                    </button>
                    ${Array.from({ length: totalPages }, (_, i) => i + 1).map(p => `
                        <button class="${p === page ? 'active' : ''}" onclick="changePage(${p})">
                            ${p}
                        </button>
                    `).join('')}
                    <button onclick="changePage(${page + 1})" ${page === totalPages ? 'disabled' : ''}>
                        下一页
                    </button>
                </div>
                
                <p style="text-align: center; margin-top: 20px; color: #666;">
                    当前第 ${page} 页,共 ${totalPages} 页
                </p>
            `;
        }
        
        // 渲染标签页
        function renderTabs(state = {}) {
            const activeTab = state.activeTab || appState.tabs.activeTab;
            
            const tabs = [
                { id: 'tab1', title: '标签一', content: '这是标签一的内容,展示了标签页切换功能。' },
                { id: 'tab2', title: '标签二', content: '这是标签二的内容,标签状态会被保存到历史记录中。' },
                { id: 'tab3', title: '标签三', content: '这是标签三的内容,使用浏览器后退可以回到上一个标签。' }
            ];
            
            return `
                <h2>标签页示例</h2>
                <div class="tabs">
                    ${tabs.map(tab => `
                        <div class="tab ${tab.id === activeTab ? 'active' : ''}" 
                             onclick="changeTab('${tab.id}')">
                            ${tab.title}
                        </div>
                    `).join('')}
                </div>
                
                ${tabs.map(tab => `
                    <div class="tab-content ${tab.id === activeTab ? 'active' : ''}">
                        <p>${tab.content}</p>
                        <p>当前激活的标签: <strong>${tab.title}</strong></p>
                    </div>
                `).join('')}
                
                <div style="margin-top: 20px; padding: 15px; background: #f9f9f9; border-radius: 5px;">
                    <p><strong>提示:</strong>切换标签会添加历史记录,可以使用浏览器后退按钮返回上一个标签。</p>
                </div>
            `;
        }
        
        // 渲染关于页
        function renderAbout() {
            return `
                <h2>关于历史记录API</h2>
                
                <div style="margin: 20px 0;">
                    <h3>什么是History API?</h3>
                    <p>History API是HTML5提供的接口,允许Web应用与浏览器历史记录进行交互。</p>
                </div>
                
                <div style="margin: 20px 0;">
                    <h3>主要方法</h3>
                    <ul>
                        <li><strong>pushState()</strong> - 添加新的历史记录</li>
                        <li><strong>replaceState()</strong> - 替换当前历史记录</li>
                        <li><strong>back()</strong> - 后退</li>
                        <li><strong>forward()</strong> - 前进</li>
                        <li><strong>go()</strong> - 跳转到指定历史记录</li>
                    </ul>
                </div>
                
                <div style="margin: 20px 0;">
                    <h3>主要事件</h3>
                    <ul>
                        <li><strong>popstate</strong> - 历史记录变化时触发</li>
                    </ul>
                </div>
                
                <div style="margin: 20px 0;">
                    <h3>应用场景</h3>
                    <ul>
                        <li>单页应用(SPA)路由</li>
                        <li>无刷新页面切换</li>
                        <li>状态保存与恢复</li>
                        <li>分页导航</li>
                    </ul>
                </div>
            `;
        }
        
        // 路由类
        class Router {
            constructor() {
                this.routes = routes;
                this.contentEl = document.getElementById('mainContent');
                this.init();
            }
            
            init() {
                // 监听导航点击
                document.querySelectorAll('.main-nav a[data-route]').forEach(link => {
                    link.addEventListener('click', (e) => {
                        e.preventDefault();
                        const route = e.target.dataset.route;
                        this.navigate(route);
                    });
                });
                
                // 监听历史记录变化
                window.addEventListener('popstate', (e) => {
                    if (e.state) {
                        this.render(e.state.route, e.state);
                    } else {
                        this.navigate('home', true);
                    }
                });
                
                // 初始渲染
                const initialRoute = this.getRouteFromURL();
                this.navigate(initialRoute, true);
            }
            
            navigate(route, replace = false, state = {}) {
                const url = route === 'home' ? '/' : `/${route}`;
                const fullState = { route, ...state };
                
                if (replace) {
                    history.replaceState(fullState, '', url);
                } else {
                    history.pushState(fullState, '', url);
                }
                
                this.render(route, fullState);
            }
            
            render(route, state = {}) {
                const routeConfig = this.routes[route];
                
                if (routeConfig) {
                    document.title = routeConfig.title + ' - 东巴文';
                    this.contentEl.innerHTML = routeConfig.render(state);
                    
                    // 更新导航激活状态
                    this.updateNavActive(route);
                    
                    // 更新信息显示
                    this.updateInfo(state);
                }
            }
            
            updateNavActive(route) {
                document.querySelectorAll('.main-nav a[data-route]').forEach(link => {
                    if (link.dataset.route === route) {
                        link.classList.add('active');
                    } else {
                        link.classList.remove('active');
                    }
                });
            }
            
            updateInfo(state) {
                document.getElementById('currentUrl').textContent = location.href;
                document.getElementById('historyLength').textContent = history.length;
                document.getElementById('currentState').textContent = JSON.stringify(state);
            }
            
            getRouteFromURL() {
                const path = location.pathname;
                const route = path.substring(1) || 'home';
                return this.routes[route] ? route : 'home';
            }
        }
        
        // 初始化路由
        const router = new Router();
        
        // 切换页面
        function changePage(page) {
            const totalPages = Math.ceil(products.length / appState.products.perPage);
            
            if (page >= 1 && page <= totalPages) {
                appState.products.page = page;
                router.navigate('products', false, { page });
            }
        }
        
        // 切换标签
        function changeTab(tabId) {
            appState.tabs.activeTab = tabId;
            router.navigate('tabs', false, { activeTab: tabId });
        }
    </script>
</body>
</html>

最佳实践

1. 使用pushState而非hash

// 推荐:使用pushState
history.pushState({ page: 1 }, '', '/page/1');
// URL: https://example.com/page/1

// 不推荐:使用hash
location.hash = '#page/1';
// URL: https://example.com/#page/1

东巴文点评pushState提供更干净的URL,更符合RESTful风格,对SEO更友好。

2. 处理初始URL

// 推荐:处理初始URL
window.addEventListener('DOMContentLoaded', function() {
    const route = getRouteFromURL();
    renderPage(route);
});

// 不推荐:忽略初始URL
window.addEventListener('DOMContentLoaded', function() {
    renderPage('home'); // 总是渲染首页
});

3. 保存必要状态

// 推荐:保存必要状态
history.pushState({
    page: 'detail',
    id: 123,
    scrollPosition: window.scrollY
}, '', '/detail/123');

// 不推荐:保存过多数据
history.pushState({
    page: 'detail',
    data: hugeDataObject, // 大数据对象
    dom: element          // DOM元素(无法序列化)
}, '', '/detail');

4. 提供降级方案

// 推荐:检查API支持
if (window.history && history.pushState) {
    // 使用History API
    setupSPA();
} else {
    // 降级到传统导航
    window.location.href = url;
}

学习检验

知识点测试

问题1:以下哪个方法会触发popstate事件?

A. history.pushState() B. history.replaceState() C. history.back() D. history.go(0)

<details> <summary>点击查看答案</summary>

答案:C

东巴文解释pushStatereplaceState不会触发popstate事件。只有浏览器前进后退(如back()forward()go())或用户点击前进后退按钮时才会触发popstate事件。

</details>

问题2pushStatereplaceState的主要区别是?

A. pushState会刷新页面,replaceState不会 B. pushState添加新记录,replaceState替换当前记录 C. pushState可以跨域,replaceState不能 D. 没有区别

<details> <summary>点击查看答案</summary>

答案:B

东巴文解释pushState会在历史记录中添加新记录,用户可以后退;replaceState会替换当前历史记录,用户不能后退到之前的页面。两者都不会刷新页面,都必须同源。

</details>

实践任务

任务:创建一个简单的图片浏览器,支持前进后退导航,使用History API保存当前图片索引。

<details> <summary>点击查看参考答案</summary>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片浏览器</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
            text-align: center;
        }
        
        .image-container {
            position: relative;
            width: 100%;
            height: 400px;
            background: #f5f5f5;
            border-radius: 10px;
            overflow: hidden;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        .image-container img {
            max-width: 100%;
            max-height: 100%;
            object-fit: contain;
        }
        
        .controls {
            display: flex;
            justify-content: center;
            gap: 20px;
            margin-top: 20px;
        }
        
        .controls button {
            padding: 10px 30px;
            font-size: 16px;
            border: none;
            background: #667eea;
            color: white;
            border-radius: 5px;
            cursor: pointer;
        }
        
        .controls button:hover {
            background: #5568d3;
        }
        
        .controls button:disabled {
            background: #ccc;
            cursor: not-allowed;
        }
        
        .info {
            margin-top: 20px;
            padding: 15px;
            background: #f9f9f9;
            border-radius: 5px;
        }
        
        .thumbnails {
            display: flex;
            justify-content: center;
            gap: 10px;
            margin-top: 20px;
        }
        
        .thumbnail {
            width: 60px;
            height: 60px;
            border-radius: 5px;
            overflow: hidden;
            cursor: pointer;
            border: 2px solid transparent;
            transition: all 0.3s;
        }
        
        .thumbnail:hover {
            border-color: #667eea;
        }
        
        .thumbnail.active {
            border-color: #667eea;
        }
        
        .thumbnail img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
    </style>
</head>
<body>
    <h1>图片浏览器</h1>
    
    <div class="image-container">
        <img id="currentImage" src="" alt="">
    </div>
    
    <div class="controls">
        <button id="prevBtn" onclick="prevImage()">上一张</button>
        <button id="nextBtn" onclick="nextImage()">下一张</button>
    </div>
    
    <div class="info">
        <p>当前: <span id="currentIndex">1</span> / <span id="totalCount">0</span></p>
        <p>提示: 使用浏览器前进后退按钮或按钮导航</p>
    </div>
    
    <div class="thumbnails" id="thumbnails"></div>
    
    <script>
        // 图片数据
        const images = [
            { id: 1, url: 'https://picsum.photos/800/400?random=1', title: '图片1' },
            { id: 2, url: 'https://picsum.photos/800/400?random=2', title: '图片2' },
            { id: 3, url: 'https://picsum.photos/800/400?random=3', title: '图片3' },
            { id: 4, url: 'https://picsum.photos/800/400?random=4', title: '图片4' },
            { id: 5, url: 'https://picsum.photos/800/400?random=5', title: '图片5' }
        ];
        
        let currentIndex = 0;
        
        // 初始化
        function init() {
            // 渲染缩略图
            const thumbnailsEl = document.getElementById('thumbnails');
            thumbnailsEl.innerHTML = images.map((img, index) => `
                <div class="thumbnail ${index === 0 ? 'active' : ''}" 
                     onclick="goToImage(${index})">
                    <img src="${img.url}" alt="${img.title}">
                </div>
            `).join('');
            
            // 更新总数
            document.getElementById('totalCount').textContent = images.length;
            
            // 监听历史记录变化
            window.addEventListener('popstate', function(e) {
                if (e.state && typeof e.state.index === 'number') {
                    showImage(e.state.index, false);
                }
            });
            
            // 从URL获取初始索引
            const initialIndex = getIndexFromURL();
            showImage(initialIndex, false);
            history.replaceState({ index: initialIndex }, '', `?image=${initialIndex + 1}`);
        }
        
        // 显示图片
        function showImage(index, addToHistory = true) {
            if (index < 0 || index >= images.length) return;
            
            currentIndex = index;
            const image = images[index];
            
            // 更新图片
            document.getElementById('currentImage').src = image.url;
            document.getElementById('currentImage').alt = image.title;
            
            // 更新索引显示
            document.getElementById('currentIndex').textContent = index + 1;
            
            // 更新按钮状态
            document.getElementById('prevBtn').disabled = index === 0;
            document.getElementById('nextBtn').disabled = index === images.length - 1;
            
            // 更新缩略图激活状态
            document.querySelectorAll('.thumbnail').forEach((thumb, i) => {
                thumb.classList.toggle('active', i === index);
            });
            
            // 添加历史记录
            if (addToHistory) {
                history.pushState({ index }, '', `?image=${index + 1}`);
            }
        }
        
        // 上一张
        function prevImage() {
            if (currentIndex > 0) {
                showImage(currentIndex - 1);
            }
        }
        
        // 下一张
        function nextImage() {
            if (currentIndex < images.length - 1) {
                showImage(currentIndex + 1);
            }
        }
        
        // 跳转到指定图片
        function goToImage(index) {
            showImage(index);
        }
        
        // 从URL获取索引
        function getIndexFromURL() {
            const params = new URLSearchParams(location.search);
            const imageParam = params.get('image');
            
            if (imageParam) {
                const index = parseInt(imageParam) - 1;
                if (index >= 0 && index < images.length) {
                    return index;
                }
            }
            
            return 0;
        }
        
        // 键盘导航
        document.addEventListener('keydown', function(e) {
            if (e.key === 'ArrowLeft') {
                prevImage();
            } else if (e.key === 'ArrowRight') {
                nextImage();
            }
        });
        
        // 初始化
        init();
    </script>
</body>
</html>
</details>

东巴文(db-w.cn) - 让编程学习更有趣、更高效!