开发CLI工具过程中,为了便于扩展,将CLI的实现分为基础功能和扩展功能。基础功能包括init、build、lint、publish等伴随工程从初始化到最终发布到生产环境,也即为CLI 的core。扩展功能包括规范检测、代码生成、图片上传等和多个平台集成的开发配套服务设施。本篇文章将会叙述如何优雅的实现插件机制,通过插件扩展子命令和开放CLI的生态。

CLI初始化流程

运行某个CLI命令时,CLI的初始化加载如下图:

第一步,判断当前用户信息,获取用户的rtx名字。然后读取cli的配置信息,包括cli的根目录,插件目录路径。之后加载内部核心插件和安装的外部插件,最后整个初始化过程完成。

外部插件加载

先读取cli根目录(一般设在user目录下,比如.feflow)下的package.json里的dependencies和devDependencies内容,过滤掉不是以feflow-plugin开头的npm包。然后通过module.require的方式加载各个插件。

...
init() {
return this.loadModuleList(ctx).map(function(name) {
const pluginDir = ctx.plugin_dir;
const path = require.resolve(pathFn.join(pluginDir, name)); // Load plugins
return ctx.loadPlugin(path).then(function() {
ctx.log.debug('Plugin loaded: %s', chalk.magenta(name));
}).catch(function(err) {
ctx.log.error({err: err}, 'Plugin load failed: %s', chalk.magenta(name));
});
});
} /**
* Read external plugins.
*/
loadModuleList(ctx) {
const packagePath = pathFn.join(ctx.base_dir, 'package.json');
const pluginDir = ctx.plugin_dir; // Make sure package.json exists
return fs.exists(packagePath).then(function(exist) {
if (!exist) return []; // Read package.json and find dependencies
return fs.readFile(packagePath).then(function(content) {
const json = JSON.parse(content);
const deps = json.dependencies || json.devDependencies || {}; return Object.keys(deps);
});
}).filter(function(name) {
// Ignore plugins whose name is not started with "feflow-plugin-"
if (!/^feflow-plugin-|^@[^/]+\/feflow-plugin-/.test(name)) return false; // Make sure the plugin exists
const path = pathFn.join(pluginDir, name);
return fs.exists(path);
});
}

外部插件执行

外部插件包从本地的plugin目录读取之后,接下来就需要执行插件代码了。那么插件包里如何获取cli的上下文环境呢?

这里有一个非常巧妙的设计,需要使用node提供的module和vm模块,这样通过cli require的文件,都可以通过feflow变量(注入到插件里的全局变量)访问到cli的实例,从而能够访问cli上的各种属性,比如config, log和一些helper等。

const vm = require('vm');
const Module = require('module'); ...
loadPlugin(path, callback) {
const self = this; return fs.readFile(path).then(function(script) {
// Based on: https://github.com/joyent/node/blob/v0.10.33/src/node.js#L516
var module = new Module(path);
module.filename = path;
module.paths = Module._nodeModulePaths(path); function require(path) {
return module.require(path);
} require.resolve = function(request) {
return Module._resolveFilename(request, module);
}; require.main = process.mainModule;
require.extensions = Module._extensions;
require.cache = Module._cache; script = '(function(exports, require, module, __filename, __dirname, feflow){' +
script + '});'; var fn = vm.runInThisContext(script, path); return fn(module.exports, require, module, path, pathFn.dirname(path), self);
}).asCallback(callback);
}

插件的runtime

插件代码执行过程中,需要获取某个命令是否有注册过,及注册新的子命令及子命令的处理方法。

class Plugin {

    constructor() {
this.store = {};
this.alias = {};
} get(name) {
name = name.toLowerCase();
return this.store[this.alias[name]];
} list() {
return this.store;
} register(name, desc, options, fn) {
if (!name) throw new TypeError('name is required'); if (!fn) {
if (options) {
if (typeof options === 'function') {
fn = options; if (typeof desc === 'object') { // name, options, fn
options = desc;
desc = '';
} else { // name, desc, fn
options = {};
}
} else {
throw new TypeError('fn must be a function');
}
} else {
// name, fn
if (typeof desc === 'function') {
fn = desc;
options = {};
desc = '';
} else {
throw new TypeError('fn must be a function');
}
}
} if (fn.length > 1) {
fn = Promise.promisify(fn);
} else {
fn = Promise.method(fn);
} const c = this.store[name.toLowerCase()] = fn;
c.options = options;
c.desc = desc; this.alias = abbrev(Object.keys(this.store));
}
}

通过register方法来注册的命令会将子命令及其处理函数存储在上下文的store里面。

比如:


feflow.plugin.register('upload', function() {
// Do upload picture here
});

之后就可以通过运行feflow upload来运行插件扩展的命令了。

$ feflow upload

子命令调用

初始化完成后,用户输入命令都会从上下文的store来查找是否有注册过该命令。

function call = function(name, args, callback) {
if (!callback && typeof args === 'function') {
callback = args;
args = {};
} var self = this; return new Promise(function(resolve, reject) {
var c = self.plugin.get(name); if (c) {
c.call(self, args).then(resolve, reject);
} else {
reject(new Error('Command `' + name + '` has not been registered yet!'));
}
}).asCallback(callback);
};

存在的问题

上述实现方式存在一个问题,每次运行一个命令都需要重现初始化一次。后续考虑编写一个daemon来守护CLI进程。

CLI子命令扩展-插件机制实现的更多相关文章

  1. rust cargo 一些方便的三方cargo 子命令扩展

    内容来自cargo 的github wiki,记录下,方便使用 可选的列表 cargo-audit - Audit Cargo.lock for crates with security vulner ...

  2. Maven生命周期和插件机制

    Maven中的一个非常重要的概念是生命周期和插件,这篇文章重点介绍下Maven的生命周期. Maven的生命周期是抽象的,具体的功能是有具体的插件来完成的,Maven有相当多的功能插件,以至于Mave ...

  3. 100个精选zencart扩展插件

    100个精选zencart扩展插件 特别推荐 1. 数据库备份 2. 产品横向布局. 3. 邮件订阅Newsletter Subscribe. 4. google 翻译google_translate ...

  4. Ganglia监控扩展实现机制

    Ganglia监控扩展实现机制 默认安装完成的Ganglia仅向我们提供基础的系统监控信息,通过Ganglia插件可以实现两种扩展Ganglia监控功能的方法.1.添加带内(in-band)插件,主要 ...

  5. (弃) Keystone CLI_选项与子命令概况

    本文档介绍icehouse发行版keystone命令 keystone Command-Line Interface (CLI)提供用于和keystone服务器交互的方便工具,但是该命令行工具逐渐受到 ...

  6. jupyter notebook设置主题背景,字体和扩展插件

    windows上安装Anaconda (IPython notebook) Anaconda是一个包与环境的管理器,一个Python发行版,以及一个超过1000多个开源包的集合.它是免费和易于安装的, ...

  7. FIS 插件机制

    FIS 插件机制 author: @TiffanysBear 当我们使用 FIS 插件的时候,有没有想过自己也开发一个基于 FIS 的插件,参与 FIS 打包编译的整个流程:那么问题就来了: FIS ...

  8. JMeter扩展插件实现对自定义协议进行支持 转

    本文版权归xmeter.net 所有.欢迎转载,转载请注明出处. 摘要## JMeter本身提供了插件机制,允许第三方扩展JMeter以支持JMeter不支持的协议的测试.本文以扩展一个简单的Apac ...

  9. 使用Hystrix的插件机制,解决在使用线程隔离时,threadlocal的传递问题

    背景 在我们的项目中,比较广泛地使用了ThreadLocal,比如,在filter层,根据token,取到用户信息后,就会放到一个ThreadLocal变量中:在后续的业务处理中,就会直接从当前线程, ...

随机推荐

  1. mysql数据与Hadoop之间导入导出之Sqoop实例

    前面介绍了sqoop1.4.6的 如何将mysql数据导入Hadoop之Sqoop安装,下面就介绍两者间的数据互通的简单使用命令. 显示mysql数据库的信息,一般sqoop安装测试用 sqoop l ...

  2. mysql的my.ini文件详解

    mysql数据库在配置时包含很多信息:端口号,字符编码,指定根路径 basedir,指定数据存放的路径等信息 mysql的字体编码分为两种: 服务器编码 客户端输入的编码 通常服务器的编码都是utf- ...

  3. PILLOW图片中加入中文 曲线救国Opencv

    索引 简述 准备 示例 效果图 结语 简述 我在使用opencv2或3的时候想要在图片上添加中文文字,需要去下载Freetype库,编译好链接到opencv库中才能中文的输出.网上大部分在图片中插入中 ...

  4. React文档翻译 (快速入门)

    翻译自react的大部分文档,方便自己查阅. 目录 生命周期 实例化 存在期 销毁期 state Do Not Modify State Directly State Updates May Be A ...

  5. 最新的极光推送服务器端代码(java服务器后台向手机端自定义推送消息)

    一共两个类 一个Jdpush  一个JpushClientUtil 代码如下   注解都写的很清楚 package com.sm.common.ajpush; import org.slf4j.Log ...

  6. Java WebService学习资料

    最近用到了WebService,以前没用过,想要好好学习一下.感觉网上资料比较少,而且很杂,找了很久,觉得下面的两个文章解释的比较清楚,分享一下: WebService概念.原理:http://mp. ...

  7. Ajax获取数据的几种格式和解析方式

    一.什么是ajax  AJAX的全称是Asynchronous JavaScript and XML(是异步的 javascript 和 XML).  ajax不是新的编程语言,而是一种使用现有标准的 ...

  8. 解决window7 x64位Anaconda启动报错:AttributeError: '_NamespacePath' object has no attribute 'sort'

    最近论文需要用到python做数据分析,python语法简单,但是Windows下安装第三方包恶心的要命,statsmodels用pip死活安装不上,网上查了说包相互依赖windows下的pip不能下 ...

  9. 如何在python脚本里面连续执行adb shell后面的各种命令

    如何在python脚本里面连续执行adb shell后面的各种命令 adb shell "cd /data/local && mkdir tmp" adb shel ...

  10. git合并历史提交

    背景 以前一直觉得只要pull和push就够了,但合作中总会遇到各种非理想的情况.这时候才发现git其他命令的作用. 现在的情况是,repo是一个远程team维护的,我们需要增加新feature,那么 ...