fouber / blog

没事写写文章,喜欢的话请点star,想订阅点watch,千万别fork!
22.63k stars 2.37k forks source link

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

Open fouber opened 10 years ago

fouber commented 10 years ago

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

假设我们有三个文件,比如 foo.coffee, foo.scssfoo.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化处理。

popomore commented 10 years ago

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

fouber commented 10 years ago

@popomore

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

popomore commented 10 years ago

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

fouber commented 10 years ago

@popomore

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

fouber commented 10 years ago

@popomore

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

hax commented 10 years ago

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

hax commented 10 years ago

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

popomore commented 10 years ago

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

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

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

fouber commented 10 years ago

@hax

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

fouber commented 10 years ago

@popomore

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

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

fouber commented 10 years ago

@chuyik

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

fouber commented 10 years ago

@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 commented 10 years ago

@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 commented 10 years ago

@popomore @hax @chuyik

源码项目:fouber/static-resource-digest-project · GitHub 部署对照:fouber/static-resource-digest-project-release · GitHub

shunyitian commented 10 years ago

/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操作报这个问题,求解

fouber commented 10 years ago

@shunyitian

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

shunyitian commented 10 years ago

安装的时候提示了一个这个npm WARN optional dep failed, continuing fsevents@0.2.1

popomore commented 10 years ago

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

shunyitian commented 10 years ago

我在重装一次试试

fouber commented 10 years ago

@shunyitian

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

shunyitian commented 10 years ago

@fouber 就当我帮忙了,哈哈

fouber commented 10 years ago

@shunyitian

非常感谢

FEsy commented 10 years ago

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

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

fouber commented 10 years ago

@FEsy

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

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

fouber commented 10 years ago

@FEsy

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

maplejan commented 10 years ago

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

fouber commented 10 years ago

@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];
FEsy commented 10 years ago

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

fouber commented 10 years ago

@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一样能运行起来,你感受一下哈

FEsy commented 9 years ago

@fouber 今天感受了一下"php版静态资源管理系统示例"这个demo,又有问题问你了, 我们公司可能会在bae,sae做一些开发, 1.我们一开始相当于就在线上开发了,使用fis产出目录是会跟原来的重合了,不知道你有么有好点的建议 2.假如我想发布在某个目录下而不是根目录,那文件的路径改如何修改,因为文件文件相对项目根目录的路径,如http://1.mashuangshuang.sinaapp.com/rsd/下文件就无法访问

fouber commented 9 years ago
  1. fis release -d xxx把代码构建到某个目录下,用这个目录下的代码去上线
  2. 用roadmap.path可以控制一切文件属性,比如发布路径、访问url等等

    fis.config.set('roadmap.path', [{
       reg: /^\/(.*)/,
       release: '/subpath/$1'
    }]);
thethreeonee commented 9 years ago

@fouber 用 fis 的时候,发现 LESS 中使用相对路径定位资源无效的问题。

background: url(../img/a.png);
fouber commented 9 years ago

@celtavonce

应该不会,一般相对路径无效都是因为没有以当前为基础进行资源定位

ghost commented 9 years ago

@fouber
今天用fis的时候,出现一点小问题: a.html文件通过引入b.html,在b.html中我引用了很多js和css文件,在我的项目文件目录执行命令rsd release -m -d ../output之后文件b.html中引用的css和js文件没有加上md5戳。

fouber commented 9 years ago

@carriey

相对路径写错了吧,写的时候是以文件本身为依据查找相对路径的,不是已嵌入后的文件为依据

ghost commented 9 years ago

@fouber
非常感谢fouber,更改完相对路径后我的上一个问题已经解决了,但是还是遇到了其他的问题,想请教fouber,我在项目中使用了handlebars预编译模板,在模板中引用的图片的相对路径是正确的, image 通过预编译以后在生成的js文件中相对路径也是正确的,如下图 image 但是使用了fis以后预编译生成的js文件引用的图片依然没有加上md5戳,请问这有解决办法吗?

fouber commented 9 years ago

@carriey

预编译过程,会把handlebars当做js来处理,你可以理解为:

handlebars内容 → handlebars预编译 → js内容 → 资源定位处理 → ...

对js内容进行资源定位处理的时候,识别的是 __uri(xxx) 所以,你的html写法就不受用了。当然,我们也没有直接的办法把这里编译成js的资源定位语法,所以我们可以通过传入模板变量的方式资源路径字符串传过去,从而实现你的需求:

{{#stockList}}<li data-id="{{id}}">
    <img class="lazy" data-original="{{stockImg}}" src="{{placeholderImg}}" alt="{{name}}">
    <footer>
        <h2>{{name}}</h2>
        <span>¥{{priceout}}</span><strong><i class="icon-like"></i>{{liked}}</strong>
    </footer>
</li>{{/stockList}}

可以看到,我在handlebars中把原来的图片变成了一个模板变量,那么,在使用这个模板的js中就可以这样写:

var tpl = __inline('tpl.handlebars');
function render(data){
    data.placeholderImg = __uri('../images/stockbg.png'); //追加图片资源
    return tpl(data);  //返回渲染结果
}
ghost commented 9 years ago

@fouber
谢谢 fouber耐心的解答,我还有几个问题: 1.fis有没有什么方法可以控制只给部分文件添加md5戳。 2.fouber有没有用过gulp给文件添加版本号?

fouber commented 9 years ago

@carriey

问题1,可以

fis是通过给文件添加各种属性来控制插件工作的,其中控制是否加md5戳的文件属性就是useHash,比如我希望部分文件没有md5戳,那么只要这些文件对象的useHash属性为false就可以了,其配置为:

fis.config.set('roadmap.path', [
    {
        reg: 'lib/**',
        useHash: false
    }
]);

这样,lib目录下的所有文件在构建的时候都有了useHashfalse的属性值,负责添加md5戳的插件会根据这个属性来判断是否加md5(此属性默认是true),当然,有的时候我们不是希望某个目录都不加,而只是个别文件加,那么可以通过配置多条规则来指定那些文件。我个人比较推荐将“加与不加”这件事在架构中设计成一种规范,比如我规定:

所有以 _ 开头的文件都不要加md5戳

其配置为:

fis.config.set('roadmap.path', [
    ...
    {
        reg: /\/_[^\/]+$/,   //用正则匹配以_开头的文件
        useHash: false
    }
    ...
]);

这样,我们在项目下一眼就能看出来哪些文件是不会产生md5戳的了。此外还有一些供插件识别的其他属性也通过这种方式给文件分配属性。

问题2,有了解

有看过gulp/grunt的usemini和rev工具,目前最好的加版本戳的方式就是用文件内容的md5戳了,grunt/gulp由于采用task-based的调度方式来使用插件,个人觉得没有办法完整实现版本戳的替换,其原因我也写在前面了

ghost commented 9 years ago

@fouber
哈哈 ,真的非常感谢fouber,我遇到的问题都已经全部解决了,由于我们是用gulp来编译sass,handlebars以及压缩代码,然后又用fis添加版本号,感觉好绕啊,下面我要好好看看fis,准备全部转成fis,如果遇到问题还请fouber能够继续帮忙解决.

fouber commented 9 years ago

@carriey

fis也有sass、handlebars的插件

ghost commented 9 years ago

@fouber 有2个问题: 问题1:a.min_a4bbf66.js内容发生变化以后,生成了新的md5戳a.min_8dbb11c.js这两个文件会并列存在,有没有什么配置方法可以只保留最新的一个文件。 image

问题2:执行fis的命令以后,终端会显示如下图: image 但是执行gulp的命令,终端会把编译的详细信息显示出来: image 请问fis可不可以也像gulp一样执行命令以后,可以把编译文件的详细信息展示出来。

fouber commented 9 years ago

@carriey

问题一:fis并不会存储每次构建的文件列表,然后两次构建之间进行diff。fis期待的用法是把代码构建到一个空目录下,并将其部署上线。 问题二:fis构建的时候,每个点代表一个文件,灰色的点表示构建速度较快,白色表示速度一般,红色表示速度很慢。个人使用的时候,感觉不是很需要看详细信息,略无谓。但如果真的要看到构建信息,fis也保留了可以打印全部构建信息的能力,执行release命令的时候,加上 --verbose 参数即可,比如: rsd release -md ../dist --verbose

fouber commented 9 years ago

@liuyuan87

发布了最新版的rsd,你可以再安装一下试试

liuyuan87 commented 9 years ago

@fouber 已经安装上了 thanks ~

feifeipan commented 9 years ago

@fouber FIS非常棒,最近我们也在尝试构建前端的解决方案。 针对md5的计算我个人更倾向@chuyik 的

  1. 遍历工程/project下的所有文件,将html/css/js中的引用路径全部替换为相对于project的路径,保证资源都有唯一的引用路径
  2. 所有资源文件生成md5文件,并形成一张map表
  3. 根据map表替换project中每个文件内容中的引用路径 不知道是否描述清楚,请问一下这样的方式大家觉得如何,会有什么弊端吗?
fouber commented 9 years ago

@feifeipan

正如我这篇blog所讲的,md5计算必然是一个“递归计算”的过程,只能在一次处理中以递归的方式完成计算,而不能分步骤批量处理。

fouber commented 9 years ago

@feifeipan

我用一个图示来展示你的方案的计算过程:

如上图,假设我们的项目有三个文件,index.html、a.css、a.png,其中a.css中引用了a.png,index.html中引用了a.css,如你所述方案,采用三步进行处理:

  1. 获取所有文件
  2. 逐个计算文件md5,得到表
  3. 逐个文件替换对应的资源引用

问题就出在步骤2上,步骤2计算的文件md5并不是文件最终的md5,由于分步骤计算,第3步替换引用使得文件内容修改,文件最终的md5其实和第2步算出的并不一致,这导致css和图片的依赖关系没能建立起来。比如我们某次迭代,只更新了图片,按照上述计算,我们将得到:

请注意,我们在第2步中,如果仅仅根据单一文件内容进行md5计算,那么,只有图片因为内容改变修改了md5戳(红色字标出),但css的内容没有发生变化,所以针对css计算的md5戳与上个版本一致(蓝色字标出)

接下来进行步骤3的替换,严重注意,此时index.html替换后引用的css地址相比上个版本是 没有改变的,这意味着,这次发布的版本,虽然css内容更新了图片的地址,但index.html中却没有更新css路径,进而导致浏览器最终还是使用了上个版本的css文件,我们这次的图片更新没!生!效!

Galen-Yip commented 9 years ago

云兄 @fouber 看了你在https://github.com/fouber/blog/issues/4#issuecomment-104861981 里回复@tm-roamer的。我也遇到了跟他同样的问题,公司原因必须用requirejs。我自己私下里其他项目也有用fis。 相信云兄对grunt也研究比较深吧?我的问题不在requirejs合并,我遇到的一个问题跟你上面讲的其实是同一个问题,我是用grunt做构建,用了filerev和usemin。但是问题就是 filerev名字md5化的时候,是一次性全部变的,之后才进行资源替换,这时候的md5有些并不是最后的md5.。。。 其实我就想问下,有没有一个好的解决方案,基于grunt的话,当然如果基于fis,能结合requirejs也行.... 希望云兄能解惑~~