fex-team / fis

Front-end Integrated Solution - 前端集成解决方案, 最新版请进入 FIS3 https://github.com/fex-team/fis3
http://fis.baidu.com
MIT License
2.96k stars 655 forks source link

fis插件开发的一些槽点(后续会增加,慢慢来) #540

Open longyiyiyu opened 9 years ago

longyiyiyu commented 9 years ago

最近在开发fis插件,遇到一些问题,抛一下:

  1. fis模块内部有许多开关,但是并没有提供出来,譬如cache开关,譬如log,至少在文档上面没找到参数开关。cache开关真的很必要,因为在开发fis插件的时候,调试就得跑一个项目,如果用了cache,每次重跑都要修改一下源代码,蛋疼。
  2. 对fis.compile的一点吐槽,有点复杂,慢慢道来,言辞或许较激烈,先请原谅 一句话:插件开发API里面有提到fis.compile接口可用,但是实际上一点用处都没有

造成这个问题的原因有很点复杂,我理一下,先大致说我的需求: 在处理文件A时,想动态透明地对文件B,文件C的内容进行增删后处理。

首先,compile过程是针对单文件的,它完全独立,这个思想也不能说它有问题,所以不抱怨,这里需要关联其他文件,所以插件选用prepackage插入点(我能抱怨我一开始选用preprocessor时发现想关联其他文件基本不可能么?)。

选用prepackage有一个问题,就是这里的资源都是已经compile过的了,修改内容就很难了。这里很自然地就想到修改源代码,然后再手动compile一下,最后替换掉resourceMap就好了,多美好~ 文档里面也明文写好可以使用fis.compile,太简单了。

说到这里,需要深入一下fis-kernel的代码,看了release的代码,发现fis.compile需要一个file对象,于是,代码处理如下:

var path = 'xxx';
var file = fis.file(path);
file.setContent(newContent);
fis.compile(file);

然后发现没有效果,最后出来还是原来的内容,找了一晚上,以为是resourceMap关联问题,以为是cache问题,以为是newContent的问题,以为是插件其他问题,就是没想过fis.compile不能用!这里我必须狠狠地吐槽一下!

直接看fis.compile的源码,useCache的那一段忽略,缓存没什么好看的,只要知道想不用缓存的时候,只要设置一下file.useCache=false即可。关键在于下面几句(可查源码):

/*14*/  file = fis.file.wrap(file);
/*35*/  file.setContent(fis.util.read(file.realpath));
/*36*/  process(file);   //还有几个地方有process,但同样前面都会有类似上面那句

为什么不能用?why? file作为文件的抽象,这能力也太弱了吧。去看file的代码和处理,会发现它主要只有两个作用:

  1. 可以用cache文件内容
  2. 在compile流中充当pipe(类似gulp) 实际上,两个都是cache,因此,这里的file根本就不是文件的抽象,仅仅是文件的cache。 对于这个设计理念,我只能呵呵

好,我们不讨论设计理念问题,我再说一下这样带来的问题,现在我想要compile一个文件,我必须修改src/下面的源代码,基于src/下面应该全是源码的理念,工具的修改是不应该改动src的,但是为了解决问题,我打算先修改src,然后compile,最后再还原,这总可以了吧。

好,去构建,然后就悲剧地死循环,因为watch,后面不解释,点点点

一条路死了,我们换一条,既然compile必须基于文件系统的文件内容,那我们可以用缓存文件,复制一份src的代码到tmp/下面,然后试试,你会发现,因为file的id是基于路径的,然后各种路径关联问题,最后满头黑线,再也不想用缓存文件啦!

综上所述,fis.compile的使用路径死循环了:

整了一晚上,掉进一个设计理念的坑里,我不服! grunt系没有file抽象,都是直接读取文件系统的内容,路径关联的自由度极高,所以基本不会有这些蛋疼的问题。 但是,既然fis整了一个file抽象,路径关联也是基于file的基础上,那为什么不是所有操作都是基于file?为什么compile是必须基于文件系统的文件内容? 我以不稳定的情绪上来吐槽,不是针对fis和fis团队,只为发泄,我想吼一句,这设计真是渣。

如果compile可以基于file._content,比如以下写法:

if (!file.getContent()) readContentFromFileSystem(file)
process(file)

仅仅只是一个小小地判断,但却是完全不同的两个设计理念,怎么就能差那么远呢? ps:如果想说md5是基于文件内容的话(实际上现在的实现是这样),这问题也不难解决,不赘述

当任何操作都是基于file抽象的时候,我们就可以手动创建虚拟文件file加入到构建过程中,然后做更多的事情,只要你有足够的想象力。

最后,给出我的最终解决方案,在不修改fis-kernel的前提下,在源头做了hack:

//hack fis.util.read
var readCache = {};
function hack(origin) {
    return function(path, convert) {
        if (path in readCache) return readCache[path];
        return origin.apply(this, arguments);
    }
}
fis.util.read = hack(fis.util.read);

直接hack fis的读取文件系统内容的api,自己做了一层文件内容虚拟层 虽然看起来解决方案很简单,但是不到万不得已,谁想hack!

oxUnd commented 9 years ago

Q1

fis模块内部有许多开关,但是并没有提供出来,譬如cache开关,譬如log,至少在文档上面没找到参数开关。cache开关真的很必要,因为在开发fis插件的时候,调试就得跑一个项目,如果用了cache,每次重跑都要修改一下源代码,蛋疼。

$ fis release -h
  Usage: release [options]

  Options:

    -h, --help             output usage information
    -d, --dest <names>     release output destination
    -m, --md5 [level]      md5 release option
    -D, --domains          add domain name
    -l, --lint             with lint
    -t, --test             with unit testing
    -o, --optimize         with optimizing
    -p, --pack             with package
    -w, --watch            monitor the changes of project
    -L, --live             automatically reload your browser
    -c, --clean            clean compile cache
    -r, --root <path>      set project root
    -f, --file <filename>  set fis-conf file
    -u, --unique           use unique compile caching
    --verbose              enable verbose output

其中明确有

fis release --clean

来清理缓存的命令。

Q2

对fis.compile的一点吐槽,有点复杂 ...

来个例子

https://github.com/fex-team/fis-spriter-csssprites/blob/master/libs/image.js#L125-L127

END

fouber commented 9 years ago

fis采用了与grunt完全不同的构建流程,所以:

请先不要先入为主的判断fis应该怎样(以符合grunt的思路来)设计。

  1. fis的构建分为两个阶段:一个是单文件编译阶段,一个是打包(多文件处理阶段)。单文件编译的过程控制非常严格,只能关注源文件本身的内容,因为要基于这些内容建立缓存,无缓存的时候必须直接读取源码内容,否则可能会产生缓存错误。此外,单文件编译过程不允许与其他文件产生“弱关联”,也不允许新建文件,这些限制都是为了保证缓存的有效性,严格要求单文件编译只关注文件本身的内容,这是fis保证系统构建准确性的一种设计。 如果非要类比,你可以将多文件处理阶段与grunt的task-based类比起来,把重头戏放到这个阶段,而单文件编译只是构建前的“开胃小菜”。
  2. 过了单文件之后,所有文件都将进入打包流程,在这些流程中可以建立文件与文件之间的关系,比如添加弱依赖关系(同名依赖、存在判断)、创建新文件、合并文件、sourcemap等。因为这些新创建的文件不能走任何缓存,它们是受某些条件影响才创建的,都是即时的编译,这些事情都可以在prepackager阶段完成
  3. 在打包阶段你可能还需要对一些新创建的文件进行一定的内容处理,那就调用fis.compile就可以了,调用的方式是:

    var file = fis.file(path);
    fis.setContent(xxx);
    fis.compile(file);

    注意,由于新创建的文件并不是一个真实存在的文件,所以以上的代码最终走到的逻辑分支是 这里

  4. 你上面吐槽的问题确实存在一种情况:在打包阶段尝试修改一个真实存在的文件的内容,修改后并再次对这个文件进行compile编译,这会导致进入你所说的分支重读源码或者读取了缓存,但这种使用场景我始终想不到为了什么,既然前面的单文件编译已经帮你处理过源文件内容了,为什么在打包阶段修改内容之后还需要重新编译?

另外,我也想吐槽:

你这只见吐槽没见原始需求,让人怎么判断对错啊。当你觉得一个系统难用的时候,有没有考虑过另外一种情况,就是你没有按照它的设计初衷去实现你的需求,而是先入为主的判断它的逻辑,然后发现逻辑与预期不符就硬着头皮hack下去,这是不是也不合理啊。。。

所以,希望你贴出你的原始需求,我来说一下fis的设计下改怎么实现,然后顺便解释一下设计思想,你看怎么样

longyiyiyu commented 9 years ago

我想的是不通过fis release命令参数去设置,而是可以在类似配置之类的,譬如fis.config.set('project.clear', false);这样。而且即使是fis release也没有完全开放所有的配置吧

oxUnd commented 9 years ago

@longyiyu roadmap.path 这块是否有看到过。确实我们的文档未偏向插件开发者弄,后续这块会有所增强;

longyiyiyu commented 9 years ago

第二点和需求是一点关系都没有的,提到只是为了解释方便而已,再者需求各种各样,情况万千变化,API有着时刻完善的使命但也不能肩负面面俱到的责任,既然如此,那就需要很好的文档加以说明!

如题只是吐槽,而且不是针对fis团队的,所以你们是可以忽略的

不过有一点,请在插件开发API那里,给fis.compile添加一些解释,注明使用方法,并且只能对新创建文件进行compile,不能对已存在的文件进行compile!

文档一点说明没有,直接误导开发者,所以开发者情绪不满上来吐槽一下,请别见怪~ 也算一点建议哈

PS:如果不是喜欢fis,我都懒得来吐槽的,所以我还是很支持你们的,加油!

longyiyiyu commented 9 years ago

roadmap.path不是针对文件的么,一些全局性的开关是应该放到project的吧?

oxUnd commented 9 years ago

@longyiyiyu 好,大家都加油;

chyingp commented 9 years ago

@xiangshouding

shouding君赶紧把文档完善下,功德无量 =。= http://fis.baidu.com/docs/api/dev.html

oxUnd commented 9 years ago

@chyingp OK,卡卡,一起啊

https://github.com/fex-team/fis-site/blob/master/docs/api/dev.md