Skip to content
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

Open
fouber opened this issue Oct 30, 2014 · 89 comments
Open

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

fouber opened this issue Oct 30, 2014 · 89 comments
Labels

Comments

@fouber
Copy link
Owner

fouber commented Oct 30, 2014

要实现完整的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 fouber added the 杂谈 label Oct 30, 2014
@fouber fouber changed the title 关于task-based机制的构建工具实现md5戳问题 资源定位中md5戳的计算过程 Oct 30, 2014
@popomore
Copy link

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

@fouber
Copy link
Owner Author

fouber commented Oct 30, 2014

@popomore

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

@popomore
Copy link

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

@fouber
Copy link
Owner Author

fouber commented Oct 30, 2014

@popomore

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

@fouber
Copy link
Owner Author

fouber commented Oct 30, 2014

@popomore

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

@hax
Copy link

hax commented Oct 30, 2014

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

@hax
Copy link

hax commented Oct 30, 2014

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

@popomore
Copy link

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

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

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

@fouber
Copy link
Owner Author

fouber commented Oct 30, 2014

@hax

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

@fouber
Copy link
Owner Author

fouber commented Oct 30, 2014

@popomore

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

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

@fouber
Copy link
Owner Author

fouber commented Oct 30, 2014

@chuyik

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

@fouber
Copy link
Owner Author

fouber commented Oct 30, 2014

@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
Copy link
Owner Author

fouber commented Oct 30, 2014

@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
Copy link
Owner Author

fouber commented Oct 30, 2014

@shunyitian
Copy link

/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
Copy link
Owner Author

fouber commented Oct 31, 2014

@shunyitian

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

@shunyitian
Copy link

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

@popomore
Copy link

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

@shunyitian
Copy link

我在重装一次试试

@fouber
Copy link
Owner Author

fouber commented Oct 31, 2014

@shunyitian

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

@shunyitian
Copy link

@fouber 就当我帮忙了,哈哈

@fouber
Copy link
Owner Author

fouber commented Oct 31, 2014

@shunyitian

非常感谢

@FEsy
Copy link

FEsy commented Oct 31, 2014

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

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

@fouber
Copy link
Owner Author

fouber commented Oct 31, 2014

@FEsy

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

@FEsy
Copy link

FEsy commented Oct 31, 2014

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

@fouber
Copy link
Owner Author

fouber commented Oct 31, 2014

@FEsy

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

@maplejan
Copy link

maplejan commented Nov 1, 2014

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

@fouber
Copy link
Owner Author

fouber commented Nov 1, 2014

@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
Copy link
Owner Author

fouber commented Jun 19, 2015

@feifeipan

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

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

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

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

@feifeipan
Copy link

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

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

希望描述清楚了

@fouber
Copy link
Owner Author

fouber commented Jun 19, 2015

@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是改变了的

@fouber
Copy link
Owner Author

fouber commented Jun 19, 2015

@feifeipan

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

@fouber
Copy link
Owner Author

fouber commented Jun 19, 2015

@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也就变化了

@feifeipan
Copy link

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

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

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

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

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

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

@fouber
Copy link
Owner Author

fouber commented Jun 19, 2015

@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的,所以几乎没有人会在运行时计算文件指纹,都是线下计算。

@fouber
Copy link
Owner Author

fouber commented Jun 19, 2015

@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啊~~~

@feifeipan
Copy link

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

我把问题重新整理了一下,因为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去更新呢?

@feifeipan
Copy link

关于.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
Copy link

nimoc commented Jul 1, 2015

@feifeipan

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

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


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

@fouber
Copy link
Owner Author

fouber commented Jul 1, 2015

@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的设计精髓就在这里了。。。

@feifeipan
Copy link

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

  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必须高可用,如果一旦挂了,是否有备选方案呢?

@fouber
Copy link
Owner Author

fouber commented Jul 2, 2015

@feifeipan

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

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

@feifeipan
Copy link

好的,谢谢。

问题二:你所指的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>

@feifeipan
Copy link

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

@feifeipan
Copy link

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

@nimoc
Copy link

nimoc commented Jul 3, 2015

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

var map = __inline('map.json')

@fouber
Copy link
Owner Author

fouber commented Jul 3, 2015

@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方案会导致多一个请求从而影响性能。

@feifeipan
Copy link

我又来了。

现在我们设计了给.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>

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

@fouber
Copy link
Owner Author

fouber commented Jul 23, 2015

@feifeipan

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

@feifeipan
Copy link

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

@fouber
Copy link
Owner Author

fouber commented Jul 23, 2015

@feifeipan

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

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

@nimoc
Copy link

nimoc commented Oct 6, 2016

最近折腾了一下 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
Copy link

nimoc commented Oct 9, 2016

接着上面的 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",

@zhaoqize
Copy link

zhaoqize commented Nov 4, 2016

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

@mini188
Copy link

mini188 commented May 23, 2017

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

@smalike
Copy link

smalike commented May 27, 2017

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

15 participants