Feflow(Front-end flow)是腾讯IVWEB团队的前端工程化解决方案,致力于改善多类型项目的开发流程中的规范和非业务相关的问题,可让开发者将绝大部分精力集中在业务开发上,从而提升研发效率。它能够自动化地完成项目建立,开发,构建和规范检查到最终项目上线,而且更加标准化。前端
本文主要如下面几个角度进行分析node
feflow目录结构以下:git
简单分析一波,package.json中bin字段指向./bin/feflow,这个文件直接require("./lib/feflow")
, 那么入口就在这个文件里,在这个文件里,主要作了这几件事:github
feflow对象是由从core中导出的Feflow对象new出来的new Feflow(args)
,咱们再看core中的index.js文件,其中声明了Feflow这个类,定义了包括init这些类方法,Feflow作了如下几个事情:web
以上就是feflow提供的原子性内核操做,简单来讲就是初始化(包括激活日志模块,检查运行环境,配置的生成),响应命令和加载插件这三个原子操做。 咱们来看看做者是怎么基于内核的生态作相关拓展的,也就是看一下内部的插件中实现了哪些功能,内部插件internal目录以下 shell
在feflow中,插件体如今拓展命令上,好比internal中的generator插件,在cmd中注册init命令,以下,其中的上下文ctx,在Feflow类中以require('./generator')(this)
的形式将自生实例传入,这样就注册了一个命令,调用这个命令须要执行的方法在第四个参数中传入。npm
module.exports = function (ctx) {
const cmd = ctx.cmd;
cmd.register('lint', 'Lint you project use eslint-config-ivweb.', {}, require('./linter'));
};
复制代码
feflow中内部插件就是这样拓展,那外部插件,也就是用户本身去下载的插件怎么集成到feflow中呢,这个过程是这样的,在Feflow.init方法中调用了loadPlugins模块,这模块负责把用户插件目录下的有效配置文件导出,再调用内核中的loadPlugin操做将之加载入,其关键是如何把内部的实例共享给外部的插件使用,内部细节在后面的插件机制中详解。json
如上就是feflow的架构概要,包括内核提供的操做,init、call和loadPlugin,还有很是重要的内外部插件机制的简单描述。固然不止这些,还有日志模块、更新模块,咱们用后面的篇幅详细分析一下这些重要的模块是如何实现的。前端工程化
按照 feflow github上的使用方式,咱们能够获得这些有效命令 初始化项目promise
初始化 feflow init
cd <folder>
本地开发 feflow dev
代码检查 feflow lint
生产环境打包 feflow build
安装 脚手架或插件 feflow install <package>
先从最基本的入手,看一下是如何让系统响应feflow
这个自定义命令的。 咱们找到项目/
目录下package.json
文件,在其中有这个内容
"bin": {
"feflow": "./bin/feflow"
}
复制代码
这个bin目录就是用来指定各个内部命令对应的可执行文件的位置,在这里feflow对应的执行文件就是当前bin目录下的feflow文件,在改目录下运行feflow,npm就会去找对应的执行文件,若是不在当前目录,想要在全局均可执行feflow命令呢,咱们须要在当前目录下执行npm link
,该命令的做用是将bin字段对应的文件建立一个软链将其添加进系统PATH,window下在C:\Users\Administrator\AppData\Roaming\npm
路径下就能够看见全部的全局软链,好比说在个人目录下找到了这两个文件
他们的做用都是去调用对应的执行文件,可是为何会有两个呢?从文件内容和后缀名能够看出一个是shell脚本,一个是cmd脚本,他们存在的意义是在不一样的console环境去作相同的事,shell脚本能够在git-bash、commder之类的console里去使用,cmd脚本容许从window的CMD去使用全局命令。
到这里node的自定义命令的实现方式也就说得差很少了,咱们回到feflow中容许咱们使用的参数,init
、dev
、lint
build
和install
,node接受参数的方法也很容易理解,其中最关键的是node中的process
对象,它提供了当前node进程的相关信息,咱们能够从process.argv中拿到开启当前进程命令行中的参数信息,第一个元素为process.execPath,第二个元素为当前执行的JavaScript文件路径,剩余的元素为其余命令行参数。
咱们来看看feflow中是怎么作的
const args = minimist(process.argv.slice(2));
复制代码
做者使用了minimist这个轻量级的命令行参数解析引擎,为何用minimist不用其余的呢,node.js的命令行参数解析工具备不少,好比:argparse、optimist、yars、commander。可是optimist和yargs内部使用的解析引擎正是minimist,它小巧精悍,简单好用。这里minimist将命令行参数解析成对象,以便后面的操做。
在feflow中会有一些问询操做,做者选用的是inquirer这个库,promise的操做风格更符合做者风格,选用inquirer也就不足为奇了,固然还有一个缘由是后面做者使用的Yeoman的问询操做promting底层也是用的inquirer。
在feflow中还有一个模块涉及到命令行操做,core中的command文件,它提供了命令注册和返回命令方法的功能,相比于EventEmitter
的实例,这里的commander更加智能,若是咱们输入一个错误的命令,好比误输入flo
可是正确的命令是flop
,command模块依然能够准确识别,并以flop
命令执行。
Feflow中的插件分为内外两类,外部插件容许开发者在npm上下载其余feflow的插件搭配使用;内部插件则由做者维护开发,是集成在feflow中的,其都是在core提供的init方法中加载。可是插件处理方法和加载方式不一样。
内部插件调用方法
require('../internal/build')(this);
复制代码
以上插件就提供了dev
,build
命令,调用的过程为加载一个模块,该模块每每是一个类,最早调用的构造函数,将Feflow的实例传入,再以此调用这个模块实例的静态方法。
咱们以generator插件为例,讲解一下feflow如何生成一个可用的脚手架,Generator的使用过程如上所说,调用构造函数传入实例,这里调用的类方法为init,在这里面作的工做为
这里面有两个关键点
增量更新做者这样调用
self.execNpmCommand('install', needUpdatePlugins, false, baseDir)
复制代码
这个方法将开发者对feflow的配置(npm包代理)和命令行参数(是否全局安卓)concat为一个命令行字符串args,并传入spawn,以下代码:
const npm = spawn('npm', args, {cwd: where});
let output = '';
npm.stdout.on('data', (data) => {
output += data;
}).pipe(process.stdout);
npm.stderr.on('data', (data) => {
output += data;
}).pipe(process.stderr);
npm.on('close', (code) => {
if (!code) {
resolve({cod: 0, data: output});
} else {
reject({code: code, data: output});
}
});
复制代码
spawn由cross-spawn
导出,cross-spawn具备原生spawn的功能和类似的调用方法,但又没有原生spawn的各类问题,能够理解为无反作用的spawn。命令交给spawn子进程去执行,输入一个流对象。增量更新的原理为找到两个版本的差分包,也就是补丁,文件校验事后,将补丁安装致本地文件便可。
脚手架做者底层使用的是yeoman,yeoman是一个通用的脚手架搭建工具,其优点在于能够搭建任何语言的脚手架,而且Yeoman自己并不作任何配置,所有都由其内部的generator实现,再借助yeoman-environment
这个工具能够容许开发者部署已经安装好的generator,看做者是如何实现这个逻辑的
run(name) {
const ctx = this.ctx;
const pluginDir = ctx.pluginDir;
let path = pathFn.join(pluginDir, name, 'app/index.js');
if (!fs.existsSync(path)) {
path = pathFn.join(pluginDir, name, 'generators', 'app/index.js');
}
yeomanEnv.register(require.resolve(path), name);
yeomanEnv.run(name, this.args, err => {
});
}
复制代码
这里并无调用yeomanEnv.lookup这方法去寻找用户所安装的全部generator,由于比较坑的一点是lookup即使是寻找到安装的generator后并不会把已安装generator的列表返回,因此得去插件安装目录匹配开发者想要安装的脚手架。幸运的是,yeomanEnv.run方法并不只仅依赖于yeomanEnv.lookup,只要是在yeomanEnv注册过的generator均可以执行。
导入外部插件的一个关键点是如何共享Feflow实例, 这里很巧妙地使用了node的vm(virtual machine)机制解决了这问题, 可直接使用feflow变量来访问执行上下文,其内部就是使用vm来加载外部插件脚本,至关于模板引擎实现原理中的new Function或eval来解析并执行字符串代码。
script = '(function(exports, require, module, __filename, __dirname, feflow){' +
script + '});';
const fn = vm.runInThisContext(script, path);
return fn(module.exports, require, module, path, pathFn.dirname(path), self);
复制代码
把外部插件包装成一个带参函数传入沙箱,编译执行后返回该函数并传入全局变量执行,便可完成对外部插件的加载,能够说很是巧妙了。
feflow在执行命令前都会自检一次是否能够更新,当前版本不知足远程库feflow-cli兼容版本的要求时就会要求更新,而且是强制性的,判断是否要更新是借助于语义化版本控制规范(SemVer),须要更新时则调用execNpmCommand
方法更新。
semver.satisfies(version, compatibleVersion)
复制代码