序言
这里假设本文读者对 FIS 已经比较熟悉,如还不了解,可猛击官方文档。
虽然 FIS 整体的源码结构比较清晰,不过讲解起来也是个系统庞大的工程,笔者尽量的挑重点的讲。如果读者有感兴趣的部分笔者没有提到的,或者是存在疑惑的,可以在评论里跑出来,笔者会试着去覆盖这些点。
下笔匆忙,如有错漏请指出。
Getting started
如在开始剖析 FIS 的源码前,有三点内容首先强调下,这也是解构 FIS 内部设计的基础。
1、 FIS 支持三个命令,分别是 fis release
、fis server
、fis install
。当用户输入 fis xx
的时候,内部调用 fis-command-release
、fis-command-server
、fis-command-install
这三个插件来完成任务。同时,FIS 的命令行基于 commander
这个插件构建,熟悉这个插件的同学很容易看懂 FIS 命令行相关部分源码。
2、FIS 以 fis-kernel
为核心。fis-kernel
提供了 FIS 的底层能力,包含了一系列模块,如配置、缓存、文件处理、日志等。FIS 的三个命令,最终调用了这些模块来完成构建的任务。参考 fis-kernel/lib/
目录,下面对每个模块的大致作用做了简单备注,后面的文章再详细展开。
1 2 3 4 5 6 7 8 9 10 |
lib/ ├── cache.js <span class="comment">// 缓存模块,提高编译速度</span> ├── compile.js <span class="comment">// (单)文件编译模块</span> ├── config.js <span class="comment">// 配置模块,fis.config </span> ├── file.js <span class="comment">// 文件处理</span> ├── log.js <span class="comment">// 日志</span> ├── project.js <span class="comment">// 项目相关模块,比如获取、设置项目构建根路径、设置、获取临时路径等</span> ├── release.js <span class="comment">// fis release 的时候调用,依赖 compile.js 完成单文件编译。同时还完成如文件打包等任务。├── uri.js // uri相关</span> └── util.js <span class="comment">// 各种工具函数</span> |
3、FIS 的编译过程,最终可以拆解为细粒度的单文件编译,理解了下面这张图,对于阅读 FIS 的源码有非常大的帮助。(主要是 fis release
这个命令)
一个简单的例子:fis server open
开篇的描述可能比较抽象,下面我们来个实际的例子。通过这个简单的例子,我们可以对 FIS 的整体设计有个大致的印象。
下文以 fis server open
为例,逐步剖析 FIS 的整体设计。其实 FIS 比较精华的部分集中在 fis release
这个命令,不过 fis server
这个命令相对简单,更有助于我们从纷繁的细节中跳出来,窥探 FIS 的整体概貌。
假设我们已经安装了 FIS。好,打开控制台,输入下面命令,其实就是打开 FIS 的 server 目录
1 2 |
fis server open |
从 package.json
可以知道,此时调用了 fis/bin/fis
,里面只有一行有效代码,调用 fis.cli.run()
方法,同时将进程参数传进去。
1 2 3 4 |
<span class="comment">#!/usr/bin/env node</span> <span class="keyword">require</span>(<span class="string">'../fis.js'</span>).cli.run(process.argv); |
接下来看下../fis.js
。代码结构非常清晰。注意,笔者将一些代码给去掉,避免长串的代码影响理解。同时在关键处加了简单的注释
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 |
<span class="comment">// 加载FIS内核</span> <span class="keyword">var</span> fis = module.exports = <span class="keyword">require</span>(<span class="string">'fis-kernel'</span>); <span class="comment">//项目默认配置</span> fis.config.merge({ <span class="comment">// ...</span> }); <span class="comment">//exports cli object</span> <span class="comment">// fis命令行相关的对象</span> fis.cli = {}; <span class="comment">// 工具的名字。在基于fis的二次解决方案中,一般会将名字覆盖</span> fis.cli.name = <span class="string">'fis'</span>; <span class="comment">//colors</span> <span class="comment">// 日志友好的需求</span> fis.cli.colors = <span class="keyword">require</span>(<span class="string">'colors'</span>); <span class="comment">//commander object</span> <span class="comment">// 其实最后就挂载了 commander 这个插件</span> fis.cli.commander = <span class="keyword">null</span>; <span class="comment">//package.json</span> <span class="comment">// 把package.json的信息读进来,后面会用到</span> fis.cli.info = fis.util.readJSON(__dirname + <span class="string">'/package.json'</span>); <span class="comment">//output help info</span> <span class="comment">// 打印帮助信息的API</span> fis.cli.help = <span class="keyword">function</span>(){ <span class="comment">// ...</span> }; <span class="comment">// 需要打印帮助信息的命令,在 fis.cli.help() 中遍历到。 如果有自定义命令,并且同样需要打印帮助信息,可以覆盖这个变量</span> fis.cli.help.commands = [ <span class="string">'release'</span>, <span class="string">'install'</span>, <span class="string">'server'</span> ]; <span class="comment">//output version info</span> <span class="comment">// 打印版本信息</span> fis.cli.version = <span class="keyword">function</span>(){ <span class="comment">// ...</span> }; <span class="comment">// 判断是否传入了某个参数(search)</span> <span class="keyword">function</span> hasArgv(argv, search){ <span class="comment">// ...</span> } <span class="comment">//run cli tools</span> <span class="comment">// 核心方法,构建的入口所在。接下来我们就重点分析下这个方法。假设我们跑的命令是 fis server open</span> <span class="comment">// 实际 process.argv为 [ 'node', '/usr/local/bin/fis', 'server', 'open' ]</span> <span class="comment">// 那么,argv[2] ==> 'server'</span> fis.cli.run = <span class="keyword">function</span>(argv){ <span class="comment">// ...</span> }; |
我们来看下笔者注释过的 fis.cli.run
的源码。
- 如果是
fis -h
或者fis --help
,打印帮助信息 - 如果是
fis -v
或者fis --version
,打印版本信息 - 其他情况:加载相关命令对应的插件,并执行命令,比如
fis-command-server
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 |
<span class="comment">//run cli tools</span> fis.cli.run = <span class="keyword">function</span>(argv){ fis.processCWD = process.cwd(); <span class="comment">// 当前构建的路径</span> <span class="keyword">if</span>(hasArgv(argv, <span class="string">'--no-color'</span>)){ <span class="comment">// 打印的命令行是否单色</span> fis.cli.colors.mode = <span class="string">'none'</span>; } <span class="keyword">var</span> first = argv[<span class="number">2</span>]; <span class="keyword">if</span>(argv.length < <span class="number">3</span> || first === <span class="string">'-h'</span> || first === <span class="string">'--help'</span>){ fis.cli.help(); <span class="comment">// 打印帮助信息</span> } <span class="keyword">else</span> <span class="keyword">if</span>(first === <span class="string">'-v'</span> || first === <span class="string">'--version'</span>){ fis.cli.version(); <span class="comment">// 打印版本信息</span> } <span class="keyword">else</span> <span class="keyword">if</span>(first[<span class="number">0</span>] === <span class="string">'-'</span>){ fis.cli.help(); <span class="comment">// 打印版本信息</span> } <span class="keyword">else</span> { <span class="comment">//register command</span> <span class="comment">// 加载命令对应的插件,这里特指 fis-command-server</span> <span class="keyword">var</span> commander = fis.cli.commander = <span class="keyword">require</span>(<span class="string">'commander'</span>); <span class="keyword">var</span> cmd = fis.<span class="keyword">require</span>(<span class="string">'command'</span>, argv[<span class="number">2</span>]); cmd.register( commander .command(cmd.name || first) .usage(cmd.usage) .description(cmd.desc) ); commander.parse(argv); <span class="comment">// 执行命令</span> } }; |
通过 fis.cli.run
的源码,我们可以看到,fis-command-xx
插件,都提供了 register
方法,在这个方法内完成命令的初始化。之后,通过 commander.parse(argv)
来执行命令。
整个流程归纳如下:
- 用户输入 FIS 命令,如
fis server open
- 解析命令,根据指令加载对应插件,如
fis-command-server
- 执行命令
fis-command-server 源码
三个命令相关的插件中,fis-command-server
的代码比较简单,这里就通过它来大致介绍下。
根据惯例,同样是抽取一个超级精简版的 fis-command-server
,这不影响我们对源码的理解
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">var</span> server = <span class="keyword">require</span>(<span class="string">'./lib/server.js'</span>); <span class="comment">// 依赖的基础库</span> <span class="comment">// 命令的配置属性,打印帮助信息的时候会用到</span> exports.name = <span class="string">'server'</span>; exports.usage = <span class="string">'<command> [options]'</span>; exports.desc = <span class="string">'launch a php-cgi server'</span>; <span class="comment">// 对外暴露的 register 方法,参数的参数为 fis.cli.command </span> exports.register = <span class="keyword">function</span>(commander) { <span class="comment">// 略过若干个函数</span> <span class="comment">// 命令的可选参数,格式参考 commander 插件的文档说明</span> commander .option(<span class="string">'-p, --port <int>'</span>, <span class="string">'server listen port'</span>, parseInt, process.env.FIS_SERVER_PORT || <span class="number">8080</span>) .action(<span class="keyword">function</span>(){ <span class="comment">// 当 command.parse(..)被调用时,就会进入这个回调方法。在这里根据fis server 的子命令执行具体的操作</span> <span class="comment">// ...</span> }); <span class="comment">// 注册子命令 fis server open</span> <span class="comment">// 同理,可以注册 fis server start 等子命令</span> commander .command(<span class="string">'open'</span>) .description(<span class="string">'open document root directory'</span>); }; |
好了,fis server open
就大致剖析到这里。只要熟悉 commander
这个插件,相信不难看懂上面的代码,这里就不多做展开了,有空也写篇科普文讲下 commander
的使用。
写在后面
如序言所说,欢迎交流探讨。如有错漏,请指出。
文章: casperchen