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

FIS源码-增量编译与依赖扫描细节 #30

Open
chyingp opened this issue May 11, 2015 · 0 comments
Open

FIS源码-增量编译与依赖扫描细节 #30

chyingp opened this issue May 11, 2015 · 0 comments

Comments

@chyingp
Copy link
Owner

chyingp commented May 11, 2015

开篇

前面已经提到了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 中找到。归纳下:

  1. 对比了下当前文件的最近修改时间,看下跟上次缓存的修改时间是否一致。如果不一致,重新编译,并将编译后的实例添加到collection中去。
  2. 执行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); // 部署~

依赖扫描概述

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

原先我的想法是:

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

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

  1. 扫描文件,看是否有资源依赖。如有,对依赖的资源进行编译,并添加到依赖表里。(递归)
  2. 编译文件。

从例子出发

假设项目结构如下,仅有index.htmlindex.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>

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

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

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

  1. 对需要编译的每个源文件,都创建一个Cache实例,假设是cache。cache里存放了一些信息,比如文件的内容,文件的依赖列表(deps字段,一个哈希表,存放依赖文件路径到最近修改时间的映射)。
  2. 对需要编译的每个源文件,扫描它的依赖,包括通过__inline内嵌的资源,并通过cache.addDeps(file)添加到deps里。
  3. 文件发生变化,检查文件本身内容,以及依赖内容(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这货怎么来的,下面会立刻讲到。

  1. 方法的返回值:缓存没过期,返回true;缓存过期,返回false
  2. 缓存检查步骤:首先,检查文件本身是否发生变化,如果没有,再检查文件依赖的资源是否发生变化;
    // 如果过期,返回false;没有过期,返回true
    // 注意,穿进来的file对象会被修改,往上挂属性
    revert : function(file){
        fis.log.debug('revert cache');
        // this.cacheInfo、this.cacheFile 中存储了文件缓存相关的信息
        // 如果还不存在,说明缓存还没建立哪(或者被人工删除了也有可能,这种变态情况不多)
        if(
            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里那段代码。归纳如下:

  1. 文件缓存不存在,或者文件缓存已过期,进入第二个处理分支
  2. 在第二个处理分支里,会调用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这个分支。从源码可以大致看出逻辑如下,更多细节就先不展开了。

  1. 首先对内嵌的资源进行合法性检查,如果通过,进行下一步
  2. 编译内嵌的资源。(一个递归的过程)
  3. 将内嵌的资源加到依赖列表里。
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;
        });

写在后面

更多内容,敬请期待。

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

No branches or pull requests

1 participant