w3ctech

Angular 跨页缓存设计

本文由糖饼编写,经糖饼授权发表

自去年开始,我将 AngularJS 引入到项目中,并逐渐推动公司产品核心模块进行重构,提升产品稳定性与开发效率。在前端架构演进的过程中最艰难的不是理解 API,而是思维方式被颠覆的无助,所有繁杂的事务都被高度抽象化,以前 WEB 富 UI 开发最头疼的表现部分放到现在几乎不费吹灰之力,前端工程师重心将由 UI 转向数据模型的构建。在这个过程中,小伙伴经常会遇到跨页数据传递这个经典问题,于是我在公司给组里同事做了一次分享,以下是概要:

业务场景

  • 跨页选中操作
  • 分步骤操作

问题

  • Angular 跳转页面后,控制器实例被注销
  • Angular 没有提供跨页传递临时数据的特性

可选方案

A. 超级单页

使用同一个控制器与同一份实例,不使用路由

缺点

  • 无历史记录:不支持浏览器前进后退操作(体验差)
  • 无URL:不支持收藏与分享地址(如果应用出BUG,客户无法提供 URL,导致售后成本变高)

B. URL传递数据

通过 URL 查询参数传递数据

缺点

  • 可能引起安全问题
  • 不支持复杂的数据模型(只支持String类型)

D. Ng Service 缓存

使用 Angular Service 构建内存缓存

权衡后,采用此方案。

实践遇到的问题

唯一性难以保证

  • 可能使用未清理的缓存引起 BUG

内存泄露

  • 过期的缓存得不到清理
  • 缓存会连同控制器一起被 Ng Service 持有(闭包的缘故)

基于路由缓存设计

保证唯一性

  • 在连续操作的页面 URL 中添加 cache key
  • 在控制器中根据 cache key 匹配缓存

cacheKey

// 取 URL 的 cache key
var cacheKey = $routeParams['cache_key'];

读写缓存

if (!AppCache.cache || AppCache.key !== cacheKey) {

    // 覆盖 service 缓存
    AppCache.cache = createCache();
    AppCache.key = cacheKey || Date.now().toString();
}

发送缓存

// 通过路由传递缓存
$scope.submit = function () {
    var queryParam = angular.extend({
        'cache_key': AppCache.key
    }, $routeParams);

    $location.search(queryParam);
}

解决内存泄露

  • $routeChangeSuccess 事件中清理缓存
  • 避免在控制器中创建缓存(解除闭包)

清理过期缓存

$rootScope.$on('$routeChangeSuccess', function () {
    if ($routeParams['cache_key'] === undefined) {
        AppCache.cache = {};
    }
})

封装 RouteCache 服务

高度抽象,屏蔽实现细节

API 设计(第一版)

// 读缓存
var routeCache = RouteCache(createCache);
var data = routeCache.getCache();
var cacheKey = routeCache.getKey();

// 通过路由传递缓存
$scope.submit = function () {
    var queryParam = angular.extend({
        'cache_key': cacheKey
    }, $routeParams);
    $location.search(queryParam);
}

API 设计(优化后)

// 读缓存
var data = RouteCache(createCache);

// 通过路由传递缓存
$scope.submit = function () {
    var queryParam = angular
         .extend({}, data, $routeParams);
    $location.search(queryParam);
}

问题:如何做到 URL 只显示 cache_key 而不暴露数据?

答案:使用原型继承,angular.extend 不会拷贝原型。

RouteCache 内部:

data = createCache();
data = Object.create(data);
data['cache_key'] = cacheKey;

Object.create(data) 是 ECMA5 增加的方法,原理类似:

Object.create = function (object) {
    function F(){};
    F.prototype = object;
    return new F();
}

RouteCache 服务完整源码

/*
 * 基于路由的缓存服务
 * 可以将任何数据模型缓存在路由参数中,适合处理跨页的数据传递
 *
 *  取缓存:
 *      $scope.data = RouteCache(cacheFactory);
 *  写缓存:
 *      $location.search(
 *          angular.extend(
*               {},
 *              $routeParams,
 *              $scope.data
 *          )
 *      );
 *
 * @author  糖饼
 */
define(['./services'], function (services) {

    services.factory('RouteCache', ['$rootScope', '$routeParams', '$cacheFactory',
        function ($rootScope, $routeParams, $cacheFactory) {

        var cache = $cacheFactory('RouteCache');
        var ROUTE_KEY = '@cache_key';
        var TABLE_NAME = 'CACHE';


        /*
         * @param   {Function}  缓存工厂
         * @return  {Object}    继承自缓存的对象
         */
        function Cache (cacheFactory) {

            var data = cache.get(TABLE_NAME);
            var routeKey = $routeParams[ROUTE_KEY];


            if (!data || cache.get(ROUTE_KEY) !== routeKey) {

                data = cacheFactory();

                // 继承缓存
                data = Object.create(data);

                cache.put(TABLE_NAME, data);
                cache.put(ROUTE_KEY, routeKey || Date.now().toString());
            }


            data[ROUTE_KEY] = cache.get(ROUTE_KEY);

            return data;
        };


        // 自动清理缓存
        $rootScope.$on('$routeChangeSuccess', function () {
            if (typeof $routeParams[ROUTE_KEY] === 'undefined') {
                cache.removeAll();
            }
        });


        return Cache;
    }]);

});
w3ctech微信

扫码关注w3ctech微信公众号

共收到4条回复

  • 正好有这方面的需求~~

    回复此楼
  • 可以考虑用localStorage缓存

    回复此楼
  • 感觉你这做的如此复杂

    回复此楼
  • @chenbin92 用localStorage做缓存的话,如何保证多个页面都能获取这个localStorage缓存呢,当前页面表单的数据能保存在这个localStorage当中?谢谢,刚好遇到这个问题,希望能帮我解一下惑

    回复此楼