11

问题

在开发react-native过程中,使用redux保存状态迁移已基本成为一个标准做法。用户登录时的状态变更,会带来redux状态迁移,而应用程序的其他部分也需要了解用户是否已登录以及相关的登录信息,只要软件不退出,通过reducer我们总是能感知到变化的。但问题是软件退出后,reducer从内存中消失,用户如果再次打开软件,还需要登录。简单做法是把登录的token等信息存储在react-native提供的AsyncStorage里,但这样一来就打断了和redux的联系。有没有可能直接把redux的信息保存在AsyncStorage里呢?这样一来我们就既解决了记住用户登录信息的问题,同时又不打破redux的优良结构。

Github上已经有现成的redux-persist包以解决redux持久化问题,但在实际使用过程中,还有很多问题需要解决。具体来说,redux-persist这个包提供的是通用解决方案,也可以用于react.js,如果你要用在react-native中的话,需要指定AsyncStorage,另外,虽然它还额外提供了两个transform插件redux-persist-transform-immutableredux-persist-immutable,但这两个插件目前使用起来还是有问题没有解决,为了尽快用上redux-persist,可以使用以下方案。

解决

首先,在建立redux store时,除了常规会用到的各种中间件以外,我们需要额外引入redux-persist里的autoRehydrate增强器,然后启动持久化。这部分代码保存在Store目录下的Store.js文件中:

// @flow

import { createStore, applyMiddleware, compose } from 'redux';
import { autoRehydrate } from 'redux-persist';
import createSagaMiddleware from 'redux-saga';
import rootReducer from '../Reducers/';
import sagas from '../Sagas/';
import RehydrationServices from '../Services/RehydrationServices';
import ReduxPersist from '../Config/ReduxPersist';
import Config from '../Config/DebugConfig';

// 屏蔽flow误报警
declare var console: any;

// 添加saga中间件
let middleware = [];
const sagaMiddleware = createSagaMiddleware();
middleware.push(sagaMiddleware);

export default () => {
  let store = {};

  // 根据配置要求采用Reactotron或者原生store
  const createAppropriateStore = Config.useReactotron ? console.tron.createStore : createStore;

  if (ReduxPersist.active) {
    // 如果配置中要求采用持久化
    const enhancers = compose(
      applyMiddleware(...middleware),
      autoRehydrate()
    );

    store = createAppropriateStore(
      rootReducer,
      enhancers
    );

    // 启动持久化
    RehydrationServices.updateReducers(store);
  } else {
    // 如果配置中不要求采用持久化
    const enhancers = compose(
      applyMiddleware(...middleware),
    );

    store = createAppropriateStore(
      rootReducer,
      enhancers
    );
  }

  // 运行saga
  sagaMiddleware.run(sagas);

  return store;
};

代码中又对其他几段代码做了依赖,其中放在Reducers目录下的index.js中定义了黑名单,放在黑名单中的reducer是不进行持久化的:

// @flow

import { combineReducers } from 'redux';

import LoginReducer from './LoginReducer';
import ActivitiesReducer from './ActivitiesReducer';
import ActivityReducer from './ActivityReducer';
import ResourcesReducer from './ResourcesReducer';
import NewsesReducer from './NewsesReducer';

export default combineReducers({
  login: LoginReducer,
  activities: ActivitiesReducer,
  activity: ActivityReducer,
  resources: ResourcesReducer,
  newses: NewsesReducer,
});

// 添加persist黑名单,以下这些reducer不需要持久化
export const persistentStoreBlacklist = [
  'activities',
  'activity',
  'resources',
  'newses',
];

设置好黑名单之后,可以开始真正启用持久化了,这部分代码放在Services目录下的RehydrationServices.js里:

// @flow

import { AsyncStorage } from 'react-native';
import { persistStore } from 'redux-persist';

import ReduxPersist from '../Config/ReduxPersist';

const updateReducers = (store: any) => {
  const reducerVersion = ReduxPersist.reducerVersion;
  const config = ReduxPersist.storeConfig;

  // 按照配置要求自动持久化reducer
  persistStore(store, config);

  AsyncStorage.getItem('reducerVersion').then((localVersion) => {
    // 从本地存储取出reducer版本并比较
    if (localVersion !== reducerVersion) {
      // 如果本地存储中的reducer版本与配置文件中的reducer版本不同,则需要清理持久化数据
      persistStore(store, config, () => {
        persistStore(store, config);
      }).purge([]);
      // 清理成功,将本地存储中的reducer版本设为配置文件中的reducer版本
      AsyncStorage.setItem('reducerVersion', reducerVersion);
    }
  }).catch(() => AsyncStorage.setItem('reducerVersion', reducerVersion));
}

export default {updateReducers};

这里要取Config目录下的ReduxPersist.js文件的配置:

// @flow

import { AsyncStorage } from 'react-native';

import immutablePersistenceTransform from '../Store/ImmutablePersistenceTransform';
import { persistentStoreBlacklist } from '../Reducers/';

const REDUX_PERSIST = {
  active: true, // 是否采用持久化策略
  reducerVersion: '2',  // reducer版本,如果版本不一致,将刷新整个持久化仓库
  storeConfig: {
    storage: AsyncStorage,  // 采用本地异步存储,react-native必须
    blacklist: persistentStoreBlacklist,  // 从根reducer获取黑名单,黑名单中的reducer不进行持久化保存
    transforms: [immutablePersistenceTransform],  // 重要,因为redux是immutable不可变的,此处必须将常规数据做变形,否则会失败
  }
};

export default REDUX_PERSIST;

这里用到了一个最重要的变形,否则整个过程不能成功,因为redux里的对象都是immutable不可变的,我们在将它们持久化的时候,必须转成mutable可变的常规js对象,而从本地存储中取出来进入redux循环的时候,又需要将它们变成immutable的。下面这段代码要放在Store目录下的ImmutablePersistenceTransform.js中:

// @flow

import R from 'ramda';
import Immutable from 'seamless-immutable';

// 将redux中的immutable对象转为普通js对象,以便于持久化存储
const isImmutable = R.has('asMutable');
const convertToJs = (state) => state.asMutable({deep: true});
const fromImmutable = R.when(isImmutable, convertToJs);

// 将普通js对象转为immutable不可变,以供redux使用
const toImmutable = (raw) => Immutable(raw);

export default {
  out: (state: any) => {
    // 设置深度合并
    state.mergeDeep = R.identity;
    // 从仓库中取出,进入内存时,转为immutable不可变
    return toImmutable(state);
  },
  in: (raw: any) => {
    // 进入仓库时,将immutable不可变数据转为常规数据
    return fromImmutable(raw);
  }
};

用法

和常规使用方法一样,原先如何使用redux,现在还是怎么样用,应用程序启动时,直接判断保存用户登录信息的reducer里有没有值就行了,如果没有的话,调出登录界面,如果有的话,直接从reducer中取值。是不是很方便呢?

案例

完整代码可参见我在Github上的项目:Wecanmobile。觉得有帮助的话,请帮我打一颗星星。


张京
13.4k 声望4.7k 粉丝

现任北京联云天下科技有限公司技术副总裁。1994年毕业于清华大学计算机科学与技术专业;20多年软件开发及项目管理经验;历任亚洲生活网络公司CTO,摩托罗拉软件中心QSE工具经理,融信恒通技术总监,安必信软件公...