blog
blog copied to clipboard
Angular 跨页缓存设计
自去年开始,我将 AngularJS 引入到项目中,并逐渐推动公司产品核心模块进行重构,提升产品稳定性与开发效率。在前端架构演进的过程中,最艰难的不是理解 API,而是思维方式被颠覆后的的适应过程,在基于 AngularJS 的架构下,所有繁杂的事务都被高度抽象化。
这是给团队小伙伴的一次关于 AngularJS 的分享,整理自 PPT:
业务场景
- 跨页选中操作,此时如何保存选中状态?
- 分步骤操作,如何保存上一次的操作数据?
问题
- 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;
}]);
});
“Angular 没有提供跨页传递临时数据的特性”,其实认真说起来是有的,$rootScope。
$rootScope与其他服务在当前场景下并无本质区别,它是一个”全局“的对象
并非专业前端,后端工程师。
你这个利用angularjs service的$cacheFactory貌似也是利用url传递的参数啊。。
如果我没有理解错的话,$location.search 就是把你的缓存往url上边写参数。$cacheFactory应该还是单纯的单页面、堆内存缓存,如果页面刷新,或url跳转,这个缓存就木有了?
对于这一句 "缓存会连同控制器一起被 Ng Service 持有(闭包的缘故)”,在 service 缓存数据跟 controller 有啥关系?service里面缓存草稿数据,controller去service取数据,这个过程为啥会内存泄露?在前端内存保存草稿用处不是很明显,最好保存到 storage 或者 存到后台。
@zhenyong 当时我演示场景和具体业务代码有关,这里确实表述有问题。
@pinkdawn 如果刷新页面,数据自然没有了,这个就是用来做临时缓存。也可以结合本地缓存避免刷新