IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    TAT.casperchen, TAT.casperchen发表于 2015-05-11 13:50:57
    love 0

    前面已经提到了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 = /[\/][_-.sw]+$/i;    // 是否安全路径(参考)
        var ignoredReg = /[/](?:outputb[^/]*([/]|$)|.|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.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>

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

    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;
            });

    写在后面

    更多内容,敬请期待。

    文章: casperchen



沪ICP备19023445号-2号
友情链接