react-router-redux与IndexedDB同步:离线应用路由状态方案

【免费下载链接】react-router-redux Ruthlessly simple bindings to keep react-router and redux in sync 【免费下载链接】react-router-redux 项目地址: https://gitcode.com/gh_mirrors/re/react-router-redux

你是否遇到过这样的尴尬?用户在你的单页应用中填写了一半表单,突然断网刷新页面,不仅表单数据丢失,连之前浏览的页面位置也重置了。作为开发者,我们不仅要处理数据持久化,还需要关注路由状态(Routing State) 的离线保存。本文将带你用react-router-redux结合IndexedDB,构建一个能在断网时记住用户浏览位置的离线应用方案。

读完本文你将掌握:

  • 路由状态(Routing State)持久化的核心价值
  • 利用IndexedDB存储路由历史的实现方法
  • react-router-redux同步机制的深度应用
  • 完整的离线路由恢复解决方案

路由状态持久化的必要性

在传统的多页应用中,浏览器的前进/后退按钮能完美记忆用户的浏览历史。但在现代单页应用(SPA)中,路由由前端框架控制,一旦页面刷新或断网,客户端路由状态(Client-side Routing State) 就会丢失。这对需要离线功能的应用来说是个严重问题。

react-router-redux通过将路由状态同步到Redux store,为我们提供了拦截和持久化路由变化的可能。其核心同步逻辑在src/sync.js中实现,通过history.listen监听路由变化并分发LOCATION_CHANGE action:

// 简化自[src/sync.js](https://link.gitcode.com/i/3d4bbae5dbe316060df86eb8c362074d)第72-97行
const handleLocationChange = (location) => {
  store.dispatch({
    type: LOCATION_CHANGE,
    payload: location
  })
}
history.listen(handleLocationChange)

而路由状态的存储则由src/reducer.js中的routerReducer处理:

// 来自[src/reducer.js](https://link.gitcode.com/i/6965fd61b68452ec792130dc5bb830a0)第17-22行
export function routerReducer(state = initialState, { type, payload } = {}) {
  if (type === LOCATION_CHANGE) {
    return { ...state, locationBeforeTransitions: payload }
  }
  return state
}

这个机制为我们提供了拦截路由变化的绝佳切入点。

IndexedDB:适合存储路由状态的客户端数据库

要实现路由状态的持久化,我们需要一个可靠的客户端存储方案。与localStorage相比,IndexedDB具有以下优势:

  • 支持更大的存储容量(通常为50MB以上)
  • 提供事务支持,确保数据一致性
  • 支持复杂查询和索引
  • 异步操作,不会阻塞UI线程

下面是一个封装好的IndexedDB工具类,专门用于存储和检索路由状态:

// 建议保存为src/utils/routerStorage.js
class RouterStorage {
  constructor() {
    this.dbName = 'RouterStateDB';
    this.storeName = 'history';
    this.version = 1;
    this.db = null;
  }

  // 初始化数据库连接
  async init() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version);
      
      request.onupgradeneeded = (event) => {
        this.db = event.target.result;
        if (!this.db.objectStoreNames.contains(this.storeName)) {
          // 创建带时间戳索引的对象存储
          const store = this.db.createObjectStore(this.storeName, { 
            keyPath: 'id', 
            autoIncrement: true 
          });
          store.createIndex('timestamp', 'timestamp', { unique: false });
        }
      };

      request.onsuccess = (event) => {
        this.db = event.target.result;
        resolve(true);
      };

      request.onerror = (event) => {
        console.error('IndexedDB初始化失败:', event.target.error);
        reject(event.target.error);
      };
    });
  }

  // 保存路由状态到IndexedDB
  async saveLocation(location) {
    if (!this.db) await this.init();
    
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      
      const entry = {
        location,
        timestamp: Date.now(),
        userAgent: navigator.userAgent
      };
      
      const request = store.add(entry);
      
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.target.error);
    });
  }

  // 获取最近的路由状态
  async getLatestLocation() {
    if (!this.db) await this.init();
    
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readonly');
      const store = transaction.objectStore(this.storeName);
      const index = store.index('timestamp');
      
      // 获取最新的记录(按时间戳降序)
      const request = index.openCursor(null, 'prev');
      
      request.onsuccess = (event) => {
        const cursor = event.target.result;
        resolve(cursor ? cursor.value.location : null);
      };
      
      request.onerror = () => reject(request.target.error);
    });
  }

  // 清除所有路由历史
  async clearHistory() {
    if (!this.db) await this.init();
    
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      const request = store.clear();
      
      request.onsuccess = () => resolve(true);
      request.onerror = () => reject(request.target.error);
    });
  }
}

export const routerStorage = new RouterStorage();

整合react-router-redux与IndexedDB

现在我们需要修改react-router-redux的同步逻辑,在路由变化时将状态保存到IndexedDB,并在应用启动时从IndexedDB恢复状态。

扩展syncHistoryWithStore方法

我们需要创建一个增强版的同步函数,在src/sync.js的基础上添加IndexedDB持久化功能:

// 建议保存为src/enhancedSync.js
import syncHistoryWithStore from './sync';
import { routerStorage } from './utils/routerStorage';

export async function syncHistoryWithIndexedDB(history, store, options = {}) {
  // 首先执行原始的同步逻辑
  const enhancedHistory = syncHistoryWithStore(history, store, options);
  
  // 尝试从IndexedDB恢复最近的路由状态
  try {
    const savedLocation = await routerStorage.getLatestLocation();
    if (savedLocation) {
      console.log('从IndexedDB恢复路由状态:', savedLocation);
      // 恢复路由状态(需确保在应用渲染前执行)
      store.dispatch({
        type: '@@router/LOCATION_CHANGE',
        payload: savedLocation
      });
      
      // 同时更新history以匹配恢复的状态
      if (savedLocation.pathname !== history.getCurrentLocation().pathname) {
        enhancedHistory.replace(savedLocation);
      }
    }
  } catch (error) {
    console.error('恢复路由状态失败:', error);
  }
  
  // 重写history.listen方法,添加IndexedDB持久化
  const originalListen = enhancedHistory.listen;
  enhancedHistory.listen = (listener) => {
    return originalListen(async (location) => {
      // 在通知监听器前保存到IndexedDB
      try {
        await routerStorage.saveLocation(location);
      } catch (error) {
        console.error('保存路由状态失败:', error);
      }
      listener(location);
    });
  };
  
  return enhancedHistory;
}

修改应用入口文件

在应用初始化时使用我们的增强版同步函数:

// 应用入口文件,如examples/basic/app.js
import { createStore, combineReducers } from 'redux';
import { browserHistory } from 'react-router';
import { routerReducer } from '../src/reducer';
import { syncHistoryWithIndexedDB } from '../src/enhancedSync';
import rootReducer from './reducers'; // 你的根reducer

// 合并reducers,确保包含routerReducer
const store = createStore(
  combineReducers({
    ...rootReducer,
    routing: routerReducer
  })
);

// 使用增强版同步函数替代原始的syncHistoryWithStore
syncHistoryWithIndexedDB(browserHistory, store)
  .then(() => {
    // 所有初始化完成后渲染应用
    ReactDOM.render(
      <Provider store={store}>
        <Router history={browserHistory} routes={routes} />
      </Provider>,
      document.getElementById('app')
    );
  });

完整的离线路由状态方案

为确保方案的健壮性,我们还需要考虑以下几点:

1. 路由状态清理策略

随着应用使用时间增长,IndexedDB中的路由记录会越来越多。我们可以添加一个清理策略,只保留最近的N条记录:

// 添加到RouterStorage类
async cleanupHistory(keepLatest = 10) {
  if (!this.db) await this.init();
  
  return new Promise((resolve, reject) => {
    const transaction = this.db.transaction([this.storeName], 'readwrite');
    const store = transaction.objectStore(this.storeName);
    const index = store.index('timestamp');
    const request = index.openCursor(null, 'prev'); // 按时间戳降序
    
    let count = 0;
    const toDelete = [];
    
    request.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor) {
        count++;
        if (count > keepLatest) {
          toDelete.push(cursor.key);
        }
        cursor.continue();
      } else {
        // 批量删除超出数量的记录
        toDelete.forEach(id => store.delete(id));
        resolve(true);
      }
    };
    
    request.onerror = () => reject(request.target.error);
  });
}

2. 网络状态感知

使用浏览器的navigator.onLine属性,我们可以在网络恢复时进行特殊处理:

// 在应用初始化时添加网络状态监听
window.addEventListener('online', () => {
  console.log('网络已恢复,同步路由状态');
  // 可以在这里添加与服务器同步的逻辑
});

window.addEventListener('offline', () => {
  console.log('网络已断开,启用离线路由模式');
});

3. 路由状态冲突解决

当同时存在多个标签页修改路由状态时,可能会导致冲突。我们可以通过添加标签页ID来区分不同会话:

// 修改RouterStorage的saveLocation方法
async saveLocation(location) {
  // ...
  const entry = {
    location,
    timestamp: Date.now(),
    userAgent: navigator.userAgent,
    tabId: Date.now().toString(36) + Math.random().toString(36).substr(2, 5)
  };
  // ...
}

方案测试与验证

为了验证我们的方案是否有效,可以按照以下步骤测试:

  1. 启动示例应用:

    cd examples/basic
    npm install
    npm start
    
  2. 在浏览器中访问应用,浏览几个不同的路由页面

  3. 断开网络连接(可以使用浏览器的开发者工具Network标签页勾选"Offline")

  4. 刷新页面,确认应用能恢复到之前的路由状态

  5. 尝试前进/后退导航,确认历史记录完整

  6. 重新连接网络,确认新的路由变化能正常同步

总结与扩展思考

本文介绍的方案通过react-router-redux的路由同步机制,结合IndexedDB的本地存储能力,解决了离线环境下路由状态丢失的问题。核心要点包括:

  • 利用react-router-redux的src/sync.js拦截路由变化
  • 使用IndexedDB而非localStorage存储路由状态,提供更大容量和更强大的查询能力
  • 通过增强版同步函数syncHistoryWithIndexedDB整合存储逻辑
  • 添加清理策略和冲突解决机制确保方案的健壮性

这个方案不仅适用于路由状态,还可以扩展到其他需要持久化的Redux状态。你可以进一步优化:

  • 添加路由状态变更的时间线可视化
  • 实现跨设备的路由状态同步(结合Service Worker)
  • 为不同用户角色保存不同的路由偏好

希望这个方案能帮助你构建更健壮的离线优先应用!如果有任何问题或改进建议,欢迎在项目仓库提交issue。

【免费下载链接】react-router-redux Ruthlessly simple bindings to keep react-router and redux in sync 【免费下载链接】react-router-redux 项目地址: https://gitcode.com/gh_mirrors/re/react-router-redux

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐