Feflow 源码解读

  1. 云栖社区>
  2. 前端那些事儿>
  3. 博客>
  4. 正文

Feflow 源码解读

行者武松 2018-07-17 14:21:41 浏览921
展开阅读全文

Feflow 源码解读

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

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

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

架构简要解析

feflow目录结构如下:

1

入口文件

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

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

内核操作

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

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

以上就是feflow提供的原子性内核操作,简单来说就是初始化(包括激活日志模块,检查运行环境,配置的生成),响应命令和加载插件这三个原子操作。 我们来看看作者是怎么基于内核的生态做相关拓展的,也就是看一下内部的插件中实现了哪些功能,内部插件internal目录如下

1

内部插件

在feflow中,插件体现在拓展命令上,比如internal中的generator插件,在cmd中注册init命令,如下,其中的上下文ctx,在Feflow类中以require('./generator')(this)的形式将自生实例传入,这样就注册了一个命令,调用这个命令需要执行的方法在第四个参数中传入。

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操作将之加载入,其关键是如何把内部的实例共享给外部的插件使用,内部细节在后面的插件机制中详解。

如上就是feflow的架构概要,包括内核提供的操作,init、call和loadPlugin,还有非常重要的内外部插件机制的简单描述。当然不止这些,还有日志模块、更新模块,我们用后面的篇幅详细分析一下这些重要的模块是如何实现的。

命令行交互

按照 feflow github上的使用方式,我们可以得到这些有效命令 初始化项目

  • 初始化 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中的沙箱注入插件依赖。

原文发布时间:2018年06月30日
原文作者:是熊大啊
本文来源掘金,如需转载请紧急联系作者

网友评论

登录后评论
0/500
评论
行者武松
+ 关注
所属云栖号: 前端那些事儿