前面已经提到了 fis release
命令大致的运行流程。本文会进一步讲解增量编译以及依赖扫描的一些细节。
首先,在 fis release
后加上--watch
参数,看下会有什么样的变化。打开命令行
1 2 |
fis release --watch |
不难猜想,内部同样是调用 release()
方法把源文件编译一遍。区别在于,进程会监听项目路径下源文件的变化,一旦出现文件(夹)的增、删、改,则重新调用 release()
进行增量编译。
并且,如果资源之间存在依赖关系(比如资源内嵌),那么一些情况下,被依赖资源的变化,会反过来导致资源引用方的重新编译。
1 2 3 4 5 6 7 |
<span class="comment">// 是否自动重新编译</span> <span class="keyword">if</span>(options.watch){ watch(options); <span class="comment">// 对!就是这里</span> } <span class="keyword">else</span> { release(options); } |
下面扒扒源码来验证下我们的猜想。
watch(opt) 细节
源码不算长,逻辑也比较清晰,这里就不上伪代码了,直接贴源码出来,附上一些注释,应该不难理解,无非就是重复文件变化–>release(opt)这个过程。
在下一小结稍稍展开下增量编译的细节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
<span class="keyword">function</span> watch(opt){ <span class="keyword">var</span> root = fis.project.getProjectPath(); <span class="keyword">var</span> timer = -<span class="number">1</span>; <span class="keyword">var</span> safePathReg = /[\/][_-.sw]+$/i; <span class="comment">// 是否安全路径(参考)</span> <span class="keyword">var</span> ignoredReg = /[/](?:outputb[^/]*([/]|$)|.|fis-conf.js$)/i; <span class="comment">// ouput路径下的,或者 fis-conf.js 排除,不参与监听</span> opt.srcCache = fis.project.getSource(); <span class="comment">// 缓存映射表,代表参与编译的源文件;格式为 源文件路径=>源文件对应的File实例。比较奇怪的是,opt.srcCache 没见到有地方用到,在 fis.release 里,fis.project.getSource() 会重新调用,这里感觉有点多余</span> <span class="comment">// 根据传入的事件类型(type),返回对应的回调方法</span> <span class="comment">// type 的取值有add、change、unlink、unlinkDir</span> <span class="keyword">function</span> listener(type){ <span class="keyword">return</span> <span class="keyword">function</span> (path) { <span class="keyword">if</span>(safePathReg.test(path)){ <span class="keyword">var</span> file = fis.file.wrap(path); <span class="keyword">if</span> (type == <span class="string">'add'</span> || type == <span class="string">'change'</span>) { <span class="comment">// 新增 或 修改文件</span> <span class="keyword">if</span> (!opt.srcCache[file.subpath]) { <span class="comment">// 新增的文件,还不在 opt.srcCache 里</span> <span class="keyword">var</span> file = fis.file(path); opt.srcCache[file.subpath] = file; <span class="comment">// 从这里可以知道 opt.srcCache 的数据结构了,不展开</span> } } <span class="keyword">else</span> <span class="keyword">if</span> (type == <span class="string">'unlink'</span>) { <span class="comment">// 删除文件</span> <span class="keyword">if</span> (opt.srcCache[file.subpath]) { delete opt.srcCache[file.subpath]; <span class="comment">// </span> } } <span class="keyword">else</span> <span class="keyword">if</span> (type == <span class="string">'unlinkDir'</span>) { <span class="comment">// 删除目录</span> fis.util.map(opt.srcCache, <span class="keyword">function</span> (subpath, file) { <span class="keyword">if</span> (file.realpath.indexOf(path) !== -<span class="number">1</span>) { delete opt.srcCache[subpath]; } }); } clearTimeout(timer); timer = setTimeout(<span class="keyword">function</span>(){ release(opt); <span class="comment">// 编译,增量编译的细节在内部实现了</span> }, <span class="number">500</span>); } }; } <span class="comment">//添加usePolling配置</span> <span class="comment">// 这个配置项可以先忽略</span> <span class="keyword">var</span> usePolling = <span class="keyword">null</span>; <span class="keyword">if</span> (typeof fis.config.get(<span class="string">'project.watch.usePolling'</span>) !== <span class="string">'undefined'</span>){ usePolling = fis.config.get(<span class="string">'project.watch.usePolling'</span>); } <span class="comment">// chokidar模块,主要负责文件变化的监听</span> <span class="comment">// 除了error之外的所有事件,包括add、change、unlink、unlinkDir,都调用 listenter(eventType) 来处理</span> <span class="keyword">require</span>(<span class="string">'chokidar'</span>) .watch(root, { <span class="comment">// 当文件发生变化时候,会调用这个方法(参数是变化文件的路径)</span> <span class="comment">// 如果返回true,则不触发文件变化相关的事件</span> ignored : <span class="keyword">function</span>(path){ <span class="keyword">var</span> ignored = ignoredReg.test(path); <span class="comment">// 如果满足,则忽略</span> <span class="comment">// 从编译队列中排除</span> <span class="keyword">if</span> (fis.config.get(<span class="string">'project.exclude'</span>)){ ignored = ignored || fis.util.filter(path, fis.config.get(<span class="string">'project.exclude'</span>)); <span class="comment">// 此时 ignoredReg.test(path) 为false,如果在exclude里,ignored也为true</span> } <span class="comment">// 从watch中排除</span> <span class="keyword">if</span> (fis.config.get(<span class="string">'project.watch.exclude'</span>)){ ignored = ignored || fis.util.filter(path, fis.config.get(<span class="string">'project.watch.exclude'</span>)); <span class="comment">// 跟上面类似</span> } <span class="keyword">return</span> ignored; }, usePolling: usePolling, persistent: <span class="keyword">true</span> }) .on(<span class="string">'add'</span>, listener(<span class="string">'add'</span>)) .on(<span class="string">'change'</span>, listener(<span class="string">'change'</span>)) .on(<span class="string">'unlink'</span>, listener(<span class="string">'unlink'</span>)) .on(<span class="string">'unlinkDir'</span>, listener(<span class="string">'unlinkDir'</span>)) .on(<span class="string">'error'</span>, <span class="keyword">function</span>(err){ <span class="comment">//fis.log.error(err);</span> }); } |
增量编译细节
增量编译的要点很简单,就是只发生变化的文件进行编译部署。在 fis.release(opt, callback)
里,有这段代码:
1 2 3 4 5 6 7 8 9 10 |
<span class="comment">// ret.src 为项目下的源文件</span> fis.util.map(ret.src, <span class="keyword">function</span>(subpath, file){ <span class="keyword">if</span>(opt.beforeEach) { opt.beforeEach(file, ret); } file = fis.compile(file); <span class="keyword">if</span>(opt.afterEach) { opt.afterEach(file, ret); <span class="comment">// 这里这里!</span> } |
opt.afterEach(file, ret)
这个回调方法可以在 fis-command-release/release.js
中找到。归纳下:
- 对比了下当前文件的最近修改时间,看下跟上次缓存的修改时间是否一致。如果不一致,重新编译,并将编译后的实例添加到
collection
中去。 - 执行
deploy
进行增量部署。(带着 collection 参数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
opt.afterEach = <span class="keyword">function</span>(file){ <span class="comment">//cal compile time</span> <span class="comment">// 略过无关代码</span> <span class="keyword">var</span> mtime = file.getMtime().getTime(); <span class="comment">// 源文件的最近修改时间</span> <span class="comment">//collect file to deploy</span> <span class="comment">// 如果符合这几个条件:1、文件需要部署 2、最近修改时间 不等于 上一次缓存的修改时间</span> <span class="comment">// 那么重新编译部署</span> <span class="keyword">if</span>(file.release && lastModified[file.subpath] !== mtime){ <span class="comment">// 略过无关代码</span> lastModified[file.subpath] = mtime; collection[file.subpath] = file; <span class="comment">// 这里这里!!在 deploy 方法里会用到</span> } }; |
关于 deploy
,细节先略过,可以看到带上了 collection
参数。
1 2 |
deploy(opt, collection, total); <span class="comment">// 部署~</span> |
依赖扫描概述
在增量编译的时候,有个细节点很关键,变化的文件,可能被其他资源所引用(如内嵌),那么这时,除了编译文件之身,还需要对引用它的文件也进行编译。
原先我的想法是:
- 扫描所有资源,并建立依赖分析表。比如某个文件,被多少文件引用了。
- 某个文件发生变化,扫描依赖分析表,对引用这个文件的文件进行重新编译。
看了下 FIS 的实现,虽然大体思路是一致的,不过是反向操作。从资源引用方作为起始点,递归式地对引用的资源进行编译,并添加到资源依赖表里。
- 扫描文件,看是否有资源依赖。如有,对依赖的资源进行编译,并添加到依赖表里。(递归)
- 编译文件。
从例子出发
假设项目结构如下,仅有 index.html
、index.cc
两个文件,且 index.html
通过 __inline
标记嵌入 index.css
。
1 2 3 4 5 |
^CadeMacBook-Pro-3:fi a$ tree . ├── index.css └── index.html |
index.html
内容如下。
1 2 3 4 5 6 7 8 9 10 11 |
<!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)
这个方法检测文件本身、文件依赖的资源是否变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<span class="keyword">if</span>(file.isFile()){ <span class="keyword">if</span>(file.useCompile && file.ext && file.ext !== <span class="string">'.'</span>){ <span class="keyword">var</span> cache = file.cache = fis.cache(file.realpath, CACHE_DIR), <span class="comment">// 为文件建立缓存(路径)</span> revertObj = {}; <span class="comment">// 目测是检测缓存过期了没,如果只是跑 fis release ,直接进else</span> <span class="keyword">if</span>(file.useCache && cache.revert(revertObj)){ <span class="comment">// 检查依赖的资源(deps)是否发生变化,就在 cache.revert(revertObj)这个方法里</span> exports.settings.beforeCacheRevert(file); file.requires = revertObj.info.requires; file.extras = revertObj.info.extras; <span class="keyword">if</span>(file.isText()){ revertObj.content = revertObj.content.toString(<span class="string">'utf8'</span>); } file.setContent(revertObj.content); exports.settings.afterCacheRevert(file); } <span class="keyword">else</span> { |
看看 cache.revert
是如何定义的。大致归纳如下,源码不难看懂。至于 infos.deps
这货怎么来的,下面会立刻讲到。
- 方法的返回值:缓存没过期,返回 true;缓存过期,返回 false
- 缓存检查步骤:首先,检查文件本身是否发生变化,如果没有,再检查文件依赖的资源是否发生变化;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<span class="comment">// 如果过期,返回false;没有过期,返回true</span> <span class="comment">// 注意,穿进来的file对象会被修改,往上挂属性</span> revert : <span class="keyword">function</span>(file){ fis.log.debug(<span class="string">'revert cache'</span>); <span class="comment">// this.cacheInfo、this.cacheFile 中存储了文件缓存相关的信息</span> <span class="comment">// 如果还不存在,说明缓存还没建立哪(或者被人工删除了也有可能,这种变态情况不多)</span> <span class="keyword">if</span>( exports.enable && fis.util.exists(<span class="keyword">this</span>.cacheInfo) && fis.util.exists(<span class="keyword">this</span>.cacheFile) ){ fis.log.debug(<span class="string">'cache file exists'</span>); <span class="keyword">var</span> infos = fis.util.readJSON(<span class="keyword">this</span>.cacheInfo); fis.log.debug(<span class="string">'cache info read'</span>); <span class="comment">// 首先,检测文件本身是否发生变化</span> <span class="keyword">if</span>(infos.version == <span class="keyword">this</span>.version && infos.timestamp == <span class="keyword">this</span>.timestamp){ <span class="comment">// 接着,检测文件依赖的资源是否发生变化</span> <span class="comment">// infos.deps 这货怎么来的,可以看下compile.js 里的实现</span> <span class="keyword">var</span> deps = infos[<span class="string">'deps'</span>]; <span class="keyword">for</span>(<span class="keyword">var</span> f in deps){ <span class="keyword">if</span>(deps.hasOwnProperty(f)){ <span class="keyword">var</span> d = fis.util.mtime(f); <span class="keyword">if</span>(d == <span class="number">0</span> || deps[f] != d.getTime()){ <span class="comment">// 过期啦!!</span> fis.log.debug(<span class="string">'cache is expired'</span>); <span class="keyword">return</span> <span class="keyword">false</span>; } } } <span class="keyword">this</span>.deps = deps; fis.log.debug(<span class="string">'cache is valid'</span>); <span class="keyword">if</span>(file){ file.info = infos.info; file.content = fis.util.fs.readFileSync(<span class="keyword">this</span>.cacheFile); } fis.log.debug(<span class="string">'revert cache finished'</span>); <span class="keyword">return</span> <span class="keyword">true</span>; } } fis.log.debug(<span class="string">'cache is expired'</span>); <span class="keyword">return</span> <span class="keyword">false</span>; }, |
依赖扫描细节
之前多次提到 deps
这货,这里就简单讲下依赖扫描的过程。还是之前 compile.js
里那段代码。归纳如下:
- 文件缓存不存在,或者文件缓存已过期,进入第二个处理分支
- 在第二个处理分支里,会调用
process(file)
这个方法对文件进行处理。里面进行了一系列操作,如文件的 “标准化” 处理等。在这个过程中,扫描出文件的依赖,并写到deps
里去。
下面会以 “标准化” 为例,进一步讲解依赖扫描的过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<span class="keyword">if</span>(file.useCompile && file.ext && file.ext !== <span class="string">'.'</span>){ <span class="keyword">var</span> cache = file.cache = fis.cache(file.realpath, CACHE_DIR), <span class="comment">// 为文件建立缓存(路径)</span> revertObj = {}; <span class="comment">// 目测是检测缓存过期了没,如果只是跑 fis release ,直接进else</span> <span class="keyword">if</span>(file.useCache && cache.revert(revertObj)){ exports.settings.beforeCacheRevert(file); file.requires = revertObj.info.requires; file.extras = revertObj.info.extras; <span class="keyword">if</span>(file.isText()){ revertObj.content = revertObj.content.toString(<span class="string">'utf8'</span>); } file.setContent(revertObj.content); exports.settings.afterCacheRevert(file); } <span class="keyword">else</span> { <span class="comment">// 缓存过期啦!!缓存还不存在啊!都到这里面来!!</span> exports.settings.beforeCompile(file); file.setContent(fis.util.read(file.realpath)); process(file); <span class="comment">// 这里面会对文件进行"标准化"等处理</span> exports.settings.afterCompile(file); revertObj = { requires : file.requires, extras : file.extras }; cache.save(file.getContent(), revertObj); } } |
在 process
里,对文件进行了标准化操作。什么是标准化,可以参考官方文档。就是下面这小段代码
1 2 3 4 |
<span class="keyword">if</span>(file.useStandard !== <span class="keyword">false</span>){ 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">>>
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<span class="keyword">function</span> standard(file){ <span class="keyword">var</span> path = file.realpath, content = file.getContent(); <span class="keyword">if</span>(typeof content === <span class="string">'string'</span>){ fis.log.debug(<span class="string">'standard start'</span>); <span class="comment">//expand language ability</span> <span class="keyword">if</span>(file.isHtmlLike){ content = extHtml(content); <span class="comment">// 如果有 <link href="index1.css?__inline" /> 会被替换成 <style type="text/css"><<<embed:"index1.css?__inline">>> 这样的占位符</span> } <span class="keyword">else</span> <span class="keyword">if</span>(file.isJsLike){ content = extJs(content); } <span class="keyword">else</span> <span class="keyword">if</span>(file.isCssLike){ content = extCss(content); } content = content.replace(map.reg, <span class="keyword">function</span>(all, type, value){ <span class="comment">// 虽然这里很重要,还是先省略代码很多很多行</span> } } |
然后,在 content.replace
里面,将进入 embed
这个分支。从源码可以大致看出逻辑如下,更多细节就先不展开了。
- 首先对内嵌的资源进行合法性检查,如果通过,进行下一步
- 编译内嵌的资源。(一个递归的过程)
- 将内嵌的资源加到依赖列表里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
content = content.replace(map.reg, <span class="keyword">function</span>(all, type, value){ <span class="keyword">var</span> ret = <span class="string">''</span>, info; <span class="keyword">try</span> { <span class="keyword">switch</span>(type){ <span class="keyword">case</span> <span class="string">'require'</span>: <span class="comment">// 省略...</span> <span class="keyword">case</span> <span class="string">'uri'</span>: <span class="comment">// 省略...</span> <span class="keyword">case</span> <span class="string">'dep'</span>: <span class="comment">// 省略</span> <span class="keyword">case</span> <span class="string">'embed'</span>: <span class="keyword">case</span> <span class="string">'jsEmbed'</span>: info = fis.uri(value, file.dirname); <span class="comment">// value ==> ""index.css?__inline""</span> <span class="keyword">var</span> f; <span class="keyword">if</span>(info.file){ f = info.file; } <span class="keyword">else</span> <span class="keyword">if</span>(fis.util.isAbsolute(info.rest)){ f = fis.file(info.rest); } <span class="keyword">if</span>(f && f.isFile()){ <span class="keyword">if</span>(embeddedCheck(file, f)){ <span class="comment">// 一切合法性检查,比如有没有循环引用之类的</span> exports(f); <span class="comment">// 编译依赖的资源</span> addDeps(file, f); <span class="comment">// 添加到依赖列表</span> f.requires.<span class="keyword">forEach</span>(<span class="keyword">function</span>(id){ file.addRequire(id); }); <span class="keyword">if</span>(f.isText()){ ret = f.getContent(); <span class="keyword">if</span>(type === <span class="string">'jsEmbed'</span> && !f.isJsLike && !f.isJsonLike){ ret = JSON.stringify(ret); } } <span class="keyword">else</span> { ret = info.quote + f.getBase64() + info.quote; } } } <span class="keyword">else</span> { fis.log.error(<span class="string">'unable to embed non-existent file ['</span> + value + <span class="string">']'</span>); } <span class="keyword">break</span>; <span class="keyword">default</span> : fis.log.error(<span class="string">'unsupported fis language tag ['</span> + type + <span class="string">']'</span>); } } <span class="keyword">catch</span> (e) { embeddedMap = {}; e.message = e.message + <span class="string">' in ['</span> + file.subpath + <span class="string">']'</span>; <span class="keyword">throw</span> e; } <span class="keyword">return</span> ret; }); |
写在后面
更多内容,敬请期待。
文章: casperchen