fis源码笔记
28 Sep 2017
第二次走读fis源码 了,带着上一次读源码的笔记,再来读一遍时,有了更好的认识,虽然可能不够全面,但觉得有必要写文章记录一下。
我本地下载的fis版本是 1.10.1
目录结构
目录结构也算很清晰了,由于其是作为命令程序的存在,所以有bin
目录
主要结构如下:(不全)
fis
—| bin
—–| fis
—| node_modules
—–| fis-command-*
—–| fis-kernel
—–| fis-optimizer-*
—–| fis-postprocessor-*
—–| fis-preprocessor-*
—–| fis-spriter-csssprites
—| fis.js
这里我主要分析的是 fis、fis.js、fis-command-release、fis-kernel,其他的略过。
我们一般使用fis命令来执行:
$ fis release -d online -p -m -o -D -w
首先,这个命令会执行 bin/fis 文件,内容很简单,就是直接调用了fis.js模块的cli.run
方法
我们看fis.js的主要代码片段:
// fis用的是fis-kernel模块
var fis = module . exports = require ( ' fis-kernel ' );
// 配置基本的模块插件
fis . config . merge ({
modules : {
preprocessor : {
js : ' components ' ,
css : ' components ' ,
html : ' components '
},
postprocessor : {
js : ' jswrapper '
},
optimizer : {
js : ' uglify-js ' ,
css : ' clean-css ' ,
png : ' png-compressor '
},
spriter : ' csssprites ' ,
packager : ' map ' ,
deploy : ' default ' ,
prepackager : ' derived '
}
});
fis . cli = {};
//省略一系列代码
fis . cli . run = function ( argv ){
fis . processCWD = process . cwd ();
if ( hasArgv ( argv , ' --no-color ' )){
fis . cli . colors . mode = ' none ' ;
}
var first = argv [ 2 ];
if ( argv . length < 3 || first === ' -h ' || first === ' --help ' ){
fis . cli . help ();
} else if ( first === ' -v ' || first === ' --version ' ){
fis . cli . version ();
} else if ( first [ 0 ] === ' - ' ){
fis . cli . help ();
} else {
//register command
var commander = fis . cli . commander = require ( ' commander ' );
var cmd = fis . require ( ' command ' , argv [ 2 ]);
cmd . register (
commander
. command ( cmd . name || first )
. usage ( cmd . usage )
. description ( cmd . desc )
);
commander . parse ( argv );
}
};
可以看到命令行主要是用了commander
来控制命令行,其中
var cmd = fis . require ( ' command ' , argv [ 2 ]);
我们知道argv[2]
,指代的是release
.(argv[0] 是node
),这里实际是获取了fis-command-release
. 这个后面说明。
于是可以明确,当使用了fis release
指令的时候,经过了bin/fis
->fis.js
->fis-command-release
的一个走读流程。
核心源码
从fis.js可以看到, fis核心实现是fis-kernel
模块,其中该模块又分为cache
、compile
、config
、file
、log
、project
、release
、uri
、util
这几个部分。
从命名上,可以看出大概:缓存、编译、配置、文件(fis文件对象)、日志、项目、发布、资源定位、工具。
其中,release是fis release的主要逻辑,compile则是release的细化单元之一。
面向对象
fis-kernel自身的代码主要是对fis这个对象的一个整合以及面向对象的补充。
Function . prototype . derive = function ( constructor , proto ){
if ( typeof constructor === ' object ' ){
proto = constructor ;
constructor = proto . constructor || function (){};
delete proto . constructor ;
}
var parent = this ;
var fn = function (){
parent . apply ( this , arguments );
constructor . apply ( this , arguments );
};
var tmp = function (){};
tmp . prototype = parent . prototype ;
var fp = new tmp (),
cp = constructor . prototype ,
key ;
for ( key in cp ){
if ( cp . hasOwnProperty ( key )){
fp [ key ] = cp [ key ];
}
}
proto = proto || {};
for ( key in proto ){
if ( proto . hasOwnProperty ( key )){
fp [ key ] = proto [ key ];
}
}
fp . constructor = constructor . prototype . constructor ;
fn . prototype = fp ;
return fn ;
};
//factory
Function . prototype . factory = function (){
var clazz = this ;
function F ( args ){
clazz . apply ( this , args );
}
F . prototype = clazz . prototype ;
return function (){
return new F ( arguments );
};
};
var fis = module . exports = {};
//register global variable
Object . defineProperty ( global , ' fis ' , {
enumerable : true ,
writable : false ,
value : fis
});
fis . emitter = new ( require ( ' events ' ). EventEmitter );
//time for debug
fis . time = function ( title ){
console . log ( title + ' : ' + ( Date . now () - last ) + ' ms ' );
last = Date . now ();
};
//log
fis . log = require ( ' ./lib/log.js ' );
//require
fis . require = function (){
var path ;
var name = Array . prototype . slice . call ( arguments , 0 ). join ( ' - ' );
if ( fis . require . _cache . hasOwnProperty ( name )) return fis . require . _cache [ name ];
var names = [];
for ( var i = 0 , len = fis . require . prefixes . length ; i < len ; i ++ ){
try {
var pluginName = fis . require . prefixes [ i ] + ' - ' + name ;
names . push ( pluginName );
path = require . resolve ( pluginName );
try {
return fis . require . _cache [ name ] = require ( pluginName );
} catch ( e ){
fis . log . error ( ' load plugin [ ' + pluginName + ' ] error : ' + e . message );
}
} catch ( e ){
if ( e . code !== ' MODULE_NOT_FOUND ' ) {
throw e ;
}
}
}
fis . log . error ( ' unable to load plugin [ ' + names . join ( ' ] or [ ' ) + ' ] ' );
};
fis . require . _cache = {};
fis . require . prefixes = [ ' fis ' ];
//system config
fis . config = require ( ' ./lib/config.js ' );
//utils
fis . util = require ( ' ./lib/util.js ' );
//resource location
fis . uri = require ( ' ./lib/uri.js ' );
//project
fis . project = require ( ' ./lib/project.js ' );
//file
fis . file = require ( ' ./lib/file.js ' );
//cache
fis . cache = require ( ' ./lib/cache.js ' );
//compile kernel
fis . compile = require ( ' ./lib/compile.js ' );
//release api
fis . release = require ( ' ./lib/release.js ' );
//package info
fis . info = fis . util . readJSON ( __dirname + ' /package.json ' );
//kernel version
fis . version = fis . info . version ;
开篇就扩展了Function原型两个方法,derive
和factory
.
factory其实很好理解,就是返回一个普通方法,省去了使用原方法new构造模式的繁琐。
查看原图
derive 细看,其实是多继承,派生出一个构造器,它继承了原函数以及提供参数的构造器和原型对象。
查看原图
在fis源码模块中多次用到了这两个方法,且先看fis.file
,在lib/file.js里
module . exports = File . factory ();
于是如果使用fis.file(xx)
,就是相当于new File()
.
再看fis.config
, lib/config
var Config = Object . derive ({});
module . exports = ( new Config ). init ( DEFAULE_SETTINGS );
则fis.config
代表的是一个已经实例化的Config对象。
了解了这两个方法的作用,就能比较好的理解lib里的代码了。
插件配合方式 与 pipe
fis的插件主要分三种:单文件编译插件、打包插件、命令行插件,详见插件扩展点列表
fis-kernel入口文件的代码里,声明里fis.require
方法,这是fis模块化处理的一个核心点,并且还使用了成员变量_cache
来缓存模块,fis的每一个插件都是一个独立的npm包,那么总需要有require
,而fis.require
就实现了这个。
fis.require要结合实际应用来看才容易理解,这个涉及到pipe
方法的使用。
代码里涉及到pipe的主要有三处.
util的pipe工具方法:
_ . pipe = function ( type , callback , def ){
var processors = fis . config . get ( ' modules. ' + type , def );
if ( processors ){
var typeOf = typeof processors ;
if ( typeOf === ' string ' ){
processors = processors . trim (). split ( / \s *, \s */ );
} else if ( typeOf === ' function ' ){
processors = [ processors ];
}
type = type . split ( ' . ' )[ 0 ];
processors . forEach ( function ( processor , index ){
var typeOf = typeof processor , key ;
if ( typeOf === ' string ' ){
key = type + ' . ' + processor ;
processor = fis . require ( type , processor );
} else {
key = type + ' . ' + index ;
}
if ( typeof processor === ' function ' ){
var settings = fis . config . get ( ' settings. ' + key , {});
if ( processor . defaultOptions ){
settings = _ . merge ( processor . defaultOptions , settings );
}
callback ( processor , settings , key );
} else {
fis . log . warning ( ' invalid processor [modules. ' + key + ' ] ' );
}
});
}
};
compile的内部pipe方法:
function pipe ( file , type , ext , keep ){
var key = type + ext ;
fis . util . pipe ( key , function ( processor , settings , key ){
settings . filename = file . realpath ;
var content = file . getContent ();
try {
fis . log . debug ( ' pipe [ ' + key + ' ] start ' );
var result = processor ( content , file , settings );
fis . log . debug ( ' pipe [ ' + key + ' ] end ' );
if ( keep ){
file . setContent ( content );
} else if ( typeof result === ' undefined ' ){
fis . log . warning ( ' invalid content return of pipe [ ' + key + ' ] ' );
} else {
file . setContent ( result );
}
} catch ( e ) {
//log error
fis . log . debug ( ' pipe [ ' + key + ' ] fail ' );
var msg = key + ' : ' + String ( e . message || e . msg || e ). trim () + ' [ ' + ( e . filename || file . realpath );
if ( e . hasOwnProperty ( ' line ' )){
msg += ' : ' + e . line ;
if ( e . hasOwnProperty ( ' col ' )){
msg += ' : ' + e . col ;
} else if ( e . hasOwnProperty ( ' column ' )) {
msg += ' : ' + e . column ;
}
}
msg += ' ] ' ;
e . message = msg ;
error ( e );
}
});
}
然后就是compile中的pipe调用,process方法:
function process ( file ){
if ( file . useParser !== false ){
pipe ( file , ' parser ' , file . ext );
}
if ( file . rExt ){
if ( file . usePreprocessor !== false ){
pipe ( file , ' preprocessor ' , file . rExt );
}
if ( file . useStandard !== false ){
standard ( file );
}
if ( file . usePostprocessor !== false ){
pipe ( file , ' postprocessor ' , file . rExt );
}
if ( exports . settings . lint && file . useLint !== false ){
pipe ( file , ' lint ' , file . rExt , true );
}
if ( exports . settings . test && file . useTest !== false ){
pipe ( file , ' test ' , file . rExt , true );
}
if ( exports . settings . optimize && file . useOptimizer !== false ){
pipe ( file , ' optimizer ' , file . rExt );
}
}
}
关于process处理,这里其实是单文件编译的一个过程,详见编译过程运行原理 。
process的每一个pipe调用,实际就是对file对象的一次操作处理,以preprocessor
为例。
如果是处理一个js文件,那么如下
pipe(file, 'preprocessor', '.js');
//转化成
fis.util.pipe('preprocessor.js', function cb(){})
回头看util的pipe实现,会发现,其实这里pipe的主要作用是把对应的插件通过fis.require引入进来,然后回调给fis.util.pipe的第二参数回调函数使用。
最后得到的回调参数:
processor = fis.require('preprocessor', 'components');//结合前面提到的配置
最终到了fis.require处,其实就是得到一个require('fis-preprocessor-components')
可参考fis插件调用机制
到这里,就比较明了,插件的配合,通过fis配置来决定编译文件每个步骤需要的插件配置,最终通过fis.require来获取到对应的插件模块,然后在pipe函数里执行逻辑中,针对file对象进行一系列操作,重新setContent
.
每一层的pipe处理,基本都是在对file对象进行getContent和setContent。
在插件开发 处,fis介绍了编译插件和打包插件的开发方法。
由上面介绍的pipe方法,可知编译插件的代码开发格式大致如下:
module . exports = function ( content , file , settings ){
return xxx ;
};
而打包插件的开发格式则是:
module . exports = function ( ret , conf , settings , opt ) {
// ret.src 所有的源码,结构是 {'<subpath>': <File 对象>}
// ret.ids 所有源码列表,结构是 {'<id>': <File 对象>}
// ret.map 如果是 spriter、postpackager 这时候已经能得到打包结果了,
// 可以修改静态资源列表或者其他
}
两者的主要区别在于构造的插件函数接收参数不同,原因在于源码处的实现:(编译插件在上面提及了)
//package callback
var cb = function ( packager , settings , key ){
fis . log . debug ( ' [ ' + key + ' ] start ' );
packager ( ret , conf , settings , opt );
fis . log . debug ( ' [ ' + key + ' ] end ' );
};
//prepackage
fis . util . pipe ( ' prepackager ' , cb , opt . prepackager );
//package
if ( opt . pack ){
//package
fis . util . pipe ( ' packager ' , cb , opt . packager );
//css sprites
fis . util . pipe ( ' spriter ' , cb , opt . spriter );
}
//postpackage
fis . util . pipe ( ' postpackager ' , cb , opt . postpackager );
打包扩展的打包器使用直接通过fis.util.pipe
,设定了回调函数,而编译插件则是通过内部pipe方法来指定回调函数。
ext处理
在各种插件pipe或者是compile流程里的standard方法,都存在着对文件内容字符串进行内容处理的过程。
关于standard有这么一句描述:
standard(标准化处理):前面两项处理会将文件处理为标准的js、css、html语法,fis内核的标准化处理过程对这些语言进行 三种语言能力 扩展处理。这也就意味着,使用less、coffee等语法在fis系统中一样具备 资源定位、内容嵌入,依赖声明 的能力。该过程 不可扩展。
standard对资源定位、内容嵌入、依赖声明的处理,代码篇幅比较略长,就不贴了:传送门» ,standard主要两个步骤,一个是文件内容处理转换,另一个则是对转换内容进行识别然后还原内容。
这里对html\css\js
分别使用extHtml\extCss\extJs
方法进行内容处理:主要是利用正则,给各种约定的格式做转换,转为fis语法糖,便于后面逻辑对依赖和定位的读取,大概格式的转换例子,可以参考扒一扒前端构建工具FIS的内幕
涉及到的正则,可以利用这里解析 来方便理解。
缓存
fis使用了文件缓存以减少重复编译,缓存的位置取决于lib/project.js处的getTempPath.
fis主要缓存分两种,用了两个目录来分开,一个是conf,即配置文件缓存,另一个则是编译文件缓存,后者内容比较多。
缓存内容基本包含两部分 ,一个是.tmp文件,一个是.json文件,后者是缓存文件的信息内容,比如依赖、生成时间戳等等信息。
对于编译文件缓存,则会区分目录来指明是纯release,还是带优化、带域名等情况。
编译文件缓存,在compile.js中做处理 ,当文件命中缓存时,直接使用缓存内容,不经过process处理。
总结
主心骨代码基本走读了一遍,记录了fis源码里我自认为比较核心部分的内容。在代码实现里,我注意到了一点,那就是多处利用了函数参数为对象的情况,函数通过修改实参属性的方式来改变数据,比如,var obj={}; function change(obj){obj.name="hello";}
.这点刚开始没注意,总会在走读流程上不自觉产生疑惑。再次翻看官方文档,发现其实好多我可以从文档入手,这样走读代码会更加方便理解,真是走了不少弯路。后续准备细读一下后面关于打包的处理细节和正则的应用。