本文作者:ivweb 程柳锋
开发CLI工具过程中,为了便于扩展,将CLI的实现分为基础功能和扩展功能。基础功能包括init、build、lint、publish等伴随工程从初始化到最终发布到生产环境,也即为CLI 的core。扩展功能包括规范检测、代码生成、图片上传等和多个平台集成的开发配套服务设施。本篇文章将会叙述如何优雅的实现插件机制,通过插件扩展子命令和开放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) {
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);
}
插件代码执行过程中,需要获取某个命令是否有注册过,及注册新的子命令及子命令的处理方法。
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进程。
原文出处:IVWEB社区 未经同意,禁止转载
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有