新聞中心
之前校招面試的時候遇到過一個很有趣的問題:

“假設(shè)有一個超超超大型的Web項目,JS源代碼壓縮之后超過10MB(實際怎么可能會有這么大的嘛=。=),每次更新之后都需要用戶重新加載JS是不可接受的,那么怎么樣從工程的角度解決這種問題?”
一開始立馬想到了好幾點解決方案,比如:
- 抽出基礎(chǔ)的不常更新的模塊作為長期緩存;
- 如果使用了 React 或者 Vue2.0 這樣支持服務(wù)器端渲染的框架的話,就采用服務(wù)器端渲染然后再逐步分塊加載 JS 的方法;
- 如果是 Hybrid 開發(fā)的話,可以考慮使用本地資源加載,類似“離線包”的想法(之前在騰訊實習的時候天天遇到這東西)。
后來在面試官的引導下想到了一種“增量式更新”的解決方案,簡單地說就是在版本更新的時候不需要重新加載資源,只需要加載一段很小的 diff 信息,然后合并到當前資源上,類似 git merge 的效果。
1、用戶端使用 LocalStorage 或者其它儲存方案,存儲一份原始代碼+時間戳:
- {
- timeStamp: "20161026xxxxxx",
- data: "aaabbbccc"
- }
2、每次加載資源的時候向服務(wù)器發(fā)送這個時間戳;
3、服務(wù)器從接受到時間戳中識別出客戶端的版本,和***的版本做一次 diff,返回兩者的 diff 信息:
- diff("aaabbbccc", "aaagggccc");
- // 假設(shè)我們的diff信息這樣表示:
- // [3, "-3", "+ggg", 3]
4、客戶端接收到這個 diff 信息之后,把本地資源和時間戳更新到***,實現(xiàn)一次增量更新:
- mergeDiff("aaabbbccc", [3, "-3", "+ggg", 3]);
- //=> "aaagggccc"
實踐
下面把這個方案中的核心思想實現(xiàn)一遍,簡單地說就是實現(xiàn) diff 和 mergeDiff 兩個函數(shù)。
今天找到了一個不錯的 diff 算法:
GitHub – kpdecker/jsdiff: A javascript text differencing implementation.
我們只需要調(diào)用它的 diffChars 方法來對比兩個字符串的差異:
- var oldStr = 'aaabbbccc';
- var newStr = 'aaagggccc';
- JsDiff.diffChars(oldStr, newStr);
- //=>
- //[ { count: 3, value: 'aaa' },
- // { count: 3, added: undefined, removed: true, value: 'bbb' },
- // { count: 3, added: true, removed: undefined, value: 'ggg' },
- // { count: 3, value: 'ccc' } ]
上面的 diff 信息略有些冗余,我們可以自定義一種更簡潔的表示方法來加速傳輸?shù)乃俣龋?/p>
- [3, "-3", "+ggg", 3]
整數(shù)代表無變化的字符數(shù)量,“-”開頭的字符串代表被移除的字符數(shù)量,“+”開頭的字符串代表新加入的字符。所以我們可以寫一個 minimizeDiffInfo 函數(shù):
- function minimizeDiffInfo(originalInfo){
- var result = originalInfo.map(info => {
- if(info.added){
- return '+' + info.value;
- }
- if(info.removed){
- return '-' + info.count;
- }
- return info.count;
- });
- return JSON.stringify(result);
- }
- var diffInfo = [
- { count: 3, value: 'aaa' },
- { count: 3, added: undefined, removed: true, value: 'bbb' },
- { count: 3, added: true, removed: undefined, value: 'ggg' },
- { count: 3, value: 'ccc' }
- ];
- minimizeDiffInfo(diffInfo);
- //=> '[3, "-3", "+ggg", 3]'
用戶端接受到精簡之后的 diff 信息,生成***的資源:
- mergeDiff('aaabbbccc', '[3, "-3", "+ggg", 3]');
- //=> 'aaagggccc'
- function mergeDiff(oldString, diffInfo){
- var newString = '';
- var diffInfo = JSON.parse(diffInfo);
- var p = 0;
- for(var i = 0; i < diffInfo.length; i++){
- var info = diffInfo[i];
- if(typeof(info) == 'number'){
- newString += oldString.slice(p, p + info);
- p += info;
- continue;
- }
- if(typeof(info) == 'string'){
- if(info[0] === '+'){
- var addedString = info.slice(1, info.length);
- newString += addedString;
- }
- if(info[0] === '-'){
- var removedCount = parseInt(info.slice(1, info.length));
- p += removedCount;
- }
- }
- }
- return newString;
- }
實際效果
有興趣的話可以直接運行這個:
GitHub – starkwang/Incremental
使用 create-react-app 這個小工具快速生成了一個 React 項目,隨便改了兩行代碼,然后對比了一下build之后的新舊兩個版本:
- var JsDiff = require('diff');
- var fs = require('fs');
- var newFile = fs.readFileSync('a.js', 'utf-8');
- var oldFile = fs.readFileSync('b.js', 'utf-8');
- console.log('New File Length: ', newFile.length);
- console.log('Old File Length: ', oldFile.length);
- var diffInfo
- = getDiffInfo(JsDiff.diffChars(oldFile, newFile));
- console.log('diffInfo Length: ', diffInfo.length);
- console.log(diffInfo);
- var result = mergeDiff(oldFile, diffInfo);
- console.log(result === newFile);
下面是結(jié)果:
可以看到 build 之后的代碼有 21w 多個字符(212KB),而 diff 信息卻相當短小,只有151個字符,相比于重新加載新版本,縮小了1000多倍(當然我這里只改了兩三行代碼,小是自然的)。
一些沒涉及到的問題
上面只是把核心的思路實現(xiàn)了一遍,實際工程中還有更多要考慮的東西:
1、服務(wù)器不可能對每一次請求都重新計算一次 diff,所以必然要對 diff 信息做緩存;
2、用戶端持久化儲存的實現(xiàn)方案,比如喜聞樂見的 LocalStorage、Indexed DB、Web SQL,或者使用 native app 提供的接口;
3、容錯、用戶端和服務(wù)器端的一致性校對、強制刷新的實現(xiàn)。
標題名稱:實現(xiàn)前端資源增量式更新的一種思路
網(wǎng)站鏈接:http://fisionsoft.com.cn/article/cosdiei.html


咨詢
建站咨詢
