fouber/blog

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

fouber opened this issue · 86 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化处理。

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

@popomore

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

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

@popomore

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

@popomore

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

hax commented

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

hax commented

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

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

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

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

@hax

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

@popomore

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

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

@chuyik

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

@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);

@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());

/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

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

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

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

我在重装一次试试

@shunyitian

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

@fouber 就当我帮忙了,哈哈

@shunyitian

非常感谢

FEsy commented

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

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

@FEsy

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

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

@FEsy

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

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

@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

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

@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

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

  1. fis release -d xxx把代码构建到某个目录下,用这个目录下的代码去上线

  2. 用roadmap.path可以控制一切文件属性,比如发布路径、访问url等等

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

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

background: url(../img/a.png);

@celtavonce

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

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

@carriey

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

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

@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);  //返回渲染结果
}

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

@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的调度方式来使用插件,个人觉得没有办法完整实现版本戳的替换,其原因我也写在前面了

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

@carriey

fis也有sass、handlebars的插件

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

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

@carriey

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

@liuyuan87

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

@fouber
已经安装上了
thanks ~

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

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

@feifeipan

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

@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文件,我们这次的图片更新没!生!效!

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

@Galen-Yip

没有特别好的方案,基于grunt的话只能自己写一个这样的处理任务,而且要一次性替换。这个替换必须发生在所有task最后,因为接下来就要把这些文件产出,不能再做其他修改。相应的,还要解决中间文件问题,比如less被编译成css,那么HTML中引用less的地方要能识别这种映射,因为grunt是task-based的,先编译less生成css,而html中的资源引用要么一开始就写成css,与最终结果对应上,要么就需要自己识别对应关系,中间文件问题在前面 @chuyik 的回答中也有讨论。

中间文件问题、编译后会修改发布路径的资源的问题、md5递归计算问题,我觉得这些在task-based工具设计下是非常恶心的问题,他们明显意味着前端构建是one-task的过程,在没有nodejs没有grunt的年代我们就用php写过类似grunt的任务调度工具,后来走不下去了,才设计了fis这种模式。

总的来说,要想实正确的处理流程,我在前面回复 chuyik 的内容有提到,看这里 #5 (comment) 你需要一个同步的,编译文件的 compile 函数,用这个函数来一次性完成对一个文件的构建,把构建流程写在这个函数中。这样我们就能实现正确的md5替换,因为在compile一个文件的时候,遇到资源定位,我们可以立即调用compile去编译被引用文件,从而获取文件的最终内容,并计算其md5戳

@fouber 谢谢云兄!对 确实也是这样。grunt的task-based必须是一个任务完成再执行另外一个,不能暂停。我本来的思路是想着先分析出文件依赖,然后根据这个依赖,filerev去md5化依赖最深的文件,完成这个,usemin去替换所有的资源的引用。直到这个依赖树完成,构建就完成了。但是去弄这个东西貌似十分的复杂,不知道fis是否也是类似的思路,方便的话,有没fis这一块的原理能够贴出来指导一下。

@Galen-Yip

只要不是 task-based 的,这件事就非常简单了,设计一个compile函数,可以对任何文件进行同步编译,并得到文件编译后的内容,然后用这个内容进行计算即可。

fis的构建过程是这样的:

var files = fis.getAllFiles(); // 获取所有项目文件
files.forEach(function(file){
    fis.compile(file);   // 逐个编译文件
});
fis.package(files);   // 针对全部文件进行打包
fis.deploy(files, cli.opt.dest);   // 发布到指定目录

fis的构建没有中间文件,都是在内存中维护一个files列表,对文件编译的时候只是set文件对象的content属性而已。这里可以再展开一下 fis.compile 函数的内容:

fis.compile = function(file){
    if(fis.cache.expire(file)){  // 检查编译缓存是否过期
        // 缓存已过期
        fis.lint(file);     // 校验
        fis.parse(file);  // 语法编译,比如less -> css,coffee -> js
        fis.magic(file); // 资源定位和内嵌处理
        fis.optimize(file);  // 压缩
        fis.cache.save(file);   // 保存缓存
    } else {
        // 缓存未过期
        fis.cache.restore(file);  // 从缓存恢复内容
    }
};

其中,fis.magic是资源定位的,其中的代码大致是:

fis.magic = function(file){
    if(file.isHtmlLike){
        // 如果文件是类html文件,我这里写了一个大正则,分析其中各种资源定位标记:
        var reg = /(<script(?:(?=\s)[\s\S]*?["'\s\w\/\-]>|>))([\s\S]*?)(?=<\/script\s*>|$)|(<style(?:(?=\s)[\s\S]*?["'\s\w\/\-]>|>))([\s\S]*?)(?=<\/style\s*>|$)|<(img|embed|audio|video|link|object|source)\s+[\s\S]*?["'\s\w\/\-](?:>|$)|<!--inline\[([^\]]+)\]-->|<!--(?!\[)([\s\S]*?)(-->|$)/ig;
        file.content = file.content.replace(reg, function(m, path){
            // 假设我们匹配到的资源路径是path变量
            var f = fis.file(path, file.path);  // 根据当前文件路径计算被引用文件
            fis.compile(f);   // 【关键】在这里,同步的递归编译,
            return f.getUrlWithHash();  // 编译后的文件就能知道真正的md5了
        });
    } else if(file.isJsLike){
        ...
    } else if(file.isCssLike){
        ...
    }
};

最最关键的就是这个 fis.compile 函数,它是同步的单文件构建函数,这个函数能完整处理文件的全部构建过程,我们需要在这个函数的处理过程中再同步的去构建其他文件,只有这样才能正确解决文件之间引用嵌入等依赖问题。

fis构建没有中间文件,都是内存中操作内容,最后把整个files数组deploy出来,完成输出,deploy的过程可以是写入磁盘,也可以是ftp上传,或者推送其他发布平台。

我个人的定义是:

fis是一种工程化构建工具。

@fouber 谢谢你的解答,我的先前提到的方案确实有问题。我后续又改进了一个方案(先暂时忽略less,coffee等过程)
设定有project项目目录

  1. 遍历project,计算出所有文件的依赖关系,如下
     {
    "css/demo.css": {
        "deps": ["img/plus.png"],
        "md5": false
    },
    "cache.html": {
        "deps": ["js/index.js"],
        "md5": false
    },
    "img/minus.png": {
        "deps": [],
        "md5": false
    },
    "img/plus.png": {
        "deps": [],
        "md5": false
    },
    "img/running.png": {
        "deps": [],
        "md5": false
    },
    "img/loading.gif": {
        "deps": [],
        "md5": false
    },
    "js/index.js": {
        "deps": ["css/demo.css"],
        "md5": false
    },
    "css/magnific-popup.css": {
        "deps": ["css/demo.css", "img/plus.png", "img/minus.png"],
        "md5": false
    }
}
  1. 每次都读取file_deps_tree,获取deps为空或者deps已经生成md5的文件,push到一个数组中,然后针对这批文件生成md5,再递归调用此方法,直到所有的文件都生成完毕

    function entry() {
    console.log("Start generate md5 files.");
    var batch = [];
    for (var key in file_deps_tree) {
        if (!hasGenerateMd5(key) && readyGenerateMd5(key)) {
            batch.push(key);
        }
    }
    if (batch.length == 0) {
        console.log("Finish generate md5 files.");
        return true;
    } else {
        generate_md5_batch(batch, entry);
    }
    

    }

@fouber 你的思路是从上往下递归,我的想法是从下至上。我这边都运行ok的,不知道是否有情况不兼容?

另外,根据你上面提到的fis的思路,我针对文件中替换路径有一个思路:

以你上面提到的foo的例子,假设目录结构为

project
    -foo.html
    -js
        -foo.js
    -css
        -foo.css
    -img
        -foo.png

在foo.js中引用了css

var csslink = __uri("../css/foo.css");

Step1 将所有的引用转换为相对于project的路径

var csslink = "css/foo.css" 

Step2 替换为md5文件

var csslink = "css/foo.0ccddf8d145ead4e.css"

Step3 替换为发布之后的链接

var csslink = "http://resource.fis.com/project/css/foo.0ccddf8d145ead4e.css"

不知道上述思路是否有问题?

还有,我理解中Step1和Step2是在编译的时候做的,Step3是在发布的时候做的,是否正确呢?

@feifeipan

我了解可以用递归的方式逐层处理依赖树中的叶子节点,实现逆向的md5计算,但没太看懂你的描述过程,主要是计算完第一批叶子节点的文件之后,这个依赖表变成什么样了?我主要关心的是:

如何把文件中依赖的文件的md5合并到文件本身的md5计算过程中

如果解决了这个问题,无论是从一个文件出发递归计算md5,还是从依赖树叶子节点开始,逐层扫描,应该都是可以解决的,就是逐层扫描的复杂度我个人觉得比自顶向下递归编译的高。

另外,关于第二个问题,就是各个步骤的处理阶段问题,fis不是这样处理的,整个url替换,是在一次文件处理过程中完成,这个替换,可以接收命令行参数来决定是否加domain以及是否加md5戳,这里要提到fis的另外一个核心设计,就是file对象:

var file = fis.file('css/a.css');

这个fis生成的file对象,有一个getUrl方法,基本逻辑是:

File.prototype.getUrl = function(withHash, withDomain){
    var url = this.url;
    if(withHash){
        var content = fis.compile(this); //【关键】
        url = addHash(url, content);   // 加上md5
    }
    if(withDomain){
        url = this.domain + url;
    }
    return url + this.query;
}

fis release的时候,可以设置是否加md5,以及是否加domain,这两个命令参数会在fis替换url的时候传递给这个getUrl作为参数来决定生成的url是否带md5以及是否带域名,这些都是在替换url的过程中做的,没有二次替换。

fis的关键步骤就是这个同步编译文件内容的compile函数,使得fis可以在处理一个文件内容的时候立即编译另外一个文件得到其编译结果而计算md5值,同时替换url还可以决定是否加域名

@feifeipan

你这里用表的形式建立md5戳可能要对整个项目的文件中url资源定位标识做多次的扫描,第一次要抽取依赖关系,然后处理整个依赖关系表,处理完毕后,再二次扫描,替换定位标识,这是第一个问题。

第二个问题就是很难再杂糅处理less、coffee,这个原因也是task-based处理过程造成的,如果less处理在先,task-based恐怕要生成临时文件,对于你的md5插件运算造成一定困难,因为要映射less插件生成的文件和源文件之间的关系从而计算md5;如果less处理在后,这个问题更麻烦。

还有就是md5计算的过程应该也需要综合压缩处理,有时候文件修改只是增加一些注释什么的,正常如果经过压缩再计算md5,内容就不会有改变,也不会引起md5变化,但如果单纯对原内容求md5,可能就会引起内容变更,不是非常严谨。

第四个问题是上面描述的算法应该没有识别循环依赖,如果出现循环依赖,会使得某次依赖扫描中batch数组收集为空,导致md5处理终止,但其实是由于循环依赖引起的。

@fouber 我现在又遇到一个问题。比如页面index.html引用了A.js,
script src="http://resource.baidu.com/A.md5_v1.js" charset="utf-8"

其中A.js为如下,B和C是另外的components
define(["components/B","components/C"],function(B,C){
//.....
});

A发布的时候,已经将B.md5_v1.js,C.md5_v1.js的path config放入了A中。

当B更新了一个版本v2,访问index.html的时候,A引用的B path还是原来的B.md5_v1.js。通过什么办法能够更新到B.md5_v2.js呢?

不知道是否描述清楚了

@feifeipan

正如本blog描述的,md5是一个递归依赖的过程,如果你要解决这个问题,必须这样做:

  1. 先获取B和C的带md5的url,放入到A的path config中
  2. 对有了path config的A的内容求md5在放入index.html中

也就是说A的md5是依赖B、C内容才能计算得到的,而不是分别计算ABC各自的内容在插入到引用的文件中。

MD5内容指纹的计算是要递归进行的!

可能是我的问题没有描述清楚
递归计算md5是肯定的 我们这边也这样实现了的.

我其实是另一个问题,如上述所属,A中require了B和C。已经递归算出A的md5,同时某页面index.html已经引用了A(md5 path).这时B更新了一个版本,(A和B是两个不同component),我如何通知到A修改呢,如何通知到index.html的A引用修改呢?

希望描述清楚了

@feifeipan

如果实现了递归计算,这个问题不是自然而然的就解决了么。。。你确定理解我说的“递归计算”的真正过程么?

所谓的递归计算,不是说我们分别计算A.js,B.js,C.js的md5,然后插入到对应的引用的位置,这根本不是递归。我说的递归是——比如你这个例子,A依赖了B和C,你要:

  1. 必须先计算出B.js和C.js的md5
  2. 把他们俩的md5结果先替换到A.js的内容中,得到A.js的内容这时候已经带上了 B.js 和 C.js 的md5戳,在修改了A.js的内容之后,才对A.js求md5,这个时候,因为B.js和C.js的内容发生改变产生的指纹会变更A.js的内容,从而导致A.js的md5戳有所改变
  3. 把A.js最终的md5结果写入index.html

只要你是

必须先计算B、C的md5,把它们写入到A.js中之后再求A.js的md5,这个时候B、C的内容变更将影响A的最终内容,其更新关系也会级联起来,最终导致index.html引入的A.js是改变了的

@feifeipan

你试着举例说明index.html、A.js、B.js、C.js的原始内容,和构建后的内容

@feifeipan

我先举例一下fis是怎么设计的吧,或许可以作为参考:

1. 源代码:

index.html:

<script src="a.js"></script>

a.js:

var url = __uri('b.js'); // fis的资源定位标识
load(url);

b.js:

alert(123);

2. 构建过程:

  1. 先计算b.js的内容,得到b.js的md5是 0fab3c

  2. 把这个值替换到a.js中,得到:

    var url = '/js/b.0fab3c.js';
    load(url);
  3. 对以上内容(包括了b.js的md5戳的内容)计算md5,得到a.js的指纹是 b33fc9

  4. 把index.html资源定位标识替换成带指纹的:

    <script src="/js/a.b33fc9.js"></script>

3. 构建工具设计

  • 递归计算,这个不用说了
  • 每次构建,都是全量构建,不用“通知”谁变更,因为是递归计算的,所以构建index.html就会去处理a.js,发现还依赖了b.js,就立即处理b.js,得到结果插入到a.js,计算a.js最终内容的md5再插入到index.html。

由于采用了文件内容作为指纹,所以如果所有文件没改动,构建的结果都还一样,如果b.js改了,那么热插入到a.js中的b的指纹会自动变化,a.js最终的内容也发生改变,a的指纹也变更了,最终index.html上引用的a.js的url也就变化了

嗯嗯 谢谢解释这么清楚,这个逻辑我也有看过代码。

不过若是:html/.net页面和js/css静态资源是分开独立发布的呢?

也就是说 不会因为改了a.js把html/.net页面重新build一次。

现在我们新建了一个.net方法,在.net编译时会去取到a.js的md5 name.

但是若a有引用b,b更新之后是无法通知到a的。(a和b是两个独立的component)

因为我电脑现在不在身边,如果我没解释清除,我回头再画一张图来

@feifeipan

现在我们新建了一个.net方法,在.net编译时会去取到a.js的md5 name.

但是若a有引用b,b更新之后是无法通知到a的。(a和b是两个独立的component)

这里就是破坏了递归计算md5啊,你们的这个 新建的.net方法 读取a.js的md5 name实现逻辑 根本就是偷懒的算法好么,没有递归计算啊,亲!这个.net方法 不能只读取的a.js的内容就匆匆计算了md5 还是我上面反复说的问题,但凡读取md5,都是递归计算完这个资源的所有依赖资源之后才能计算自己的md5的,所以几乎没有人会在运行时计算文件指纹,都是线下计算。

@feifeipan

如果你的问题仅仅是 不想重新编译.net的模板文件 fis也另有方案,这个方案正是fis的核心**:基于表的资源管理系统。

fis每次构建,会扫描所有代码,计算标准的md5并且生成一张资源表,它是一份json数据,里面记录了所有资源的id和对应的带md5的url,形如:

{
    "res" : {
        "a.js" : "/js/a.b33fc9.js",
        "b.js" : "/js/b.0fab3c.js"
    }
}

然后,你在.net模板中提供一个查询资源路径的接口,比如:

<script src="<%# getURI("a.js") %>"></script>

这个getURI的后台方法就负责查上面的表来读取url,这样,每次前端构建就负责生成表,发布的时候把表部署到后台服务中,不用重新编译,只要重新读取数据就行了。线下构建的时候记得递归计算md5啊~~~

谢谢,抱歉过了好几天才来。

我把问题重新整理了一下,因为md5引发的会有两个问题。

问题一:
目前的场景是这样,有如下2个模块/库
核心模块:core
基于核心模块开发的UI:calendar

前提:这两个库是两个独立部门研发的。

core的代码如下
源码path: http://resource.com/core/core.js
编译后path: http://resource.com/core/core.t3a8V.js

define("core",[], function(){
     return {
            message:"today is ",
            color:"red"
        }
});

calendar的代码如下
源码path: http://resource.com/ui/calendar.js
编译后path: http://resource.com/ui/calendar.kI3lG.js

requirejs.config({
    paths: {
        "core": __package(“core:core”)
    },
});
define("calendar", ["core"], function(c){
    function init(){
        var box = document.getElementById("box");
        box.style.backgroundColor = c.color;
        box.innerHTML = c.message + (new Date()).toDateString();
    }
    return {
        createDate: init
    }
});

其中__package()方法会在calendar build的时候做替换,类似于fis中的__uri()方法.
返回”http://resource.com/ui/core.t3a8V.js"

那么问题来了,某天更新了core.js源码,执行build core时,会将core的md5替换为core.t3a8V.js ->core.Dc3Al.js

如何通知到calendar去更新呢?

关于.net页面中引用,现在的场景如下

.net源码

<script src="<%# getURI(“a.js") %>"></script>

后端服务器维护一张md5的表

{
    “A" : {
        “a.js" : "/js/a.b33fc9.js"
    },
    “B”: {
         “b.js” : “/js/b.343dvd.js" 
     }
}

其中A和B是两个独立模块

a.js的代码如下:

var script = document.createElement(“script”);
script.type=“text/javascript”;
script.src= __package(“B:b”);
document.getElementsByTagName(“head”)[0].appendChild(script);

遇到的问题和上面的类似,就是b.js更新的时候,如何通知到a。

你的意思是不是在b更新的时候,要把所有模块的md5值都要重新算一遍呢?

或者这样的场景有没有其他的解决方案?

另外,我看了一下FB的源码,发现他们是将页面需要的js的md5配置项都放在页面inline script中,猜测应该是在php运行的时候去做两件事情:

  1. 读取页面中需要的js list
  2. 根据js list实时获取js md5 list。
    因为看不到服务器端代码,只是猜测。

问题有点多,先谢谢了。

nimoc commented

@feifeipan

遇到的问题和上面的类似,就是b.js更新的时候,如何通知到a

无法通知到 a ,必须是当任何代码有修改后重新对所有文件进行 md5 摘取计算。然后更新资源表。


如果硬要做成通知 a 也等于是对所有文件进行 md5 摘取计算,因为你无法直接知道哪些文件依赖了 b。需要对所有文件进行扫描,在扫描的过程中发现 a.js 依赖了 b.js 。

@feifeipan

fis就是这样的设计思路——基于静态资源表的资源管理框架。这件事说起来可能稍微长一些,希望你能耐心看完:

首先,这是一个 跨业务 资源引用问题。前端静态资源定位分为两种情况:

  • 第一种是以字面量形式写在源代码中的,比如css中的背景图地址,js中的文件url字面量,还有html中的各种资源定位标识等。
  • 第二种是可以通过编程接口获取的资源定位,比如当html是模板的时候,前面给出了解决方案,就是在模板中实现一个getURI接口。

第一种资源定位情况,需要经过构建工具处理,因为构建工具需要读取源码计算资源的指纹信息,因此这种方法仅仅适用于 一个业务模块内的资源定位静态替换 ,而第二种资源定位方式才是我们能跨业务模块加载带指纹信息的资源的唯一出路。

我想强调的是:

仅凭以上两条规则,完全可以组合出你所有的资源加载方案

是的,所有的。

大概要做这么几件事:

1. 每个业务模块会生成一张资源表

比如你的例子,假设有团队A维护了一个业务子系统叫 base,里面有有个core.js ,这个模块由A团队开发,他们团队的项目构建之后,会产生一个 base-map.json ,其中内容为:

{
    "core" : {
        "uri": "http://resource.com/base/core.t3a8V.js"
    }
}

然后,你们还有一个B团队,负责维护业务子系统,假设维护的子系统叫 ui,里面有一个 calendar.js ,其代码为(如你上述):

// 注意,这里的依赖关系必须加上一个模块的命名空间,变成了base:core
define("ui:calendar", ["base:core"], function(c){  
    function init(){
        var box = document.getElementById("box");
        box.style.backgroundColor = c.color;
        box.innerHTML = c.message + (new Date()).toDateString();
    }
    return {
        createDate: init
    }
});

好了,B团队的模块构建之后,也得到一张表,名为 ui-map.json ,其内容为:

{
    "calendar" : {
        "uri": "http://resource.com/ui/calendar.kI3lG.js",
        "deps": [ "base:core" ]
    }
}

上线部署的时候,所有业务模块的资源表是部署在一起的,于是就有了:

webserver
    - map
        - base-map.json
        - ui-map.json
        ...

2. 接下来,要实现一个模板中的资源引用接口

假设它的名字叫require,比之前的 getURI 高级一些,这个接口只需支持多表查询,而且能在运行时分析依赖关系,它并不像getURI那样会输出资源的uri,而是仅负责收集,最后我们再多一个接口,比如叫 renderjs 吧,把收集到的资源都输出出来,这样,我们的页面就变成了:

<!doctype html>
<html>
    ...
    <% require('ui:calendar'); %>
    ...
    <%= renderjs() %>
    ...
</html>

模板引擎执行的时候,require函数运行,发现依赖 ui:calendar 知道要读取 ui-map.json 文件,查找其中的 calendar 资源,读表之后发现它还依赖了 base:core ,ok,又去读取 base-map.json 然后找到了资源路径。

全部收集起来之后,执行到 renderjs,检查资源收集情况,发现一共有两个:

[
    "http://resource.com/base/core.t3a8V.js",
    "http://resource.com/ui/calendar.kI3lG.js"
]

这个时候我们就可以渲染真正的资源加载代码了,可以将两个资源直接拼接成script标签输出在renderjs执行的位置:

<!doctype html>
<html>
    ...

    ...
    <script src="http://resource.com/base/core.t3a8V.js"></script>
    <script src="http://resource.com/ui/calendar.kI3lG.js"></script>
    ...
</html>

也可以拼装一段js,把两个资源注册到前端模块化框架中:

<!doctype html>
<html>
    ...

    ...
    <script>
    requirejs.config({
        paths: {
            "base:core": "http://resource.com/base/core.t3a8V.js",
            "ui:calendar": "http://resource.com/ui/calendar.kI3lG.js"
        },
    });
    </script>
    ...
</html>

生成什么结构都是随心所欲的。(这里吐槽一下,我们最终生产生根本不会用requirejs这样框架,因为规范虽好,但很多冗余,如果我们自己定制,其实非常精简,资源表把依赖关系都记录好了,谁还需要前端框架运行时分析)

至此,我们利用后端的模块化框架实现了前端的资源管理,并且简单的多表查询逻辑就能实现跨模块资源引用。此外,我们还可以把css也入表,个别图片也入表,require接口可以查询样式及其依赖,getURI也支持多表查询,这样,一个页面就可以这样写了:

<!doctype html>
<html>
   <head>
        ...
        <% require('base:reset.css'); %>
        <% require('base:grid.css'); %>
        <% require('ui:calendar.css'); %>
        ...
        <% renderCSS(); %>
   </head>
   <body>
        <img src="<%= getURI('foo:logo.png') %>" >
        ...
        <% require('ui:dialog'); %>
        <% require('ui:calendar'); %>
        ...
        <%= renderjs() %>
        ...
    </body>
</html>

可能你会有一个疑问,这里仅解决了模板中的资源定位问题,其他情况怎么办?比如js代码中想跨模块资源定位、css图片中也想。

我想说,规则不需要很多,只要足够原子,能组合出全部应用场景即可。我们现在都有了什么呢:

  • 规则1:模板中的 require 接口,解决跨业务资源引用问题
  • 规则2:js中的模块化框架,解决js模块接口导入导出问题
  • 规则3:业务内构建工具对字面量资源定位的替换(比如fis的__uri标识)
  • 规则4:静态资源表,供前后端资源管理框架使用,可以存js、css以及任何你希望动态查找获取的资源

除了这些,其实我们还有一种隐蔽的资源引用方式:

  • 规则5:css层叠样式

有了以上5中基本资源定位能力,回头看看我们的需求:

场景一:JS中想跨业务引用图片

比如ui中的js想引用base中的图片,我们首先在base中搞一个 icon 的js模块,其内容是:

// base/icon.js
define('base:icon', function(){
    return {
        logo: __uri('logo.png'),
        file: __uri('file.png'),
        folder: __uri('folder.png'),
        foo: __uri('foo.png'),
    }
});

这里使用了工具构建的资源定位替换,发生在base模块内部,构建之后得到:

// base/icon.js
define('base:icon', function(){
    return {
        logo: 'http://resource.com/base/logo.b33fc9.png',
        file: 'http://resource.com/base/file.a5cf24.png',
        folder: 'http://resource.com/base/folder.4234cb.png',
        foo: 'http://resource.com/base/foo.2aabc3.png',
    }
});

然后再在ui业务中的js依赖这个js模块即可,通过模块化接口导入导出来获取资源定位:

// 注意,这里的依赖关系必须加上一个模块的命名空间,变成了base:core
define("ui:calendar", ["base:icon"], function(c){  
    console.log(c.logo);
});

也就是说,我们将 JS中想跨业务引用图片 的需求转换成了:

  1. js跨业务依赖js模块(规则1)
  2. 在跨业务的那个模块中静态编译资源定位标识(规则3)
  3. 通过模块化框架导出一个url列表对象(规则2)

场景二:CSS中想跨业务引用图片

由于css不支持运行时的编程逻辑,所以无法应用规则1-4,但我们有规则5,css可以有层叠啊,也就是说,你在css中使用图片,无非就是要引用为背景图嘛,那为何不考虑在跨业务的那里创建一个css单元,管理各种icon,并提供 class 定义呢?好像font-awsome那样。

所以,这个场景的处理就转换成了:

  1. 跨业务依赖另外一个css单元(规则1)
  2. 被依赖的css单元组织自己的图片,使用业务内资源定位(规则3)
  3. 之间在模板中使用class“接口”,如果需要复写,可以自己定义一个css来层叠样式(规则5)

比如我在base业务中搞了一个 fa 的css单元:

/**
 * base/fa.css
 */
.fa-logo {
    background: url(logo.png) no-repeat 0 0;
}
.fa-file {
    background: url(file.png) no-repeat 0 0;
}
.fa-folder {
    background: url(folder.png) no-repeat 0 0;
}
.fa-foo {
    background: url(foo.png) no-repeat 0 0;
}

那么,我就可以在ui这个业务中直接使用这个样式API:

<!doctype html>
<html>
   <head>
        ...
        <% require('base:fa.css'); %>
        ...
        <% renderCSS(); %>
   </head>
   <body>
        ...
        <div class="fa fa-logo">logo</div>
        ...
    </body>
</html>

fis的设计精髓就在这里了。。。

非常感谢解答,很有收获。仍然有两个小问题

  1. 关于我上面提到的在A:a中原本有这样的代码
var script = document.createElement(“script”);
script.type=“text/javascript”;
script.src= __package(“B:b”);
document.getElementsByTagName(“head”)[0].appendChild(script);

按照场景一的思路是不是要这么调整代码为:这样是否正确?

B:b的代码

define('B:b', function(){
    return {
        script_uri: __uri('b_core.js')
    }
});

A:a的代码

define('A:a', ["B:b"], function(b){
    var script = document.createElement(“script”);
    script.type=“text/javascript”;
    script.src= b.script_uri;
    document.getElementsByTagName(“head”)[0].appendChild(script);
});
  1. 关于资源引用接口

    <!doctype html>

    ... <% require('ui:calendar'); %> ... <%= renderjs() %> ...

是不是每次run页面的时候,都要去读取一下md5的config list?这个会不会影响性能和稳定性?

后端是不是有一个service,比如get/restful_md5list 。这个service应该有一套后端缓存设计吧。另外,这个service必须高可用,如果一旦挂了,是否有备选方案呢?

@feifeipan

问题一:是的。跨业务引用资源的时候,要把依赖关系由静态的转成动态查表的形式。

问题二:是否每次运行页面都要读取map,这个要看你的框架实现。支持持久化的架构,每次都把表载入到内存中,我们可以在框架中判断map文件的修改时间,如map文件修改,就重新读入,否则直接使用内存中的数据,以提高性能。不过如果后端用的是php,就需要每次都读取了,这个时候,也可以做一些小优化,比如我们可以把map.json转成map.php,相比反序列化json文件,直接是php文件的话性能好很多。后端并不需要提供RESTFul的md5查询接口,所有资源md5值都是直接输出在模板中的

好的,谢谢。

问题二:你所指的map是一个维护了所有静态资源文件的一对一MD5表吗?类似于这样:

map.json

{
    base:"base-map.json"
    ui:"ui-map.json",
    ....
    xx:"xx-map.json"
}

index.php页面是

<!doctype html>

...
<% require('ui:calendar'); %>
...
<%= renderjs() %>
...

当用户访问index.php, 我理解下来的步骤是如下,请看下是否正确?

  1. 读取map.json,load到内存中
  2. require只是将"ui:calendar" push到某个数组arr中
  3. renderjs是读取arr,读取map.json取出json文件名list
  4. 通过读取json list获取每个的MD5名和依赖项,生成一份最终的md5 name json
  5. 返回类似如下的代码
<script src="http://resource.com/base/core.t3a8V.js"></script>
<script src="http://resource.com/ui/calendar.kI3lG.js"></script>

补充一下上述的内容
6. 将md5 name json的内容放到页面的一个<script>标签中。

另外,如果用户访问的是静态页面index.html,就不会有类似编译php页面这样的流程。这样的话,读取json、md5这样的信息,是不是只能用ajax请求来玩了?

nimoc commented

@feifeipan
第一次构建生成 map.json 后试试这个?

var map = __inline('map.json')

@feifeipan

基本步骤理解的差不多,有些细节不太合理

1. 读取map.json,load到内存中
2. require只是将"ui:calendar" push到某个数组arr中
3. renderjs是读取arr,读取map.json取出json文件名list
4. 通过读取json list获取每个的MD5名和依赖项,生成一份最终的md5 name json
5. 返回类似如下的代码

步骤2中可以多做一些事情,直接遍历ui:calendar的依赖也push到数组中,而且push的值直接就是uri,这样等到了第3步就是直接输出数组的内容,不需要再多查表了。

此外,纯前端(只有静态页面index.html的时候)实现不了很极致的静态资源管理,只能把表内嵌到页面上(推荐),或者ajax请求map。不过ajax方案会导致多一个请求从而影响性能。

我又来了。

现在我们设计了给.net应用使用的方案,有两套。请大神帮忙看看哪种更合适些

假设页面需要jquery和calendar,以及main样式
方案1
#Base Page#

<html>
<head>
<%=require_css("main")%>
<%=render()%>
</head>
<div>
</div>
<%=require_js("jquery")%>
<%=require_js("calendar")%>
<%=render()%>
</html>

方案2(Razer)
#Base Page#

<html>
<head>
<res type="css" name="head" /> //定义css占位符
</head>
<div>
</div>
<res type="js" name="foot"/>  //定义js占位符
</html>

#CS#

resman.add(['main','jquery','calendar']); //添加资源名
resman.render();   

最终代码是一致的

<html>
<head>
<style link="http://webresource.com/common/css/main.jd84jf.css"></style>
</head>
<div>
</div>

<script>
var ARES_MAP = {
   "ui:calendar:1.0.0": {
       "uri":"http://webresource.com/ui/calendar/index.djf93fdjdhffh.js",
       "deps":["lizard"]
   },
   "core:jquery:1.0.0": {
        "uri": "http://webresource.com/core/jquery/index.81b2b7fbdd1f6326.js"
   }
};
</script>
<script src="http://webresource.com/core/jquery/index.81b2b7fbdd1f6326.js"></script>
<script src="http://webresource.com/ui/calendar/index.djf93fdjdhffh.js"></script>
</html>

不知道是否描述清楚了?谢谢。

@feifeipan

第一种更好一些,运行时收集资源,能做到资源的就近使用与按需。第二种相当于中心化的管理,时间长了资源的引用会出现“泄露”问题

谢谢。不好意思,你说的“泄漏”问题是指?

@feifeipan

就是你集中管理之后,资源的引用与功能分离,将来会不自觉的增大,因为你下线功能的时候总是不敢随意删除集中管理起来的这些资源,导致资源越加越多,性能优化反倒变成了性能恶化。

可以参考这个小故事:https://github.com/fouber/blog/blob/master/201405/01.md#静态资源管理与模块化框架

nimoc commented

最近折腾了一下 gulp 文件 hash 资源定位。 html,css,js,img 的互相关联,思路大概是

  1. less > css , es6 > js
  2. 利用 gulp-rev 生成 map.json
  3. 扫描 html js css 文件,根据 map.json 的hash信息 替换引用路径

虽然实现了资源定位,但是属于 非 watch 模式。watch 模式中第三步的替换引用路径如果扫描所有文件会很慢。

现在要想办法加快watch时编译速度,参考本贴的讨论。
需要在第一次替换引用路径 时记录依赖关系。希望有做过 gulp 资源定位的分享一下 gulp watch 模式中如何高效的记录文件依赖。

我接着踩坑,有新思路了本帖继续回复。

nimoc commented

接着上面的 gulp hash 资源定位问题,多次尝试后发现只能跳过解决。

watch 模式放弃文件 hash 和资源定位,人工手段强制源码和编译目录一致

发布构建时做2次编译,1. 编译源码生成静态资源 hash表 2. 根据 hash表 替换所有路径引用。慢速操作都在发布时才操作


非要折腾 gulp 的原因是 gulp + webpack-stream 的编译速度非常快
fis + webpack 的速度挺慢的,不支持webpack异步记载js(但花时间整合一下也能解决,但是需要投入的工作量太大了)

webpack-stream 有一个 watch 模式会让 webpack 编译速度飞快。


2016年12月01日0:40:27 踩坑更新
gulp + webpack 也有不少坑。主要是要无法像 fis 一样完美解决文件hash的问题。最终方案换成了 fis + webpack ,但是 webpack 是独立运行的

主要配置如下

fis.match('*.js', {
	release: false
})

var src_To_SrcAndUndo = {
	preprocessor: function (content) {
		return content.replace(/src=(['"].*?.js['"])/g, '_src=$1')
	},
	postprocessor: function (content, file) {
		return content.replace(/_src=(['"].*?.js['"])/g, 'src=$1')
	}
}
fis.media('dev').match('{*.html,*.md}', src_To_SrcAndUndo)
fis.media('online1').match('{*.html,*.md}', src_To_SrcAndUndo)

// static domain
fis.media('online2').match('**', {
	domain: staticDomain
})


// 最终发布阶段需要编译 js,因为此js是 webpack 生成的
fis.media('online2').match('*.js', {
	release: true
})
fis.media('online2').match('**', {
	useHash: true
})
fis.media('online2').match('*.html', {
	useHash: false
})

package.json scripts

"dev": "fis3 release -w -d ./output",
"webpack": "webpack -w --progress --colors",
"online": "fis3 release online1 -d ./output && NODE_ENV=online webpack --progress --colors && fis3 release online2 -r ./output -d ./output",

原来是这样 ,怪不得我文件内容不变的时候 ,打包加的md5戳不会变化。
这些明白了,原来是这样层层计算得来的。

md5戳只有8个字符是不是会产生重复?

个人认为,这种根据状态改变触发的方式很适用于订阅发布机制来处理,要比文件内容一层层的递归查找要好得多,避免无效的递归操作。