前端如何更好的實現介面的緩存和更新?
一個比較複雜的SPA系統,服務端已經做好了數據的緩存。
前端如何更進一步,把一些不需要頻繁更新的介面數據緩存下來。假設一個場景:一個私信列表,列表展示私信的縮略信息。 點擊展開,點擊的時候請求私信id 對應的詳細信息。 像這種情況 私信內容是不會改變的, 如果重複的點擊不同的私信而進行的http請求是一種浪費,且網路質量波動的時候也影響體驗。現在的做法是在 請求層(做了一個全局的ajax的封裝)申明了一個Object,key 就是 請求地址,value 是 返回的數據。 每次請求會先去找 Object 中 有木有這個key 如果有,就直接拿value。 如果沒有這個Key 就發送一個 http 請求,並把結果緩存下來。 同時也做了一個開關,有些需要頻繁更新的介面,就不進行緩存處理。我覺得這種做法比較低效,有沒有一種更好的做法解決這一問題。
這個問題很好。
以傳統方式來說,我們一般會搞一個store,上層業務的請求都是從它走,由它去拿到遠程數據,並且控制緩存。
但我們面臨一個問題,當數據更新了,需要通知所有使用這個數據的業務方,所以就不得不在這裡再補一種機制,用於通知上層。通常就是某種訂閱機制:
store1.subcribe("somedata", data =&> {
//處理這個data,更新到視圖模型上供刷新界面
viewModel.somedata = data;
});
這樣問題是解決了,但業務代碼就比較繁瑣了,為什麼我還要在每個地方都寫訂閱?尤其是如果上層用了angular或者vue,本來可以很簡單的,你這一弄,框架剛幫我簡化了一些代碼,又被搞得很長了。
因為之前我們第一次查詢數據,不是這麼乾的,而是:
service1.getData().then(data =&> {
//處理這個data,更新到視圖模型上供刷新界面
viewModel.somedata = data;
});
所以這裡就出現了不同的兩種方式來更新界面數據,這非常討厭。
我之前提出過一種折中的解決辦法,比如在angular中,可以這樣:
- store中保存數據對象的引用,上層業務在查詢的時候,持有這個引用,並且,不得修改內容
- 如果需要修改或者更新遠程數據,調用store中封裝的修改方法,把數據合併到數據對象中這樣,界面上的刷新之類,可以不勞自己費心,但整體還是挺彆扭的,而且,限制上層不能動引用,這個事情很坑,萬一需要二次加工數據,就歇菜了。
React有一套機制來處理這個事情,留給這派的人來闡述吧。
我說說另外一種方式,FRP。
在egghead上有一個RxJS的視頻系列,其中有一節大致介紹了使用Reactive理念處理這種場景,我覺得非常好,地址如下,可能要會員才能看:
Reactive Programming
它這個場景就是為了復用請求,裡面的代碼摘錄:
var startupRequestStream = Rx.Observable.just("https://api.github.com/users");
var requestOnRefreshStream = refreshClickStream
.map(ev =&> {
var randomOffset = Math.floor(Math.random()*500);
return "https://api.github.com/users?since=" + randomOffset;
});
var requestStream = startupRequestStream.merge(requestOnRefreshStream);
var responseStream = requestStream
.flatMap(requestUrl =&>
Rx.Observable.fromPromise(jQuery.getJSON(requestUrl))
)
.shareReplay(1);
// refreshClickStream: -------f-------------&>
// requestStream: r------r-------------&>
// responseStream: ---R-------R---------&>
// closeClickStream: ---------------x-----&>
// suggestion1Stream: N--u---N---u---u-----&>
function getRandomUser(listUsers) {
return listUsers[Math.floor(Math.random()*listUsers.length)];
}
function createSuggestionStream(responseStream, closeClickStream) {
return responseStream.map(getRandomUser)
.startWith(null)
.merge(refreshClickStream.map(ev =&> null))
.merge(
closeClickStream.withLatestFrom(responseStream,
(x, R) =&> getRandomUser(R))
);
}
這裡面的關鍵在於:shareReplay和withLatestFrom。
shareReplay的介紹在這:shareReplay | RxJS
withLatestFrom的介紹在這:withLatestFrom
拋開代碼細節不談,我覺得理念是這樣:
我把請求當做流,然後,上層業務來訂閱這個流,正常來說,我們的期望可能是,訂閱之後,流的每次更新都會得到處理,但這時可能出現:
某一個訂閱者來得太晚,流已經走了一段數據了,會需要「補課」。
所以,Rx的機制會允許我們緩存一些數據,供「我們來晚了」的朋友們使用。
使用這種流的理念來處理,有什麼好處呢?那就是可以保證上層業務的一致性。對於上層代碼,我只管是從這個流獲取數據,更新界面,我不關注你是哪來的,只認你一家,反正只要根據你那邊的信息,都作了對應處理,就肯定好了。
而這個流的提供方,也很方便,他只需考慮把不同來源的數據進行歸併,從多個流合併成一個流,就是這麼簡單。
Angular 2深度整合了RxJS,可以關注,不過RxJS本身是不綁定到任何框架的,你跟jQuery或者React配合使用,也是沒問題。
個人認為這是目前我見過最優雅的解決此類問題的方案。看了一下回答,我簡單說一點其他人沒說到的,當年做郵箱時,很符合你的這個場景,每封郵件都是不會更改的,查看過後就會被要求緩存。其實沒那麼複雜,你跳出js這個圈,其實html也是可以被設置緩存的,只是一般大家不這麼干,所以對私信部分採用的ajax介面在伺服器端做強緩存就好了,利用瀏覽器的304,就這樣…效果拔群。
瀏覽器的緩存機制比你讀ls數據有時候還要快…當時我們設置的是一周。記住只對介面做緩存配置,頁面算了,萬一改版呢。
更新的話只需要加個版本號就行了。做一個ajax的白名單和伺服器端的同步就行了。這個具體還要看伺服器端的緩存機制。
好處是加上你文中說的還有徐叔叔說的,這樣你一段時間內刷頁面都不會慢了。也可以拋棄ls了。把緩存層抽離出來,在請求的地方只做請求動作,把請求到的數據交給緩存。
其實你可以預見更複雜的場景,比如另一個地方的操作影響到你的消息列表中的某一個數據,你需要同步更新這裡的緩存,這個時候你就可以很方便的在緩存層裡面做各種和你業務邏輯相關的動作。
copy 自徐叔叔的例子,service1.getData().then(data =&> {
//處理這個data,更新到視圖模型上供刷新界面
viewModel.somedata = data;
});
service裡面可以這樣:
import {serviceCache} from "../cache"
import {restAPI} from "../utils"
class Service1 {
getData() {
const cache = serviceCache.getData()
return cache ?
Promise.resolve(cache) :
restAPI.get({
type: "data",
id: "xxx"
})
.then((result) =&> {
serviceCache.save(result)
return serviceCache.getData()
})
}
}
// if angular
angular.module("xxx").service("service1", Service1)
如果只是用變數存起來放內存,我一刷新頁面或者一關標籤頁,這些操作等於白做。
為什麼不把這些key/value放sessionstorage或者localstorage? 這兩個對應是上面說的兩種情況,根據具體情況來做,在第一次請求的時候,把結果緩存起來,用cookie去記錄請求特徵值和版本號,後續的請求根據cookie去找storage。
如何更新請求?
第二次請求的時候,服務端判斷這個cookie知道是否需要更新這個請求,不需要就返回個不需要更新的頭,需要就返回一個完整的,前端知道需要更新了,就去重寫這個cookie。
當然對於私信這種,我覺得可以簡化請求,把本地最後一條記錄id取出來,再去請求,而不是請求完整記錄,後者需要做版本判斷。如果前面的記錄沒有變化且需要更新後端就返回追加的,做個標記讓前端知道,如果前面的記錄也需要變化,就做個標記然後返回比如刪除哪個id的。
這個優化需要花費大量心思,如果不是項目後期,要慎重使用。
我說下我這邊的解決方案吧。
angular系解決方案,但是原理通用。原理很簡單,利用restful基於資源的url設計,然後利用ngResource的便利api。我們對一個資源做查詢操作時默認走緩存(url為key,結果為value的格式,緩存是$http自帶的只要開啟即可),當對該資源做 update/delete/insert 等更新操作時則清空資源緩存。整個過程是通過http攔截器做的,對調用者而言完全透明。
具體用法:
1. 定義資源,基於自己封裝的服務var User = genResource("/users/:userId/", $cacheFactory("user", {capacity: 20}));
2.資源操作
// 第一次
$scope.users = User.query(); // ---&> GET /users/ 請求,結果被緩存起來
.....
// 第二次
$scope.list = User.query(); // 不發請求,直接從緩存返回
.....
// 更新操作
User.update({userId:1}, {roleId:2}); // PUT /users/1 更新資源,緩存會更新
.....
// 因為資源做過更新操作,所以緩存已更新,結果會重新從後台拿
$scope.array = User.query(); // ---&> GET /users/ 請求,結果被緩存起來
3.資源緩存清空實現(http攔截器)
// 很簡單,在攔截器的response回調中加入下面邏輯
if (config.method !== GET config.cache) {
clearCache(config);
}
具體代碼,封裝的服務:angular-utils/base-services.js at 1.3.1 · kuitos/angular-utils · GitHub 攔截器:https://github.com/kuitos/angular-utils/blob/1.3.1/services%2Fhttp-handler.js
目前在我廠多個系統下跑,運作良好。
後來看rest api cookbook發現這是restful做緩存的標準思路哈哈。
唯一的問題是,整個設計基於restful,方案上有一定的局限性。數據的保留與更新 這個很好啊 我一直在思考怎麼做 不過最近有些思路了 這個問題我要慢慢更新 先佔坑 不停挖墳 我的初次思路 數據怎麼組織 現在我覺得應該是對象樹app為root router為二級根節點 不過有傳統單的跳轉頁面和單頁面兩種情況 那麼要滿足數據需要傳遞過去 還要保持兼容到ie8 數據和view層通過vm連接 vm不處理數據 數據由model觀察者處理 每次更新都要向上傳播 這裡一定要分下來 數據處理最好放在worker里 rxjs提供一種可能 是否是不錯的方案 要考慮 另外zone.js可以對非同步操作監控 這是必須的
我們是這麼處理的:
提供統一方法獲取或修改數據
使用sessionStorage或localStorage緩存數據
數據變更派發自定義事件,通知緩存數據發生變化
提供強制從數據介面請求數據的方法題主的這個設計和我之前完全一致。只不過用localstorage 代替了object。還實現了過期協議。
這裡,有一個前端緩存的問題。如果緩存了一個錯誤的結果,緩存系統就沒有任何意義。前端緩存在沒有很強控制力的時候最好別上。會涉及到後台介面變更等等諸多問題要解決。
望三思!不邀自來,個人覺得已經足夠優化了,如果系統比較複雜,可能會經常調整業務,不如花點心思考慮如何能夠調整起來更靈活,再搞下去,個人覺得有點過度優化。
加緩存要有明確可見的結果或者統計數據,不要僅憑感覺就加,只會讓問題複雜化,另外你這個例子更像是前後端API設計不合理。
推薦閱讀:
※現在是2014年,新開發的網站還有必要支持IE6-IE8嗎?
※瀏覽器沙箱(sandBox)到底是什麼?
※Web前端開發比較好的技術類資源網站有哪些?
※知乎為什麼不把網頁背景改成潤眼色?亮白亮白的看時間長了不舒服!
※h5頁面製作,設計多大的尺寸,怎麼和前端適配,實現設計的視覺稿效果?