當前位置:
首頁 > 知識 > CLI子命令擴展-插件機制實現

CLI子命令擴展-插件機制實現

開發CLI工具過程中,為了便於擴展,將CLI的實現分為基礎功能和擴展功能。基礎功能包括init、build、lint、publish等伴隨工程從初始化到最終發布到生產環境,也即為CLI 的core。擴展功能包括規範檢測、代碼生成、圖片上傳等和多個平台集成的開發配套服務設施。本篇文章將會敘述如何優雅的實現插件機制,通過插件擴展子命令和開放CLI的生態。

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進程。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 達人科技 的精彩文章:

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:圍繞一致性建設,投票機制甄別攻擊杜絕交易衝突