Feflow 源码解读

Feflow 源码解读

Feflow(Front-end flow)是腾讯IVWEB团队的前端工程化解决方案,致力于改善多类型项目的开发流程中的规范和非业务相关的问题,可让开发者将绝大部分精力集中在业务开发上,从而提升研发效率。它能够自动化地完成项目建立,开发,构建和规范检查到最终项目上线,而且更加标准化。前端

本文主要如下面几个角度进行分析node

  • 架构简要解析
  • 命令行交互(CLI)
  • 插件机制
  • 更新能力

架构简要解析

feflow目录结构以下:git

入口文件

简单分析一波,package.json中bin字段指向./bin/feflow,这个文件直接require("./lib/feflow"), 那么入口就在这个文件里,在这个文件里,主要作了这几件事:github

  • 接收参数
  • 判断运行环境
  • 调用feflow.init()
  • 执行命令对应的操做

内核操做

feflow对象是由从core中导出的Feflow对象new出来的new Feflow(args),咱们再看core中的index.js文件,其中声明了Feflow这个类,定义了包括init这些类方法,Feflow作了如下几个事情:web

  • 初始化各类须要的路径、日志系统以及拿到相关的用户和本地配置文件
  • 提供init方法,加载内部、外部插件,初始化feflow须要的环境,更新策略。
  • 提供call方法,调用参数对应的方法,这里使用了参数混淆机制,支持模糊匹配参数。
  • 提供loadPlugin方法,注册插件,巧妙运用了node的vm 沙箱机制。

以上就是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路径下就能够看见全部的全局软链,好比说在个人目录下找到了这两个文件

  • cnpm
  • cnpm.cmd

他们的做用都是去调用对应的执行文件,可是为何会有两个呢?从文件内容和后缀名能够看出一个是shell脚本,一个是cmd脚本,他们存在的意义是在不一样的console环境去作相同的事,shell脚本能够在git-bash、commder之类的console里去使用,cmd脚本容许从window的CMD去使用全局命令。

到这里node的自定义命令的实现方式也就说得差很少了,咱们回到feflow中容许咱们使用的参数,initdevlint buildinstall,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)
复制代码

总结

一些思考

  • 对于一些工具类库能够考虑收编在一块儿发布一个feflow-util的npm包,这样作的好处是当某些类库发生一些重大的更变时咱们能够只须要在其中进行修改,对于引用这个类库包的文件就能够不用修改,达到一个类库解耦的目的。
  • feflow其实在致力于提升开发效率上已经作得很好了,多样化的脚手架,规范开发风格,eslint等这些几乎涵盖开发的方方面面,可是有一点却没有涉及到,测试环境,这里的测试环境只是针对后台接口来讲,咱们能够将之拓展出mock模块,生成对应的mock数据,和脚手架脱离,这个想法有几个关键点须要实现
    • 根据什么规则来生成mock数据
    • mock数据如何部署可用

亮点

  • feflow其本质是为了提升开发效率,规范开发流程的解决方案,在这一点上确实是作到了,而且得益于优良的架构,其拓展性,和体系都为完善,总体代码可读性很是高。
  • 源码中回调和异步问题用promise解决,不能解决的就用bluebird加强一下promise解决,能够说做者的一手promise用得炉火纯青,很是值得学习。
  • 同时也运用了不少巧妙的设计,好比插件机制中的插件加载的方法,巧妙运用node中的沙箱注入插件依赖。
相关文章
相关标签/搜索