CLI子命令擴展-插件機制實現
開發CLI工具過程中,為了便於擴展,將CLI的實現分為基礎功能和擴展功能。基礎功能包括init、build、lint、publish等伴隨工程從初始化到最終發布到生產環境,也即為CLI 的core。擴展功能包括規範檢測、代碼生成、圖片上傳等和多個平台集成的開發配套服務設施。本篇文章將會敘述如何優雅的實現插件機制,通過插件擴展子命令和開放CLI的生態。
CLI初始化流程運行某個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進程。
![](https://pic.pimg.tw/zzuyanan/1488615166-1259157397.png)
![](https://pic.pimg.tw/zzuyanan/1482887990-2595557020.jpg)
※JDBC連接資料庫的基本步驟
※nginx的坑:帶埠號的自動跳轉
※純JS實現像素逐漸顯示
※Restful介面調用方法超詳細總結
TAG:達人科技 |
※簡介事務ACID的實現機制
※重構-設計API的擴展機制
※CELL:機器學習揭示抗生素作用機制
※Spring AOP 的實現機制
※最新的Turla後門利用電子郵件PDF附件作為C&C機制
※讓CAR-T療法邁向實體瘤,科學家發現關鍵機制|PNAS
※生物物理所揭示III型CRISPR-Cas系統免疫機制
※PHP 運行機制與原理
※Oculus VR移動機制開源工具VRTP開始支持虛幻引擎
※TCP內部機制揭秘
※ENSO非對稱性動力學機制研究取得進展
※HPV病毒致癌分子機制研究取得進展
※可擴展准入機制進入 Beta 階段
※基於 CGLIB 庫的動態代理機制
※CELL METAB:乳腺癌遠端轉移和複發機制
※JCI丨齊曉朋/陳策實合作組揭示泛素連接酶HECTD3調控病原菌感染誘導IFN-I產生機制
※讓CAR-T療法邁向實體瘤,科學家發現關鍵機制
※深入聊一聊 Spring AOP 實現機制!
※科研人員揭示III型CRISPR-Cas系統免疫機制
※技術詳解DAG區塊鏈項目SPECTRE:圍繞一致性建設,投票機制甄別攻擊杜絕交易衝突