您的位置:首頁>正文

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

同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示