中国领先的IT技术网站
|
|

实现前端资源增量式更新的一种思路

“增量式更新”简单地说就是在版本更新的时候不需要重新加载资源,只需要加载一段很小的 diff 信息,然后合并到当前资源上,类似 git merge 的效果。

作者:佚名来源:前端大全|2017-08-24 15:02

Tech Neo技术沙龙 | 11月25号,九州云/ZStack与您一起探讨云时代网络边界管理实践


实现前端资源增量式更新的一种思路

之前校招面试的时候遇到过一个很有趣的问题:

“假设有一个超超超大型的Web项目,JS源代码压缩之后超过10MB(实际怎么可能会有这么大的嘛=。=),每次更新之后都需要用户重新加载JS是不可接受的,那么怎么样从工程的角度解决这种问题?”

一开始立马想到了好几点解决方案,比如:

  1. 抽出基础的不常更新的模块作为长期缓存;
  2. 如果使用了 React 或者 Vue2.0 这样支持服务器端渲染的框架的话,就采用服务器端渲染然后再逐步分块加载 JS 的方法;
  3. 如果是 Hybrid 开发的话,可以考虑使用本地资源加载,类似“离线包”的想法(之前在腾讯实习的时候天天遇到这东西)。

后来在面试官的引导下想到了一种“增量式更新”的解决方案,简单地说就是在版本更新的时候不需要重新加载资源,只需要加载一段很小的 diff 信息,然后合并到当前资源上,类似 git merge 的效果。

1、用户端使用 LocalStorage 或者其它储存方案,存储一份原始代码+时间戳:

  1.  
  2.     timeStamp"20161026xxxxxx"
  3.  
  4.     data: "aaabbbccc" 
  5.  
  6.  

2、每次加载资源的时候向服务器发送这个时间戳;

3、服务器从接受到时间戳中识别出客户端的版本,和最新的版本做一次 diff,返回两者的 diff 信息:

  1. diff("aaabbbccc""aaagggccc"); 
  2.  
  3. // 假设我们的diff信息这样表示: 
  4.  
  5. // [3, "-3""+ggg", 3]  

4、客户端接收到这个 diff 信息之后,把本地资源和时间戳更新到最新,实现一次增量更新:

  1. mergeDiff("aaabbbccc", [3, "-3""+ggg", 3]); 
  2.  
  3. //=> "aaagggccc"  

实践

下面把这个方案中的核心思想实现一遍,简单地说就是实现 diff 和 mergeDiff 两个函数。

今天找到了一个不错的 diff 算法:

GitHub – kpdecker/jsdiff: A javascript text differencing implementation.

我们只需要调用它的 diffChars 方法来对比两个字符串的差异:

  1. var oldStr = 'aaabbbccc'
  2.  
  3. var newStr = 'aaagggccc'
  4.  
  5. JsDiff.diffChars(oldStr, newStr); 
  6.  
  7. //=> 
  8.  
  9. //[ { count: 3, value: 'aaa' }, 
  10.  
  11. //  { count: 3, added: undefined, removed: true, value: 'bbb' }, 
  12.  
  13. //  { count: 3, added: true, removed: undefined, value: 'ggg' }, 
  14.  
  15. //  { count: 3, value: 'ccc' } ]  

上面的 diff 信息略有些冗余,我们可以自定义一种更简洁的表示方法来加速传输的速度:

  1. [3, "-3""+ggg", 3] 

整数代表无变化的字符数量,“-”开头的字符串代表被移除的字符数量,“+”开头的字符串代表新加入的字符。所以我们可以写一个 minimizeDiffInfo 函数:

  1. function minimizeDiffInfo(originalInfo){ 
  2.  
  3.     var result = originalInfo.map(info => { 
  4.  
  5.         if(info.added){ 
  6.  
  7.             return '+' + info.value; 
  8.  
  9.         } 
  10.  
  11.         if(info.removed){ 
  12.  
  13.             return '-' + info.count
  14.  
  15.         } 
  16.  
  17.         return info.count
  18.  
  19.     }); 
  20.  
  21.     return JSON.stringify(result); 
  22.  
  23.  
  24.   
  25.  
  26. var diffInfo = [ 
  27.  
  28.     { count: 3, value: 'aaa' }, 
  29.  
  30.     { count: 3, added: undefined, removed: true, value: 'bbb' }, 
  31.  
  32.     { count: 3, added: true, removed: undefined, value: 'ggg' }, 
  33.  
  34.     { count: 3, value: 'ccc' } 
  35.  
  36. ]; 
  37.  
  38. minimizeDiffInfo(diffInfo); 
  39.  
  40. //=> '[3, "-3", "+ggg", 3]'  

用户端接受到精简之后的 diff 信息,生成最新的资源:

  1. mergeDiff('aaabbbccc''[3, "-3", "+ggg", 3]'); 
  2.  
  3. //=> 'aaagggccc' 
  4.  
  5.   
  6.  
  7. function mergeDiff(oldString, diffInfo){ 
  8.  
  9.     var newString = ''
  10.  
  11.     var diffInfo = JSON.parse(diffInfo); 
  12.  
  13.     var p = 0; 
  14.  
  15.     for(var i = 0; i < diffInfo.length; i++){ 
  16.  
  17.         var info = diffInfo[i]; 
  18.  
  19.         if(typeof(info) == 'number'){ 
  20.  
  21.             newString += oldString.slice(p, p + info); 
  22.  
  23.             p += info; 
  24.  
  25.             continue
  26.  
  27.         } 
  28.  
  29.         if(typeof(info) == 'string'){ 
  30.  
  31.             if(info[0] === '+'){ 
  32.  
  33.                 var addedString = info.slice(1, info.length); 
  34.  
  35.                 newString += addedString; 
  36.  
  37.             } 
  38.  
  39.             if(info[0] === '-'){ 
  40.  
  41.                 var removedCount = parseInt(info.slice(1, info.length)); 
  42.  
  43.                 p += removedCount; 
  44.  
  45.             } 
  46.  
  47.         } 
  48.  
  49.     } 
  50.  
  51.     return newString; 
  52.  
  53.  

实际效果

有兴趣的话可以直接运行这个:

GitHub – starkwang/Incremental

使用 create-react-app 这个小工具快速生成了一个 React 项目,随便改了两行代码,然后对比了一下build之后的新旧两个版本:

  1. var JsDiff = require('diff'); 
  2.  
  3. var fs = require('fs'); 
  4.  
  5.   
  6.  
  7. var newFile = fs.readFileSync('a.js''utf-8'); 
  8.  
  9. var oldFile = fs.readFileSync('b.js''utf-8'); 
  10.  
  11. console.log('New File Length: ', newFile.length); 
  12.  
  13. console.log('Old File Length: ', oldFile.length); 
  14.  
  15.   
  16.  
  17. var diffInfo  
  18.  
  19. = getDiffInfo(JsDiff.diffChars(oldFile, newFile)); 
  20.  
  21. console.log('diffInfo Length: ', diffInfo.length); 
  22.  
  23. console.log(diffInfo); 
  24.  
  25.   
  26.  
  27. var result = mergeDiff(oldFile, diffInfo); 
  28.  
  29. console.log(result === newFile);  

下面是结果:

可以看到 build 之后的代码有 21w 多个字符(212KB),而 diff 信息却相当短小,只有151个字符,相比于重新加载新版本,缩小了1000多倍(当然我这里只改了两三行代码,小是自然的)。

一些没涉及到的问题

上面只是把核心的思路实现了一遍,实际工程中还有更多要考虑的东西:

1、服务器不可能对每一次请求都重新计算一次 diff,所以必然要对 diff 信息做缓存;

2、用户端持久化储存的实现方案,比如喜闻乐见的 LocalStorage、Indexed DB、Web SQL,或者使用 native app 提供的接口;

3、容错、用户端和服务器端的一致性校对、强制刷新的实现。

【编辑推荐】

  1. 现代前端开发技术栈
  2. 前端实现文件的断点续传
  3. 前端面试中的常见的算法问题
  4. 纯前端解决跨域问题
  5. 暑期干货!2017年8月前端开发者超实用干货大合集
【责任编辑:庞桂玉 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

程序设计实践双语版

程序设计实践并不只是写代码。程序员必须评论各种折衷方案,在许多可能性之中做出选择、排除错误、做测试和改进程序性能,还要维护自己或其...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊
× CTO训练营(深圳站)