blog icon indicating copy to clipboard operation
blog copied to clipboard

资源定位中md5戳的计算过程

Open fouber opened this issue 9 years ago • 89 comments

要实现完整的md5计算,最终必须将task-based的流程转变成one-task形式。此处给出相关说明:

假设我们有三个文件,比如 foo.coffee, foo.scssfoo.png,文本文件的内容为:

  • foo.coffee

    link = document.createElement 'link'
    link.src = 'foo.scss'   # 此处要引用scss文件
    link.rel = 'stylesheet'
    document.head.appendChild link
    
  • foo.scss

    .foo {
        .bar {
            background: url(foo.png);  //此处要引用foo.png文件
        }
    }
    

最终形成这样一种资源引用关系:

+------------+  +----------+  +---------+
|            |  |          |  |         |
| foo.coffee <--+ foo.scss <--+ foo.png |
|            |  |          |  |         |
+------------+  +----------+  +---------+

当我们要计算foo.coffee的md5戳的时候,其实是一个这样的过程:

-> 读入foo.coffee的文件内容,编译成js内容
-> 分析js内容,找到资源定位标记 'foo.scss'
-> 对foo.scss进行编译:
    -> 读入foo.scss的文件内容,编译成css内容
    -> 分析css内容,找到资源定位标记 ``url(foo.png)``
    -> 对 foo.png 进行编译:
        -> 读入foo.png的内容
        -> 图片压缩
        -> 返回图片内容
    -> 根据foo.png的最终内容计算md5戳,替换url(foo.png)为url(/static/img/foo_2af0b.png)
    -> 替换完毕所有资源定位标记,对css内容进行压缩
    -> 返回css内容
-> 根据foo.css的最终内容计算md5戳,替换'foo.scss'为 '/static/scss/foo_bae39.css'
-> 替换完毕所有资源定位标记,对js内容进行压缩
-> 返回js内容
-> 根据最终的js内容计算md5戳,得到foo.coffee的资源url为 '/static/coffee/foo_3fc20.js'

整个计算过程是一个递归编译的过程,计算文件的摘要信息应该根据文件的 最终内容计算 ,所以这个过程中要加入对sass、coffee、图片的编译和压缩处理,从而能得到真正的 最终内容,这就等同于要把所有文件的处理过程整合在一次流程中,所以引入md5计算,对整个构建系统的设计影响是非常大的。

在task-based的构建机制中,task之间没有办法在处理一个文件的过程中暂停,然后去对另一个文件完成完整流程处理得到内容再继续当前流程。task-based之间仅仅是任务的调度,使得部分构建信息在调度的过程中失去了“上下文环境”,无法形成对同一个文件内容的管道式处理过程。假设上述过程我们用task-based的系统构建,会变得非常复杂,有兴趣的朋友可以尝试一下,把你们的想法写在下面。

用 F.I.S 包装了一个 小工具 ,完整实现整个资源部署方案,并提供了源码对照: 源码项目:fouber/static-resource-digest-project · GitHub 部署项目:fouber/static-resource-digest-project-release · GitHub 部署项目可以理解为线上发布的结果,可以在部署项目里查看所有资源引用的md5化处理。

fouber avatar Oct 30 '14 06:10 fouber

md5 的值主要依赖于文件的内容,而且当文件变化 md5 值也需要变化(包括依赖)。但是不一定需要替换后才能去 md5,首要关注的是文件的变化,所以我觉得只要将依赖文件计算出来,将他们的内容进行 md5 计算就可以了。

popomore avatar Oct 30 '14 06:10 popomore

@popomore

这样做不够严谨,以js、css为例,内容变化还可能是注释修改,并不会影响最终内容的改变

fouber avatar Oct 30 '14 07:10 fouber

@fouber 但是文件确实变化了,压缩也不一定 100% 正确的,压缩工具修改也会造成输出变化。

popomore avatar Oct 30 '14 07:10 popomore

@popomore

而且必须是先替换引用资源的md5,才能再计算当前内容的md5,否则某次修改,我们只改了图片,其他js、css没有改动,只关注文件本身内容的md5算法就会认为资源没有修改,最终导致上线后没有更新这些文件,而最终修改的图片没有生效

fouber avatar Oct 30 '14 07:10 fouber

@popomore

同一份文件内容,压缩工具处理后的结果不会有变化的,这个已经证实过了

fouber avatar Oct 30 '14 07:10 fouber

是不是md5其实无所谓,关键是计算出上一次部署文件和本次部署文件是否有差异。大概7、8年前就做过这样的方案——拿本次部署对应的资源文件(未加版本号的)比对上次部署对应的资源文件(未加版本号的),计算出差异,然后计算依赖,得到最终所有要变的资源文件集,所有变更的文件自增版本号,不变的用上次的版本号,更新所有依赖链接为最终的path。

hax avatar Oct 30 '14 07:10 hax

压缩导致结果变化的情况没遇到过。不过某些大厂有用差异更新的,小差异导致压缩的短变量名大量变化从而增加了diff大小的情况,倒是会有的。

hax avatar Oct 30 '14 07:10 hax

@fouber 压缩工具变更肯定会应该输出的,比如自己增加一些元信息,这个不影响压缩效果,也是可能的。

额,你们没有仔细看么,我没有说只是修改文件本身,是所有依赖文件的内容,比如图片改动,对应的 js 文件肯定会发生变化。我的分歧点是不需要坐资源定位标记的替换,其他我也是很认同,我也是这么做的。

md5 主要的作用是避免文件的覆盖,当文件变化所生成的文件变化必须不同,所有生成的 md5 只要考虑是否已经考虑到文件变化就可以了,至于是否必须为处理后的文件我就不做评价了。

popomore avatar Oct 30 '14 07:10 popomore

@hax

用md5处理只是一种便捷方式而已,确实并不重要。md5无需关系版本diff,这是它的一个小优势,最终面向的原理是完全一致的。

fouber avatar Oct 30 '14 07:10 fouber

@popomore

替换资源定位标记之后再对文件本身求md5,这样可以自然引起当前资源的内容变更,便于形成递归处理逻辑,在工具设计上比较容易实现而已。

如果用别的方式先确认了内容变更的依据,最终再去替换定位标记也是一样的

fouber avatar Oct 30 '14 07:10 fouber

很高兴加入讨论,我想象中Task-based的步骤:

  • Task-1: 编译所有coffee -> js

  • Task-2: 编译所有scss -> css

  • Task-3: 压缩所有图片

  • Task-4: 压缩所有jscss(去掉空格,注释等)

  • Task-5: 将上述文件和其他网站所需文件复制至一个文件夹中

  • Task-6: 扫描所有的 JS 文件,构建依赖树,如下:

        a.js   
          - b.js   
            - foo.png   
            - bar.png
          - c.js
            - baz.css
          - d.js
    
  • Task-7: 文件 rev,从底往上递归以下操作:

    1. 找到树中某子节点的文件,计算其 MD5 值,将结果记录下来

      如 MD5 值结果:
        /dir1/foo.png - 7b561097
        /dir2/bar.png - 5d2d4459
      
    2. 找到子节点的上级文件,替换地址引用

      b.js 中替换内容:
        /dir1/foo.png -> /dir1/7b561097.foo.png
        /dir2/bar.png -> /dir2/5d2d4459.bar.png
      
    3. 找到子节点的其他同级节点,计算其 MD5 值,进行和前两步一样的操作

      如 MD5 值结果:
        /css/baz.css - 20d37590
      c.js 中替换内容:
        /css/baz.css -> /css/20d37590.foo.css
      
    4. 确定没有同级节点,往上一层,循环前面的操作

  • Task-8: 重命名所有经 MD5 值计算过的文件

  • Task-9: 替换 html 文件中 JS 和 CSS 的地址引用

        如:    
          <script src="app/a.js"></script> -> <script src="app/9049f49e.a.js"></script>
    

我认为整个过程中,文件编译、图片压缩这些步骤应该先做了,这应该是和你的步骤的主要区别吧。 如有理解错误请指正。

chuyik avatar Oct 30 '14 07:10 chuyik

@chuyik

注意,源码中,coffee里写的是baz.scss,当先做了coffee->js和scss->css之后,资源引用路径指向已经发生了变化

fouber avatar Oct 30 '14 07:10 fouber

@fouber 如果 coffee 中写的是 baz.css 呢?

chuyik avatar Oct 30 '14 07:10 chuyik

@chuyik

恩,如果coffee中写了baz.css是可以的,但这意味着要让工程师在编码过程中带上对构建工具处理的思考,资源定位不能以原始的工程路径为依据了,而是以构建的中间产物为依据,我觉得使用效果会大打折扣,本身并不是很完美的。

如果构建工具对每个文件对象的编译只有一个compile函数,在这个compile函数中,会经历coffee->js(没有临时文件,只是返回内容),压缩,包装等内容修改,那么这个过程就变得很简单了:

var useHash = true;
var file = new File('a.coffee');
compile(file);
file.getContent().replace(/正则或者随便什么分析资源定位标记/, function(m, $1){
    var f = new File($1);
    compile(f);
    return f.getUrl(useHash);
});
return file.getUrl(useHash);

fouber avatar Oct 30 '14 07:10 fouber

@fouber 嘿嘿,其实编译打包后的文件路径和工程路径可以人为地变得一致,如图: image

我想你给的代码并没有解决你提及的「多级文件引用」(whatever it named) 问题,递归还是要有一个同级往上的步骤,不然 MD5 值会改来改去的,不过也差不多这个过程吧...

chuyik avatar Oct 30 '14 08:10 chuyik

@chuyik

不好意思,之前的回复写的着急了一些,详细的是这样的:

function compile(file, useHash){
    var content = file.getContent();
    content = parse(content, file.ext);      // less2css, coffee2js
    content = content.replace(/正则或者随便什么分析资源定位标记/, function(m, $1){
        var f = new File($1);
        compile(f);               // 递归编译
        return f.getUrl(useHash); // 计算带hash的路径引用
    });
    content = optimize(content, file.ext);  // 压缩
    file.setContent(content);
    return file;
}

var file = new File('foo.coffee');
compile(file);
console.log(file.getContent());

fouber avatar Oct 30 '14 08:10 fouber

/Workspace/git/static-resource-digest-project$ rsd release --md5 --dest ./output No command 'rsd' found, did you mean: Command 'xsd' from package 'mono-devel' (main) Command 'rbd' from package 'ceph-common' (main) Command 'rs' from package 'reminiscence' (multiverse) Command 'rs' from package 'rs' (universe) Command 'sd' from package 'sd' (universe) Command 'rs6' from package 'ipv6toolkit' (universe) Command 'red' from package 'ed' (main) Command 'rsc' from package 'radare-common' (universe) Command 'rtd' from package 'skycat' (universe) Command 'esd' from package 'pulseaudio-esound-compat' (main) Command 'rsh' from package 'rsh-redone-client' (universe) Command 'rsh' from package 'rsh-client' (universe) Command 'nsd' from package 'nsd' (universe) Command 'srsd' from package 'srs' (universe) Command 'rad' from package 'radiance' (universe) rsd: command not found 安装完成后在克隆的项目根路径下执行release操作报这个问题,求解

shunyitian avatar Oct 31 '14 03:10 shunyitian

@shunyitian

应该没有安装成功吧,或者安装的时候没有加 -g 参数,把命令安装到全局上去

fouber avatar Oct 31 '14 03:10 fouber

安装的时候提示了一个这个npm WARN optional dep failed, continuing [email protected]

shunyitian avatar Oct 31 '14 03:10 shunyitian

@shunyitian 这个可以忽略,你机器编译 fsevents 失败了

popomore avatar Oct 31 '14 04:10 popomore

我在重装一次试试

shunyitian avatar Oct 31 '14 04:10 shunyitian

@shunyitian

不好意思,确实是一个bug,刚刚更新了,再安装一次就好了

fouber avatar Oct 31 '14 05:10 fouber

@fouber 就当我帮忙了,哈哈

shunyitian avatar Oct 31 '14 05:10 shunyitian

@shunyitian

非常感谢

fouber avatar Oct 31 '14 05:10 fouber

非常感谢你提供的工具,我用过之后非常的好用,非常适合小公司,小项目,之前在使用grunt时就遇到静态资源经过grunt处理过后还要去手动去改成处理后文件的路径实在是麻烦,但不知道grunt里有没有此类的解决,不过此工具已经解决,还有一个就是md5摘要形式发布了文件不会有缓存的问题了,以前我们修改后图片,在客户那儿没有反应,最后发现是文件缓存,特别是在手机上;

在使用的过程中我遇到了以下两个问题: 1.每次修改文件之后,发布代码,会在原来的基础上重新生成了一个文件,这样的话提交线上不用的文件是不是就多了,能否在原来的文件的基础上修改只是修改原来文件的名称; 2.我新建一个二级目录view; view/index-view.php中静态资源的路径没有变化;

FEsy avatar Oct 31 '14 06:10 FEsy

@FEsy

  1. 这种存储成本其实是非常非常小的,很多工程师担心未来将面临一定的清理问题。但经过追踪统计发现,实际文件冗余的数量并没有想象中的多,虽然web应用有“小步快跑”的小版本迭代特征,发布频率非常高,但每次修改的文件是比较少的,基础库、组件库、图标icon等资源在短时间内变化的概率并不高,实际发生冗余的文件主要集中在部分业务的js、css代码上,其增长量很有限。所以清理的问题通常要许多年才发生一次,根据访问日志编写简单的脚本清理即可。
  2. 新建的二级view,写的资源引用如果是相对路径,都是以文件所在位置为依据的,所以资源路径应该以 ../ 开头吧

fouber avatar Oct 31 '14 06:10 fouber

@fouber 非常感谢,2.是文件路径的问题,我是以php中引入view路径为准的,此工具是以文件位置为依据: 对于1问题我觉得单独只是为了发布到线上,我觉得问题不大,主要是如果我边写边监听(sass->css)会产生很多文件的;

FEsy avatar Oct 31 '14 07:10 FEsy

@FEsy

本地开发不用加 --md5 参数哦

fouber avatar Oct 31 '14 07:10 fouber

css 里面的资源定位还算容易。但 js 中的资源链接是通过字符串拼接生成的,那就无解了吧?

maplejan avatar Nov 01 '14 04:11 maplejan

@maplejan js的话,要提供编译用的函数来标记资源定位,并且只能使用字面量声明,比如

var url = __uri('a.png');

构建之后变成:

var url = '/static/img/a_0d4f22a.png

如果需要运行时变量控制多个资源的选取,可以这样做:

var imgs = {
    a: __uri('a.png'),
    b: __uri('b.png'),
    ...
};

var name = 'a';
var url = img[name];

fouber avatar Nov 01 '14 04:11 fouber

@fouber 你好,我本地已经完成了资源的合并,发布后,预览页面时并没有合并资源;

FEsy avatar Nov 04 '14 10:11 FEsy

@FEsy

资源合并是另外一个问题,简单的资源合并可以用__inline实现( http://fis.baidu.com/docs/more/fis-standard-inline.html ),如果是复杂的实现,最好看看这里:https://github.com/fex-team/fis/wiki/%E5%9F%BA%E4%BA%8Emap.json%E7%9A%84%E5%89%8D%E5%90%8E%E7%AB%AF%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E6%8C%87%E5%AF%BC

我有几个demo项目:

这些项目用rsd一样能运行起来,你感受一下哈

fouber avatar Nov 04 '14 10:11 fouber