FIS 源码分析 增量编译与依赖扫描细节
前面已经提到了 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 中存储了文件缓存相关的信息
// 如果还不存在,说明缓存还没建立哪(或者被人工删除了也有可能,这种变态情况不多)
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;
});
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: FIS 源码解析 整体架构
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论