前面已经提到了fis release
命令大致的运行流程。本文会进一步讲解增量编译以及依赖扫描的一些细节。
首先,在fis release
后加上--watch
参数,看下会有什么样的变化。打开命令行
fis release --watch
不难猜想,内部同样是调用release()
方法把源文件编译一遍。区别在于,进程会监听项目路径下源文件的变化,一旦出现文件(夹)的增、删、改,则重新调用release()
进行增量编译。
并且,如果资源之间存在依赖关系(比如资源内嵌),那么一些情况下,被依赖资源的变化,会反过来导致资源引用方的重新编译。
// 是否自动重新编译 if(options.watch){ watch(options); // 对!就是这里 } else { release(options); }
下面扒扒源码来验证下我们的猜想。
源码不算长,逻辑也比较清晰,这里就不上伪代码了,直接贴源码出来,附上一些注释,应该不难理解,无非就是重复文件变化–>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
中找到。归纳下:
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>
假设文件内容发生了变化,理论上应该是这样
理论是直观的,那么看下内部是怎么实现这个逻辑的。先归纳如下,再看源码
__inline
内嵌的资源,并通过cache.addDeps(file)
添加到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
这货怎么来的,下面会立刻讲到。
// 如果过期,返回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
里那段代码。归纳如下:
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; });
更多内容,敬请期待。
文章: casperchen