zoukankan      html  css  js  c++  java
  • CLI子命令扩展-插件机制实现

    开发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进程。

  • 相关阅读:
    《算法竞赛入门经典》—— 5.2.6 栈、队列与优先队列
    《算法:C语言实现》—— 第二部分 —— 第3章 —— 基本数据结构
    《算法:C语言实现》—— 第二部分 —— 第3章 —— 基本数据结构
    《算法:C语言实现》—— 第二部分 —— 第3章 —— 基本数据结构
    Broken Keyboard (a.k.a. Beiju Text)
    Broken Keyboard (a.k.a. Beiju Text)
    Broken Keyboard (a.k.a. Beiju Text)
    mongodb实战聚合 组内排序
    mongodb实战聚合 组内排序
    MongoDB基础篇:MongoDB Shell命令大全
  • 原文地址:https://www.cnblogs.com/cpselvis/p/7306076.html
Copyright © 2011-2022 走看看