您的位置:首页 > 其它

【好文分享】用增量更新算法为 web 应用节省流量

2014-02-15 23:04 302 查看
今天看到这篇文章只能连声赞好,只能说我们广大程序员为了用户真的是无所不用其极啊!学习是用无止境的!!!

本文主要讲解如何利用 HTML5 的 localstorage 和增量更新算法实现 JavaScript 的本地化,并在版本更新的时候基本做到修改多少内容就下载多少内容,为网站和用户节省 90%以上的 JavaScript 流量,尤其适合快速迭代开发的手机网站使用。

前言

随着 web 前端技术的发展,目前的 JavaScript 的能力越来越强,它在 web 端的能力越来越强,已经可以用来做一些以前没法做的事情了。本文讲的是通过 JavaScript 自己来实现资源文件的本地存储和增量更新方案的设计和实现。

传统的 JavaScript 资源加载方式的缺点

传统的 JavaScript 资源存放方式一般就是通过 CDN 方式存放,缓存方面通过增加 maxage、Last-Modified,etag 等方式依靠 HTTP Cache 相关协议进行缓存。这种方式的问题主要是缓存命中率不是很高,另外在快速迭代的产品中,由于代码经常需要修改,虽然很多时候只是修改很小的一部分内容,但是还是需要用户全量下载整个 JavaScript 文件,造成流量上的耗费。

HTML5 appcache JavaScript 资源加载方式的优缺点

除了传统方式的存放和加载 JavaScript,HTML5 给我们提供了另一种 JavaScript 资源缓存的方式,即 HTML5 的离线存储或 application cache.通过给 manifest 头文件定义资源的本地存放方式,我们可以完全实现静态数据本地存储,减少了大量网络请求,减少网络流量。但是这种方式同时也有他致命的缺点:

1.appcache 机制定义了在更新离线存储版本的时候,用户的首次进入页面并不会启用最新的资源文件,而是由一个后台程序先把资源下载到本地,用户需要刷新或者再次进入页面时才会启用新的资源文件,当然这个问题可以通过监听离线缓存的更新完成时间,在更新完成的时候程序去刷新页面以启用新的静态资源,但是这个方式带来了一个致命的不佳的用户体验,就是用户进来后会看到浏览器自己刷新了一下页面,对一些网站来说这显然不能接受。

2.对于引入了离线存储的页面,是没有办法去掉离线存储的,这给一些首页是动态页面的网站造成了极大的困扰。一些灰度发布的策略无法很好的实施。

用 localstorage 来存储 JavaScript

针对 appcache 的一些致命问题,我们决定找到一个东西来存储 JavaScript,而不走 appcache 以避免它的一些问题。localstorage 是一个浏览器端的 key-value 型数据库,可以通过相关的 JavaScript API 来进行操作,标准的 localstorage 可以存放 5m 的数据,对于一般的网站来说用来存 JavaScript 肯定是足够了。于是我们载入 JavaScript 的流程变为比较的上次更新的版本号(可以存入本地存储)和本次更新的版本号(可以是一个 JavaScript
变量写在页面上),如果不一致用 Ajax 去服务器拉取最新的内容并通过 eval 解释执行 JavaScript,然后存入本地存储;如果一致则直接从本地存储读取 JavaScript 内容并 eval 解释执行。代码大概如清单 1 所示。

清单 1.使用本地存储 JavaScript 并用 eval 来解释 JavaScript


123456789101112131415161718192021222324var jsver=”1.0.1”;Var storeKey=jsurlVar lastver=localStorage.gettem(storeKey+"?ver");If(lastver!=jsver){ //如果版本不一样,则 Ajax 新版本 JavaScript 代码,注意这里如果不是同一个源服务器端话需要加 // Access-Control-Allow-Origin:*头允许跨域 xhr(realUrl,function(data){ Var jsCode=data; try{ globalEval(jsCode);//eval 代码 localStorage.setItem(storeKey,jsCode); localStorage.setItem(storeKey+"?ver",jsver); } catch(e){ localStorage.removeItem(storeKey); localStorage.removeItem(storeKey+"?ver"); } });}Else{ Var jsCode=localStorage.gettem(storeKey); globalEval(jsCode);//eval 代码}
到这里我们基本可以实现用本地存储来代替离线存储,从而避免离线存储的一些致命问题。

JavaScript 增量更新算法设计

用 localstorage 来存储 JavaScript 文件我们已经减少了很多不必要的 304 http 请求,但是对于版本更新的时候,我们还是必须全量下载整个 js 文件。然而大多数在快速迭代开发的网站中,我们修改 JavaScript 往往只是修改很少的一部分内容,这就造成了大部分 JavaScript 数据的下载是浪费的,接下来我们将设计一个算法来解决这个问题。首先通过 localstorage 我们能获取上一个版本的 JavaScript 内容,那么只要我们通过一种办法计算出来我们本次更新在原有的 JavaScript 内容上什么位置更新了什么内容,那么我们就可以根据这个数据和 JavaScript 上个版本的数据合并生成一个新版本的 JavaScript 文件。先来看下整个增量更新的流程:先将旧文件按一定长度分成多个块,计算 md5 值放入一个 map,如图 1 所示
图 1.旧文件按照一定长度切分并编号


在新文件上进行滚动 md5 查找,如果找到匹配的则记录块号,如果没找到则块往前移动 1 个字符,并把上个字符压入新数据块,然后扫描下一块,最终得到一个新数据和数据块号的组合的增量文件(这一步可以用上线 JavaScript 时用的打包工具或者请求 JavaScript 时用服务器程序实时计算出来)如图 2 所示
图 2.新文件滚动查找后由旧数据块号和新更新数据组成

最终增量文件表示如下:

1

1,data1,2,3,data2,4,5,6

进一步合并顺序块得到:

1[1,1],data1,[2,2],data2,[4,3]
客户端根据旧文件的 chunk 数据和第二部生成的增量更新数据,我们可以得出新版本数据由如下数据组成:

1

chunk0+data1+chunk1+chunk2+data2+chunk3+chunk4+chunk5

以 s=’12345678abcdefghijklmnopq’ 修改为 a=’123456f78abcd2efghijklmnopq’ 为例。

设块长度为 4 则,源文件可分割为如下 chunk 数据(第一行块号,第二行数据),如图 3 所示:
图 3.实例旧文件块号及块内容



通过滚动查找比对,得到新的文件构成如图 4 所示:
图 4.实例新文件块号和新数据结构



最终增量文件表示如下数组:

1["a='12",2,"f",3,"cd2ef",5,6,7]
进一步合并顺序块,可用一个 js 数组表示为

1

["a='12",[2,1],"f",[3,1],"cd2ef",[5,3]]

所以新数据为:

1a="a='12",+chunk2+”f”+chunk3+"cd2ef",+chunk5+chunk6+chunk7=a='123456f78abcd2efghi4jklmnopq'
合并代码如清单 2 所示.
清单 2.合并代码函数

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

//source
是上一个版本内容,trunkSize 是块大小,checksumcode 是两个版本间的增量文件数组

var
rsyncjs=function(source,trunkSize,checksumcode){

var
strResult="";

for(var
i=0;i<checksumcode.length;i++){

var
code=checksumcode[i];

if(typeof
(code)=='string'){

strResult+=code;

}

else{

var
start=code[0]*trunkSize;

var
len=code[1]*trunkSize;

var
oldcode=source.substr(start,len);

strResult+=oldcode;

}

}

return
strResult;

}

通过这个算法,我们可以基本达到修改哪些内容就下载哪些内容的目的,大大减少下载流量。

JavaScript 本地存储和增量更新 seajs 插件的实现

为了推广以上的算法,笔者用上边这个算法写了一个 seajs 插件 storeinc(https://github.com/luyongfugx/storeinc),seajs 用户通过使用这个插件结合为之编写的构建工具 spm-storeinc-build()就可以很容易的集成本地存储和增量更新功能,下面我们通过一个例子来展示一下如何使用这个插件.这个例子通过修改 seajs 官方 examples 的 hello 例子来引入 storeinc.到 https://github.com/luyongfugx/storeinc/tree/master/
把里面的 demo 目录下载到自己的 web 服务器。 打开 sea-moudles/seajs/seajs/2.1.1/目录,我们发现里面有个 plugin-storeinc.js,这正是 storeinc seajs 插件本身.打开 app 目录下的 hello.html,里面已经嵌入了 storeinc 插件如清单 3 所示

清单 3. 嵌入 storeinc 代码


1<script src="../sea-modules/seajs/seajs/2.1.1/plugin-storeinc.js"></script>
并且通过清单 4 的代码启用了本地存储和增量更新插件清单 4.启用 storeinc 插件

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

//
Set configuration

var
version='1.0.6'
//这里是版本,使用 storeinc 就要遵循它的规范

seajs.config({

base:
"../sea-modules/",

alias:
{

"jquery":
"jquery/jquery/1.10.1/jquery.js"

}

});

//使用 use 来启用 storeinc 插件

seajs.use('plugin-storeinc',
function(store)
{

/storeinc
插件设置

//store 表示启用本地存储

//inc 表示启用增量更新插件

//jsver 表示版本

//aliasver 表示定义了别名的 JavaScript 的版本,这个跟其他脚本做了区分,不走增量更新

//debug 表示是不是在调试状态,如果是则不走本地存储和增量更新

store.configStroreInc({'store':true,'inc':true,'jsver':version,'aliasver':'1.10.2','debug':false});

//
For development

if
(location.href.indexOf("?dev")
>
0)
{

seajs.use("../static/hello/src/main");

}

//
For production

else
{

seajs.use("examples/hello/"+version+"/main");

}

});

接下来安装构建工具 spm-storeinc-build

npm install spm-storeinc-build -g

然后到 static/hello 目录下修改 package.json 构建配置文件为清单 5 所示内容

清单 5. 构建配置文件内容


1

2

3

4

5

6

7

8

9

10

11

12

13

{

"family":
"examples",

"name":
"hello",

"lastversion":"1.0.5",
//上个版本号(如果是第一次可以不写)

"version":
"1.0.6",//本次版本号

"chunkSize":12,//增量更新块大小,填
12 即可,也可以填其他

"spm":
{

"alias":
{

"jquery":
"jquery"

},

"output":
["main.js",
"style.css"]

}

}

然后在该目录下运行 spm-storeinc-build 构建工具下会在 dist 目录下生成混淆后的 js 文件.如图 5 所示:

图 5.使用构建工具构建代码后的文件






然后我们将 1.0.6 文件夹放到,sea-modules\examples\hello 文件夹下(js 资源从这个目录拉取)

启动 web 服务器,在浏览器输入 http://localhost/spm/app/hello.html ,访问正常,看一下网络请求,由于是第一次访问所以,看到 js 访问了 main-1.0.6.js 这个文件,如图 6 所示

图 6. 1.0.6 版本 js 第一次访问时的 http 请求截图






另外看一下 localstorage,已经把这个文件内容和版本号存入了本地存储,如图 7 所示

图 7 1.0.6 版本 js 第一次访问时的本地存储内容截图






再刷新一次,已经不会有 main-1.0.6.js 这个请求,但是功能 ok,说明程序是从本地存储读取 js 内容的,较少了网络请求,加快了速度并减少了流量

接下来看下增量更新,我们分别修改 static\hello\src 目录下的 main.js 和 spinning.js

在 main.js 和 spinning.js 里面修改几个 console.log 的日志输出修改原来的 1.0.6 为 1.0.7

分别如图 8,图 9 所示

图 8. 1.0.7 main.js 修改内容截图






图 9 1.0.7 spinning.js 修改内容截图






然后修改 package.json,把版本修改为 1.0.7,把上个版本修改为 1.0.6 如图 10 所示:

图 10. 1.0.7 打包 package.json 版本信息内容






然后执行 spm-storeinc-build 命令进行构建,这时候发现在 dist 目录下生成了一个 1.0.7 目录,如图 11 所示。

图 11. 1.0.7 版本 js 构建后的文件列表






发现多了一个 main-1.0.6_1.0.7.js 的 JavaScript 文件这个文件就是传说中的增量文件了,就是说这个文件的内容是 main.js 从 1.0.6 变化到 1.0.7 所修改的内容,我们打开文件可以看到如图 12 所示内容

图 12. 增量文件 main-1.0.6_1.0.7.js 内容






发现这里只有刚才修改的 JavaScript 的更新内容,然后我们将 1.0.7 文件夹放到,sea-modules\examples\hello 文件夹下,并修改\app\hello.html 把版本改为 1.0.7,然后重新访问
http://localhost/spm/app/hello.html ,这时候发现浏览器访问的是 main-1.0.6_1.0.7.js 这个增量文件,如图 13 所示

图 13. 1.0.7 版 js http 请求截图






整个页面功能也都 ok,在看看 console 平台,发现我们刚才的修改已经生效,console 打出了 1.0.7 版本的相关信息,如图 13 所示

图 13. 1.0.7 版 js 的 console 输出截图






说明增量更新已经生效,浏览器以最小的流量损耗是想了一次 JavaScript 版本更新,以这个 demon 为例,如果走普通的版本更新方式,需要全两下载 main.js,需要下载一个 2k 的文件,而走增量更新则只需要下载 0.5k 左右的文件,流量大大节省!

增量文件 servlet 代理的实现

上面这个例子,增量文件的生成是通过 spm-storeinc-build 来离线生成的。我们这里在介绍一下通过 java servlet 来动态生成增量更新。原理就是通过前台过来的请求获取本次和上次版本号,然后 servlet(源代码可以看下载包里的 StoreIncServlet)通过比较两个版本 js 之间的不同生成增量更新文件,并返回给浏览器端。具体的代码可以在附件或者 https://github.com/luyongfugx/storeinc 获得,这里就不详细讲了。

容错处理

增量更新功能依赖于 localstorage,对于不支持本地存储的浏览器,storeinc 会自动切换到全量更新模式,所以不会造成使用上的问题,但是不支持本地存储的浏览器将无法增量更新。

由于增量更新跟传输协议无关,所以无论是 HTTP 还是 HTTPS 都可以使用。

总结

到此为止,通过本地存储,我们在不更新版本的时候基本上消灭了 JavaScript 相关的 HTTP 请求,在版本更新的时候也基本上做到了修改什么内容就更新什么内容。为 web 程序节省了 90%以上的 JavaScript 流量耗费!

来源:developerworks
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: