zl程序教程

您现在的位置是:首页 >  工具

当前栏目

FIS源码-fis release增量编译与依赖扫描细节

源码依赖 编译 扫描 细节 增量 Release
2023-09-14 09:00:24 时间

前面已经提到了fis release命令大致的运行流程。本文会进一步讲解增量编译以及依赖扫描的一些细节。

首先,在fis release后加上--watch参数,看下会有什么样的变化。打开命令行

fis release --watch

不难猜想,内部同样是调用release()方法把源文件编译一遍。区别在于,进程会监听项目路径下源文件的变化,一旦出现文件(夹)的增、删、改,则重新调用release()进行增量编译。

并且,如果资源之间存在依赖关系(比如资源内嵌),那么一些情况下,被依赖资源的变化,会反过来导致资源引用方的重新编译。

// 是否自动重新编译

if(options.watch){

 watch(options); // 对!就是这里

} else {

 release(options);

}

下面扒扒源码来验证下我们的猜想。

watch(opt)细节

源码不算长,逻辑也比较清晰,这里就不上伪代码了,直接贴源码出来,附上一些注释,应该不难理解,无非就是重复文件变化-- release(opt)这个过程。

在下一小结稍稍展开下增量编译的细节。

function watch(opt){

 var root = fis.project.getProjectPath();

 var timer = -1;

 var safePathReg = /[\\\/][_\-.\s\w]+$/i; // 是否安全路径(参考)

 var ignoredReg = /[\/\\](?:output\b[^\/\\]*([\/\\]|$)|\.|fis-conf\.js$)/i; // ouput路径下的,或者 fis-conf.js 排除,不参与监听

 opt.srcCache = fis.project.getSource(); // 缓存映射表,代表参与编译的源文件;格式为 源文件路径= 源文件对应的File实例。比较奇怪的是,opt.srcCache 没见到有地方用到,在 fis.release 里,fis.project.getSource() 会重新调用,这里感觉有点多余

 // 根据传入的事件类型(type),返回对应的回调方法

 // type 的取值有add、change、unlink、unlinkDir

 function listener(type){

 return function (path) {

 if(safePathReg.test(path)){

 var file = fis.file.wrap(path);

 if (type == add || type == change) { // 新增 或 修改文件

 if (!opt.srcCache[file.subpath]) { // 新增的文件,还不在 opt.srcCache 里

 var file = fis.file(path);

 opt.srcCache[file.subpath] = file; // 从这里可以知道 opt.srcCache 的数据结构了,不展开

 } else if (type == unlink) { // 删除文件

 if (opt.srcCache[file.subpath]) {

 delete opt.srcCache[file.subpath]; // 

 } else if (type == unlinkDir) { // 删除目录

 fis.util.map(opt.srcCache, function (subpath, file) {

 if (file.realpath.indexOf(path) !== -1) {

 delete opt.srcCache[subpath];

 }); 

 clearTimeout(timer);

 timer = setTimeout(function(){

 release(opt); // 编译,增量编译的细节在内部实现了

 }, 500);

 //添加usePolling配置

 // 这个配置项可以先忽略

 var usePolling = null;

 if (typeof fis.config.get(project.watch.usePolling) !== undefined){

 usePolling = fis.config.get(project.watch.usePolling);

 // chokidar模块,主要负责文件变化的监听

 // 除了error之外的所有事件,包括add、change、unlink、unlinkDir,都调用 listenter(eventType) 来处理

 require(chokidar)

 .watch(root, {

 // 当文件发生变化时候,会调用这个方法(参数是变化文件的路径)

 // 如果返回true,则不触发文件变化相关的事件

 ignored : function(path){

 var ignored = ignoredReg.test(path); // 如果满足,则忽略

 // 从编译队列中排除

 if (fis.config.get(project.exclude)){

 ignored = ignored ||

 fis.util.filter(path, fis.config.get(project.exclude)); // 此时 ignoredReg.test(path) 为false,如果在exclude里,ignored也为true

 // 从watch中排除

 if (fis.config.get(project.watch.exclude)){

 ignored = ignored ||

 fis.util.filter(path, fis.config.get(project.watch.exclude)); // 跟上面类似

 return ignored;

 usePolling: usePolling,

 persistent: true

 .on(add, listener(add))

 .on(change, listener(change))

 .on(unlink, listener(unlink))

 .on(unlinkDir, listener(unlinkDir))

 .on(error, function(err){

 //fis.log.error(err);

}
增量编译细节

增量编译的要点很简单,就是只发生变化的文件进行编译部署。在fis.release(opt, callback)里,有这段代码:

// ret.src 为项目下的源文件

fis.util.map(ret.src, function(subpath, file){

 if(opt.beforeEach) {

 opt.beforeEach(file, ret);

 file = fis.compile(file);

 if(opt.afterEach) {

 opt.afterEach(file, ret); // 这里这里!

 }

opt.afterEach(file, ret)这个回调方法可以在 fis-command-release/release.js 中找到。归纳下:

对比了下当前文件的最近修改时间,看下跟上次缓存的修改时间是否一致。如果不一致,重新编译,并将编译后的实例添加到collection中去。 执行deploy进行增量部署。(带着collection参数)
opt.afterEach = function(file){

 //cal compile time

 // 略过无关代码

 var mtime = file.getMtime().getTime(); // 源文件的最近修改时间

 //collect file to deploy

 // 如果符合这几个条件:1、文件需要部署 2、最近修改时间 不等于 上一次缓存的修改时间

 // 那么重新编译部署

 if(file.release lastModified[file.subpath] !== mtime){

 // 略过无关代码

 lastModified[file.subpath] = mtime;

 collection[file.subpath] = file; // 这里这里!!在 deploy 方法里会用到

};

关于deploy ,细节先略过,可以看到带上了collection参数。

deploy(opt, collection, total); // 部署~
依赖扫描概述

在增量编译的时候,有个细节点很关键,变化的文件,可能被其他资源所引用(如内嵌),那么这时,除了编译文件之身,还需要对引用它的文件也进行编译。

原先我的想法是:

扫描所有资源,并建立依赖分析表。比如某个文件,被多少文件引用了。 某个文件发生变化,扫描依赖分析表,对引用这个文件的文件进行重新编译。

看了下FIS的实现,虽然大体思路是一致的,不过是反向操作。从资源引用方作为起始点,递归式地对引用的资源进行编译,并添加到资源依赖表里。

扫描文件,看是否有资源依赖。如有,对依赖的资源进行编译,并添加到依赖表里。(递归) 编译文件。 从例子出发

假设项目结构如下,仅有index.html、index.cc两个文件,且 index.html 通过 __inline 标记嵌入 index.css。

^CadeMacBook-Pro-3:fi a$ tree

├── index.css

└── index.html

index.html 内容如下。

 !DOCTYPE html 

 html 

 head 

 title /title 

 link rel="stylesheet" type="text/css" href="index.css?__inline" 

 /head 

 body 

 /body 

 /html 

假设文件内容发生了变化,理论上应该是这样

index.html 变化:重新编译 index.html index.css 变化:重新编译 index.css,重新编译 index.html

理论是直观的,那么看下内部是怎么实现这个逻辑的。先归纳如下,再看源码

对需要编译的每个源文件,都创建一个Cache实例,假设是cache。cache里存放了一些信息,比如文件的内容,文件的依赖列表(deps字段,一个哈希表,存放依赖文件路径到最近修改时间的映射)。 对需要编译的每个源文件,扫描它的依赖,包括通过__inline内嵌的资源,并通过cache.addDeps(file)添加到deps里。 文件发生变化,检查文件本身内容,以及依赖内容(deps)是否发生变化。如变化,则重新编译。在这个例子里,扫描index.html,发现index.html本身没有变化,但deps发生了变化,那么,重新编译部署index.html。

好,看源码。在compile.js里面,cache.revert(revertObj)这个方法检测文件本身、文件依赖的资源是否变化。

 if(file.isFile()){

 if(file.useCompile file.ext file.ext !== .){

 var cache = file.cache = fis.cache(file.realpath, CACHE_DIR), // 为文件建立缓存(路径)

 revertObj = {};

 // 目测是检测缓存过期了没,如果只是跑 fis release ,直接进else

 if(file.useCache cache.revert(revertObj)){ // 检查依赖的资源(deps)是否发生变化,就在 cache.revert(revertObj)这个方法里

 exports.settings.beforeCacheRevert(file);

 file.requires = revertObj.info.requires;

 file.extras = revertObj.info.extras;

 if(file.isText()){

 revertObj.content = revertObj.content.toString(utf8);

 file.setContent(revertObj.content);

 exports.settings.afterCacheRevert(file);

 } else {

看看cache.revert是如何定义的。大致归纳如下,源码不难看懂。至于infos.deps这货怎么来的,下面会立刻讲到。

方法的返回值:缓存没过期,返回true;缓存过期,返回false 缓存检查步骤:首先,检查文件本身是否发生变化,如果没有,再检查文件依赖的资源是否发生变化;
 // 如果过期,返回false;没有过期,返回true

 // 注意,穿进来的file对象会被修改,往上挂属性

 revert : function(file){

 fis.log.debug(revert cache);

 // this.cacheInfo、this.cacheFile 中存储了文件缓存相关的信息

 // 如果还不存在,说明缓存还没建立哪(或者被人工删除了也有可能,这种变态情况不多)

 exports.enable

 fis.util.exists(this.cacheInfo)

 fis.util.exists(this.cacheFile)

 fis.log.debug(cache file exists);

 var infos = fis.util.readJSON(this.cacheInfo);

 fis.log.debug(cache info read);

 // 首先,检测文件本身是否发生变化

 if(infos.version == this.version infos.timestamp == this.timestamp){

 // 接着,检测文件依赖的资源是否发生变化

 // infos.deps 这货怎么来的,可以看下compile.js 里的实现

 var deps = infos[deps];

 for(var f in deps){

 if(deps.hasOwnProperty(f)){

 var d = fis.util.mtime(f);

 if(d == 0 || deps[f] != d.getTime()){ // 过期啦!!

 fis.log.debug(cache is expired);

 return false;

 this.deps = deps;

 fis.log.debug(cache is valid);

 if(file){

 file.info = infos.info;

 file.content = fis.util.fs.readFileSync(this.cacheFile);

 fis.log.debug(revert cache finished);

 return true;

 fis.log.debug(cache is expired);

 return false;

 },
依赖扫描细节

之前多次提到deps这货,这里就简单讲下依赖扫描的过程。还是之前compile.js里那段代码。归纳如下:

文件缓存不存在,或者文件缓存已过期,进入第二个处理分支 在第二个处理分支里,会调用process(file)这个方法对文件进行处理。里面进行了一系列操作,如文件的“标准化”处理等。在这个过程中,扫描出文件的依赖,并写到deps里去。

下面会以“标准化”为例,进一步讲解依赖扫描的过程。

if(file.useCompile file.ext file.ext !== .){

 var cache = file.cache = fis.cache(file.realpath, CACHE_DIR), // 为文件建立缓存(路径)

 revertObj = {};

 // 目测是检测缓存过期了没,如果只是跑 fis release ,直接进else

 if(file.useCache cache.revert(revertObj)){

 exports.settings.beforeCacheRevert(file);

 file.requires = revertObj.info.requires;

 file.extras = revertObj.info.extras;

 if(file.isText()){

 revertObj.content = revertObj.content.toString(utf8);

 file.setContent(revertObj.content);

 exports.settings.afterCacheRevert(file);

 } else {

 // 缓存过期啦!!缓存还不存在啊!都到这里面来!!

 exports.settings.beforeCompile(file);

 file.setContent(fis.util.read(file.realpath)); 

 process(file); // 这里面会对文件进行"标准化"等处理

 exports.settings.afterCompile(file);

 revertObj = {

 requires : file.requires,

 extras : file.extras

 cache.save(file.getContent(), revertObj);

 }

在process里,对文件进行了标准化操作。什么是标准化,可以参考官方文档。就是下面这小段代码

 if(file.useStandard !== false){

 standard(file);

 }

看下standard内部是如何实现的。可以看到,针对类HTML、类JS、类CSS,分别进行了不同的能力扩展(包括内嵌)。比如上面的index.html,就会进入extHtml(content)。这个方法会扫描html文件的__inline标记,然后替换成特定的占位符,并将内嵌的资源加入依赖列表。

比如,文件的 link href="index.css?__inline" / 会被替换成 style type="text/css" embed:"index.css?__inline" 。

function standard(file){

 var path = file.realpath,

 content = file.getContent();

 if(typeof content === string){

 fis.log.debug(standard start);

 //expand language ability

 if(file.isHtmlLike){

 content = extHtml(content); // 如果有 link href="index1.css?__inline" / 会被替换成 style type="text/css" embed:"index1.css?__inline" 这样的占位符

 } else if(file.isJsLike){

 content = extJs(content);

 } else if(file.isCssLike){

 content = extCss(content);

 content = content.replace(map.reg, function(all, type, value){

 // 虽然这里很重要,还是先省略代码很多很多行

}

然后,在content.replace里面,将进入embed这个分支。从源码可以大致看出逻辑如下,更多细节就先不展开了。

首先对内嵌的资源进行合法性检查,如果通过,进行下一步 编译内嵌的资源。(一个递归的过程) 将内嵌的资源加到依赖列表里。
content = content.replace(map.reg, function(all, type, value){

 var ret = , info;

 try {

 switch(type){

 case require:

 // 省略...

 case uri:

 // 省略...

 case dep:

 // 省略

 case embed:

 case jsEmbed:

 info = fis.uri(value, file.dirname); // value == ""index.css?__inline""

 var f;

 if(info.file){

 f = info.file;

 } else if(fis.util.isAbsolute(info.rest)){

 f = fis.file(info.rest);

 if(f f.isFile()){

 if(embeddedCheck(file, f)){ // 一切合法性检查,比如有没有循环引用之类的

 exports(f); // 编译依赖的资源

 addDeps(file, f); // 添加到依赖列表

 f.requires.forEach(function(id){ 

 file.addRequire(id);

 if(f.isText()){

 ret = f.getContent();

 if(type === jsEmbed !f.isJsLike !f.isJsonLike){

 ret = JSON.stringify(ret);

 } else {

 ret = info.quote + f.getBase64() + info.quote;

 } else {

 fis.log.error(unable to embed non-existent file [ + value + ]);

 break;

 default :

 fis.log.error(unsupported fis language tag [ + type + ]);

 } catch (e) {

 embeddedMap = {};

 e.message = e.message +  in [ + file.subpath + ];

 throw e;

 return ret;

 });

更多内容,敬请期待。


java关于File类源码的详细分析 附代码(全) 添加链接描述磁驱动分割符中,在unix中使用/表示,在window中使用\\\查看其源码,实现Serializable,Comparable的接口java之序列化与反序列化的详细解析(全)javaSE从入门到精通的二十万字总结(二)
Java Properties类新增、更新及写入文件【解决中文乱码问题】 在读.properties取配置文件时,我们经常用的就是Properties类库。本文主要讲解如何通过类来新增及编辑对应的Properties属性值,并将其写入文件。