-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
资源定位中md5戳的计算过程 #5
Comments
md5 的值主要依赖于文件的内容,而且当文件变化 md5 值也需要变化(包括依赖)。但是不一定需要替换后才能去 md5,首要关注的是文件的变化,所以我觉得只要将依赖文件计算出来,将他们的内容进行 md5 计算就可以了。 |
这样做不够严谨,以js、css为例,内容变化还可能是注释修改,并不会影响最终内容的改变 |
@fouber 但是文件确实变化了,压缩也不一定 100% 正确的,压缩工具修改也会造成输出变化。 |
而且必须是先替换引用资源的md5,才能再计算当前内容的md5,否则某次修改,我们只改了图片,其他js、css没有改动,只关注文件本身内容的md5算法就会认为资源没有修改,最终导致上线后没有更新这些文件,而最终修改的图片没有生效 |
同一份文件内容,压缩工具处理后的结果不会有变化的,这个已经证实过了 |
是不是md5其实无所谓,关键是计算出上一次部署文件和本次部署文件是否有差异。大概7、8年前就做过这样的方案——拿本次部署对应的资源文件(未加版本号的)比对上次部署对应的资源文件(未加版本号的),计算出差异,然后计算依赖,得到最终所有要变的资源文件集,所有变更的文件自增版本号,不变的用上次的版本号,更新所有依赖链接为最终的path。 |
压缩导致结果变化的情况没遇到过。不过某些大厂有用差异更新的,小差异导致压缩的短变量名大量变化从而增加了diff大小的情况,倒是会有的。 |
@fouber 压缩工具变更肯定会应该输出的,比如自己增加一些元信息,这个不影响压缩效果,也是可能的。 额,你们没有仔细看么,我没有说只是修改文件本身,是所有依赖文件的内容,比如图片改动,对应的 js 文件肯定会发生变化。我的分歧点是不需要坐资源定位标记的替换,其他我也是很认同,我也是这么做的。 md5 主要的作用是避免文件的覆盖,当文件变化所生成的文件变化必须不同,所有生成的 md5 只要考虑是否已经考虑到文件变化就可以了,至于是否必须为处理后的文件我就不做评价了。 |
用md5处理只是一种便捷方式而已,确实并不重要。md5无需关系版本diff,这是它的一个小优势,最终面向的原理是完全一致的。 |
替换资源定位标记之后再对文件本身求md5,这样可以自然引起当前资源的内容变更,便于形成递归处理逻辑,在工具设计上比较容易实现而已。 如果用别的方式先确认了内容变更的依据,最终再去替换定位标记也是一样的 |
注意,源码中,coffee里写的是 |
恩,如果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); |
不好意思,之前的回复写的着急了一些,详细的是这样的: 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 |
应该没有安装成功吧,或者安装的时候没有加 |
安装的时候提示了一个这个npm WARN optional dep failed, continuing [email protected] |
@shunyitian 这个可以忽略,你机器编译 fsevents 失败了 |
我在重装一次试试 |
不好意思,确实是一个bug,刚刚更新了,再安装一次就好了 |
@fouber 就当我帮忙了,哈哈 |
非常感谢 |
非常感谢你提供的工具,我用过之后非常的好用,非常适合小公司,小项目,之前在使用grunt时就遇到静态资源经过grunt处理过后还要去手动去改成处理后文件的路径实在是麻烦,但不知道grunt里有没有此类的解决,不过此工具已经解决,还有一个就是md5摘要形式发布了文件不会有缓存的问题了,以前我们修改后图片,在客户那儿没有反应,最后发现是文件缓存,特别是在手机上; 在使用的过程中我遇到了以下两个问题: |
|
@fouber 非常感谢,2.是文件路径的问题,我是以php中引入view路径为准的,此工具是以文件位置为依据: |
本地开发不用加 |
css 里面的资源定位还算容易。但 js 中的资源链接是通过字符串拼接生成的,那就无解了吧? |
@maplejan 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]; |
正如本blog描述的,md5是一个递归依赖的过程,如果你要解决这个问题,必须这样做:
也就是说A的md5是依赖B、C内容才能计算得到的,而不是分别计算ABC各自的内容在插入到引用的文件中。 MD5内容指纹的计算是要递归进行的! |
可能是我的问题没有描述清楚 我其实是另一个问题,如上述所属,A中require了B和C。已经递归算出A的md5,同时某页面index.html已经引用了A(md5 path).这时B更新了一个版本,(A和B是两个不同component),我如何通知到A修改呢,如何通知到index.html的A引用修改呢? 希望描述清楚了 |
如果实现了递归计算,这个问题不是自然而然的就解决了么。。。你确定理解我说的“递归计算”的真正过程么? 所谓的递归计算,不是说我们分别计算A.js,B.js,C.js的md5,然后插入到对应的引用的位置,这根本不是递归。我说的递归是——比如你这个例子,A依赖了B和C,你要:
只要你是
|
你试着举例说明index.html、A.js、B.js、C.js的原始内容,和构建后的内容 |
我先举例一下fis是怎么设计的吧,或许可以作为参考: 1. 源代码:index.html: <script src="a.js"></script> a.js: var url = __uri('b.js'); // fis的资源定位标识
load(url); b.js: alert(123); 2. 构建过程:
3. 构建工具设计
由于采用了文件内容作为指纹,所以如果所有文件没改动,构建的结果都还一样,如果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) 因为我电脑现在不在身边,如果我没解释清除,我回头再画一张图来 |
这里就是破坏了递归计算md5啊,你们的这个 |
如果你的问题仅仅是 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引发的会有两个问题。 问题一: 前提:这两个库是两个独立部门研发的。 core的代码如下
calendar的代码如下
其中__package()方法会在calendar build的时候做替换,类似于fis中的__uri()方法. 那么问题来了,某天更新了core.js源码,执行build core时,会将core的md5替换为core.t3a8V.js ->core.Dc3Al.js 如何通知到calendar去更新呢? |
关于.net页面中引用,现在的场景如下 .net源码
后端服务器维护一张md5的表
其中A和B是两个独立模块 a.js的代码如下:
遇到的问题和上面的类似,就是b.js更新的时候,如何通知到a。 你的意思是不是在b更新的时候,要把所有模块的md5值都要重新算一遍呢? 或者这样的场景有没有其他的解决方案? 另外,我看了一下FB的源码,发现他们是将页面需要的js的md5配置项都放在页面inline script中,猜测应该是在php运行的时候去做两件事情:
问题有点多,先谢谢了。 |
无法通知到 a ,必须是当任何代码有修改后重新对所有文件进行 md5 摘取计算。然后更新资源表。 如果硬要做成通知 a 也等于是对所有文件进行 md5 摘取计算,因为你无法直接知道哪些文件依赖了 b。需要对所有文件进行扫描,在扫描的过程中发现 a.js 依赖了 b.js 。 |
fis就是这样的设计思路——基于静态资源表的资源管理框架。这件事说起来可能稍微长一些,希望你能耐心看完: 首先,这是一个
第一种资源定位情况,需要经过构建工具处理,因为构建工具需要读取源码计算资源的指纹信息,因此这种方法仅仅适用于 我想强调的是: 仅凭以上两条规则,完全可以组合出你所有的资源加载方案是的,所有的。 大概要做这么几件事: 1. 每个业务模块会生成一张资源表比如你的例子,假设有团队A维护了一个业务子系统叫 {
"core" : {
"uri": "http://resource.com/base/core.t3a8V.js"
}
} 然后,你们还有一个B团队,负责维护业务子系统,假设维护的子系统叫 // 注意,这里的依赖关系必须加上一个模块的命名空间,变成了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团队的模块构建之后,也得到一张表,名为 {
"calendar" : {
"uri": "http://resource.com/ui/calendar.kI3lG.js",
"deps": [ "base:core" ]
}
} 上线部署的时候,所有业务模块的资源表是部署在一起的,于是就有了:
2. 接下来,要实现一个模板中的资源引用接口假设它的名字叫require,比之前的 <!doctype html>
<html>
...
<% require('ui:calendar'); %>
...
<%= renderjs() %>
...
</html> 模板引擎执行的时候,require函数运行,发现依赖 全部收集起来之后,执行到 [
"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图片中也想。 我想说,规则不需要很多,只要足够原子,能组合出全部应用场景即可。我们现在都有了什么呢:
除了这些,其实我们还有一种隐蔽的资源引用方式:
有了以上5中基本资源定位能力,回头看看我们的需求: 场景一:JS中想跨业务引用图片比如ui中的js想引用base中的图片,我们首先在base中搞一个 // 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);
}); 也就是说,我们将
场景二:CSS中想跨业务引用图片由于css不支持运行时的编程逻辑,所以无法应用规则1-4,但我们有规则5,css可以有层叠啊,也就是说,你在css中使用图片,无非就是要引用为背景图嘛,那为何不考虑在跨业务的那里创建一个css单元,管理各种icon,并提供 class 定义呢?好像font-awsome那样。 所以,这个场景的处理就转换成了:
比如我在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的设计精髓就在这里了。。。 |
非常感谢解答,很有收获。仍然有两个小问题
按照场景一的思路是不是要这么调整代码为:这样是否正确? B:b的代码
A:a的代码
是不是每次run页面的时候,都要去读取一下md5的config list?这个会不会影响性能和稳定性? 后端是不是有一个service,比如get/restful_md5list 。这个service应该有一套后端缓存设计吧。另外,这个service必须高可用,如果一旦挂了,是否有备选方案呢? |
问题一:是的。跨业务引用资源的时候,要把依赖关系由静态的转成动态查表的形式。 问题二:是否每次运行页面都要读取map,这个要看你的框架实现。支持持久化的架构,每次都把表载入到内存中,我们可以在框架中判断map文件的修改时间,如map文件修改,就重新读入,否则直接使用内存中的数据,以提高性能。不过如果后端用的是php,就需要每次都读取了,这个时候,也可以做一些小优化,比如我们可以把map.json转成map.php,相比反序列化json文件,直接是php文件的话性能好很多。后端并不需要提供RESTFul的md5查询接口,所有资源md5值都是直接输出在模板中的 |
好的,谢谢。 问题二:你所指的map是一个维护了所有静态资源文件的一对一MD5表吗?类似于这样: map.json
index.php页面是
当用户访问index.php, 我理解下来的步骤是如下,请看下是否正确?
|
补充一下上述的内容 |
另外,如果用户访问的是静态页面index.html,就不会有类似编译php页面这样的流程。这样的话,读取json、md5这样的信息,是不是只能用ajax请求来玩了? |
@feifeipan var map = __inline('map.json') |
基本步骤理解的差不多,有些细节不太合理
步骤2中可以多做一些事情,直接遍历ui:calendar的依赖也push到数组中,而且push的值直接就是uri,这样等到了第3步就是直接输出数组的内容,不需要再多查表了。 此外,纯前端(只有静态页面index.html的时候)实现不了很极致的静态资源管理,只能把表内嵌到页面上(推荐),或者ajax请求map。不过ajax方案会导致多一个请求从而影响性能。 |
我又来了。 现在我们设计了给.net应用使用的方案,有两套。请大神帮忙看看哪种更合适些 假设页面需要jquery和calendar,以及main样式
方案2(Razer)
#CS#
最终代码是一致的
不知道是否描述清楚了?谢谢。 |
第一种更好一些,运行时收集资源,能做到资源的就近使用与按需。第二种相当于中心化的管理,时间长了资源的引用会出现“泄露”问题 |
谢谢。不好意思,你说的“泄漏”问题是指? |
就是你集中管理之后,资源的引用与功能分离,将来会不自觉的增大,因为你下线功能的时候总是不敢随意删除集中管理起来的这些资源,导致资源越加越多,性能优化反倒变成了性能恶化。 可以参考这个小故事:https://github.com/fouber/blog/blob/master/201405/01.md#静态资源管理与模块化框架 |
最近折腾了一下 gulp 文件 hash 资源定位。 html,css,js,img 的互相关联,思路大概是
虽然实现了资源定位,但是属于 非 现在要想办法加快watch时编译速度,参考本贴的讨论。 我接着踩坑,有新思路了本帖继续回复。 |
接着上面的 gulp hash 资源定位问题,多次尝试后发现只能跳过解决。
发布构建时做2次编译,1. 编译源码生成静态资源 hash表 2. 根据 hash表 替换所有路径引用。慢速操作都在发布时才操作 非要折腾 gulp 的原因是 gulp + webpack-stream 的编译速度非常快
2016年12月01日0:40:27 踩坑更新 主要配置如下 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个字符是不是会产生重复? |
个人认为,这种根据状态改变触发的方式很适用于订阅发布机制来处理,要比文件内容一层层的递归查找要好得多,避免无效的递归操作。 |
要实现完整的md5计算,最终必须将task-based的流程转变成one-task形式。此处给出相关说明:
假设我们有三个文件,比如
foo.coffee
,foo.scss
和foo.png
,文本文件的内容为:foo.coffee
foo.scss
最终形成这样一种资源引用关系:
当我们要计算foo.coffee的md5戳的时候,其实是一个这样的过程:
整个计算过程是一个递归编译的过程,计算文件的摘要信息应该根据文件的
最终内容计算
,所以这个过程中要加入对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化处理。
The text was updated successfully, but these errors were encountered: