Feflow 源码解读

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

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

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

架构简要解析

feflow目录结构如下:

入口文件

简单分析一波,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目录如下

内部插件

在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日
原文作者:是熊大啊
本文来源掘金,如需转载请紧急联系作者

Feflow 源码解读相关推荐

  1. Bert系列(二)——源码解读之模型主体

    本篇文章主要是解读模型主体代码modeling.py.在阅读这篇文章之前希望读者们对bert的相关理论有一定的了解,尤其是transformer的结构原理,网上的资料很多,本文内容对原理部分就不做过多 ...

  2. Bert系列(三)——源码解读之Pre-train

    https://www.jianshu.com/p/22e462f01d8c pre-train是迁移学习的基础,虽然Google已经发布了各种预训练好的模型,而且因为资源消耗巨大,自己再预训练也不现 ...

  3. linux下free源码,linux命令free源码解读:Procps free.c

    linux命令free源码解读 linux命令free源码解读:Procps free.c 作者:isayme 发布时间:September 26, 2011 分类:Linux 我们讨论的是linux ...

  4. nodeJS之eventproxy源码解读

    1.源码缩影 !(function (name, definition) { var hasDefine = typeof define === 'function', //检查上下文环境是否为AMD ...

  5. PyTorch 源码解读之即时编译篇

    点击上方"AI遇见机器学习",选择"星标"公众号 重磅干货,第一时间送达 作者丨OpenMMLab 来源丨https://zhuanlan.zhihu.com/ ...

  6. Alamofire源码解读系列(九)之响应封装(Response)

    本篇主要带来Alamofire中Response的解读 前言 在每篇文章的前言部分,我都会把我认为的本篇最重要的内容提前讲一下.我更想同大家分享这些顶级框架在设计和编码层次究竟有哪些过人的地方?当然, ...

  7. spring-session源码解读 sesion

    2019独角兽企业重金招聘Python工程师标准>>> spring-session源码解读 sesion 博客分类: java spring 摘要: session通用策略 Ses ...

  8. 前端日报-20160527-underscore 源码解读

    underscore 源码解读 API文档浏览器 JavaScript 中加号操作符细节 抛弃 jQuery,拥抱原生 JS 从 0 开始学习 GitHub 系列之「加入 GitHub」 js实现克隆 ...

  9. php service locator,Yii源码解读-服务定位器(ServiceLocator)

    SL的目的也是解耦,并且非常适合基于服务和组件的应用. Service Locator充当了一个运行时的链接器的角色,可以在运行时动态地修改一个类所要选用的服务, 而不必对类作任何的修改. 一个类可以 ...

最新文章

  1. iOS安全之二次封装AFN并设置请求头/执行HTTPS加强安全
  2. ASP.NET Core 中文文档 第三章 原理(1)应用程序启动
  3. 使用Python内置集合对象和内置函数filter()过滤无效书评
  4. linux下安卓刷机,linux下安卓刷机脚本
  5. 怎么把PDF文件转换成电子书?教你如何转换
  6. 【UEFI实战】UEFI中使用汇编代码
  7. 中国遥感数据查询网址
  8. Laragon 在Windows中快速搭建Laravel本地开发环境
  9. 记2014“蓝桥杯全国软件大赛决赛北京之行
  10. python代码混淆工具,Python版代码混淆工具
  11. MySQL中三种表关系的建立
  12. PTA - 数据库合集16
  13. java 随机字母数字_Java生成字母和数字组成的随机字符串
  14. ScroolView 控制最大高度
  15. nlp-情感分类-研究
  16. Houdini下如何安装Arnold
  17. 微信公众号不限制关键词自动回复次数的解决办法,详细操作方法。
  18. Vue多选框保留勾选数据
  19. 五款苹果M1 Mac,如何选购哪款更适合你?
  20. linux 目录及文件权限 000 444 666 777

热门文章

  1. Windows Store获得Fluent Design加成
  2. python web服务器学习笔记(五) 并发尝试之popen原理探究
  3. X-UA-Compatible也无法解决的IE11兼容问题
  4. 回首2013 展望2014
  5. mac安装jmeter
  6. WPF QuickStart系列之样式和模板(Style and Template)
  7. Java中的多线程你只要看这一篇就够了
  8. 总结Unity 初学者容易犯的编译与运行时错误(第三部分)
  9. 设计模式学习 之 单例模式
  10. linux的权限管理以及特殊权限SUID,SGID,Sticky