写一个umi插件 自动生成代码 解放cv的双手
引言
最近在写一个中台项目,使用的react的umi框架。
各种增删改查。基本是列表页 新建页 详情页这种页面
为了避免不必要的简单重复(主要是想偷懒) 于是想去实现自己的一个代码生成器
简单探索
首先,在官网上看到了官方写的一个生成器
<img src="https://images.cnblogs.com/cnblogs_com/amigod/1602334/o_探索umi-官网.png"/ alt="官网图片">
再去源码里扒一扒 找到关键所在

简而言之,就是利用插件的api注册了一个生成model的指令,生成器指向目录里的model.js
代码如下
import { join } from 'path';
import assert from 'assert';
export default api => {
  const { paths, config } = api;
  const absTemplatePath = join(__dirname, '../template/generators');
  return class Generator extends api.Generator {
    writing() {
       ...
       // 判断目录名是models还是model
      const models = config.singular ? 'model' : 'models';
      const name = this.args[0].toString();
      ...
     // 将模板目录下里的model代码 拷贝到项目的model目录下 并命名为指令输入的文件名
      this.fs.copyTpl(
        join(absTemplatePath, 'model.js'),
        join(paths.absSrcPath, models, `${name}.js`),
        {
          name,
        },
      );
    }
  };
};
../template/generators/model.js
export default {
  state: '<%= name %>',
  subscriptions: {
    setup({ dispatch, history }) {
    },
  },
  reducers: {
    update(state) {
      return `${state}_<%= name %>`;
    },
  },
  effects: {
    *fetch({ type, payload }, { put, call, select }) {
    },
  },
}
model是一个常规的dva的model
里面的<%= name %>是ejs语法,对应着copyTpl方法的第三个参数中的name
模板js里的这个占位会被参数name替换
因为我们项目中习惯将model写到模块文件夹下,而且model里的代码有些我们的自己的书写
所以需要自定义一个生成方法了。
继续深入
虽然实现 但是还是带着一些疑问
- generator是基于第三方的生成器还是umi自带
- 如何注册到umi中去
- fs 又是用的是什么插件 如何运作的
generator
稍微翻了一下代码 发现了generator的真面目yeoman-generator
这玩意是一个脚手架生成器 用于生成的一些流程执行
run(cb) {
    const promise = new Promise((resolve, reject) => {
      const self = this;
      this._running = true;
      this.emit('run');
      const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
      const validMethods = methods.filter(methodIsValid);
      assert(
        validMethods.length,
        'This Generator is empty. Add at least one method for it to run.'
      );
      this.env.runLoop.once('end', () => {
        this.emit('end');
        resolve();
      });
      // Ensure a prototype method is a candidate run by default
      function methodIsValid(name) {
        return name.charAt(0) !== '_' && name !== 'constructor';
      }
      function addMethod(method, methodName, queueName) {
        queueName = queueName || 'default';
        debug(`Queueing ${methodName} in ${queueName}`);
        self.env.runLoop.add(queueName, completed => {
          debug(`Running ${methodName}`);
          self.emit(`method:${methodName}`);
          runAsync(function() {
            self.async = () => this.async();
            return method.apply(self, self.args);
          })()
            .then(completed)
            .catch(err => {
              debug(`An error occured while running ${methodName}`, err);
              // Ensure we emit the error event outside the promise context so it won't be
              // swallowed when there's no listeners.
              setImmediate(() => {
                self.emit('error', err);
                reject(err);
              });
            });
        });
      }
      function addInQueue(name) {
        const item = Object.getPrototypeOf(self)[name];
        const queueName = self.env.runLoop.queueNames.indexOf(name) === -1 ? null : name;
        // Name points to a function; run it!
        if (typeof item === 'function') {
          return addMethod(item, name, queueName);
        }
        // Not a queue hash; stop
        if (!queueName) {
          return;
        }
        // Run each queue items
        _.each(item, (method, methodName) => {
          if (!_.isFunction(method) || !methodIsValid(methodName)) {
            return;
          }
          addMethod(method, methodName, queueName);
        });
      }
      validMethods.forEach(addInQueue);
      const writeFiles = () => {
        this.env.runLoop.add('conflicts', this._writeFiles.bind(this), {
          once: 'write memory fs to disk'
        });
      };
      this.env.sharedFs.on('change', writeFiles);
      writeFiles();
      // Add the default conflicts handling
      this.env.runLoop.add('conflicts', done => {
        this.conflicter.resolve(err => {
          if (err) {
            this.emit('error', err);
          }
          done();
        });
      });
      _.invokeMap(this._composedWith, 'run');
    });
    // Maintain backward compatibility with the callback function
    if (_.isFunction(cb)) {
      promise.then(cb, cb);
    }
    return promise;
  }
这里用了Promise来进行流程控制
umi如何整合的
在umi-build-dev库下的 PluginAPI里有这样一段代码
import BasicGenerator from './BasicGenerator';
export default class PluginAPI {
  constructor(id, service) {
 .....................
    this.Generator = BasicGenerator;
  }
  registerGenerator(name, opts) {
    const { generators } = this.service;
    assert(typeof name === 'string', `name should be supplied with a string, but got ${name}`);
    assert(opts && opts.Generator, `opts.Generator should be supplied`);
    assert(!(name in generators), `Generator ${name} exists, please select another one.`);
    generators[name] = opts;
  }
..............
就是我们注册用的方法,这边一方便将BasicGenerator在实例化的时候 挂到Generator属性上
另一方吧提供了registerGenerator方法 也就是我们之前调用的,进行注册
BasicGenerator //js
import Generator from 'yeoman-generator';
const { existsSync } = require('fs');
const { join } = require('path');
class BasicGenerator extends Generator {
  constructor(args, opts) {
    super(args, opts);
    this.isTypeScript = existsSync(join(opts.env.cwd, 'tsconfig.json'));
  }
}
export default BasicGenerator;
// Service.js
export default class Service {
  constructor({ cwd }) {
    //  用户传入的 cmd 不可信任 转化一下
    this.cwd = cwd || process.cwd();
    try {
    ....
    this.generators = {};
    ....
发现generator只是一个接收数据的对象
这里顺便一提,umi插件中经常用到的api其实就是在service中用proxy属性代理了一下pluginAPI生成的
在初始化插件件方法 initPlugin中
this是service对象
  const api = new Proxy(new PluginAPI(id, this), {
        get: (target, prop) => {
          if (this.pluginMethods[prop]) {
            return this.pluginMethods[prop];
          }
          if (
            [
              // methods
              'changePluginOption',
              'applyPlugins',
              '_applyPluginsAsync',
              'writeTmpFile',
              'getRoutes',
              'getRouteComponents',
              // properties
              'cwd',
              'config',
              'webpackConfig',
              'pkg',
              'paths',
              'routes',
              // error handler
              'UmiError',
              'printUmiError',
              // dev methods
              'restart',
              'printError',
              'printWarn',
              'refreshBrowser',
              'rebuildTmpFiles',
              'rebuildHTML',
            ].includes(prop)
          ) {
            if (typeof this[prop] === 'function') {
              return this[prop].bind(this);
            } else {
              return this[prop];
            }
          } else {
            return target[prop];
          }
        },
      });
大概意思就是对PluginAPI实例化后的属性进行get的代理 优先使用pluginMethods里注册的方法 其次是如果是数组总的方法,优先在service里找 最后才到PluignAPI
指令注册和方法实现
代码入口:umi-build-dev/src/plugin/commnds 下的generate文件夹下
export default function(api) {
  const {
    service: { generators },
    log,
  } = api;
  function generate(args = {}) {
    try {
      const name = args._[0];
      assert(name, `run ${chalk.cyan.underline('umi help generate')} to checkout the usage`);
      assert(generators[name], `Generator ${chalk.cyan.underline(name)} not found`);
      const { Generator, resolved } = generators[name];
      const generator = new Generator(args._.slice(1), {
        ...args,
        env: {
          cwd: api.cwd,
        },
        resolved: resolved || __dirname,
      });
      return generator
        .run()
        .then(() => {
          log.success('');
        })
        .catch(e => {
          log.error(e);
        });
    } catch (e) {
      log.error(`Generate failed, ${e.message}`);
      console.log(e);
    }
  }
  function registerCommand(command, description) {
    const details = `
Examples:
  ${chalk.gray('# generate page users')}
  umi generate page users
  ${chalk.gray('# g is the alias for generate')}
  umi g page index
  ${chalk.gray('# generate page with less file')}
  umi g page index --less
  `.trim();
    api.registerCommand(
      command,
      {
        description,
        usage: `umi ${command} type name [options]`,
        details,
      },
      generate,
    );
  }
  registerCommand('g', 'generate code snippets quickly (alias for generate)');
  registerCommand('generate', 'generate code snippets quickly');
关于fs
// yeoman-generator
const FileEditor = require('mem-fs-editor');
class Generator extends EventEmitter {
  constructor(args, options) {
    super();
    ..........
    this.fs = FileEditor.create(this.env.sharedFs);
  }
// mem-fs-editor
'use strict';
function EditionInterface(store) {
  this.store = store;
}
EditionInterface.prototype.read = require('./actions/read.js');
EditionInterface.prototype.readJSON = require('./actions/read-json.js');
EditionInterface.prototype.exists = require('./actions/exists');
EditionInterface.prototype.write = require('./actions/write.js');
EditionInterface.prototype.writeJSON = require('./actions/write-json.js');
EditionInterface.prototype.extendJSON = require('./actions/extend-json.js');
EditionInterface.prototype.append = require('./actions/append.js');
EditionInterface.prototype.delete = require('./actions/delete.js');
EditionInterface.prototype.copy = require('./actions/copy.js').copy;
EditionInterface.prototype._copySingle = require('./actions/copy.js')._copySingle;
EditionInterface.prototype.copyTpl = require('./actions/copy-tpl.js');
EditionInterface.prototype.move = require('./actions/move.js');
EditionInterface.prototype.commit = require('./actions/commit.js');
exports.create = function (store) {
  return new EditionInterface(store);
};
我们用到的copyTpl方法
'use strict';
var extend = require('deep-extend');
var ejs = require('ejs');
var isBinaryFileSync = require('isbinaryfile').isBinaryFileSync;
function render(contents, filename, context, tplSettings) {
  let result;
  const contentsBuffer = Buffer.from(contents, 'binary');
  if (isBinaryFileSync(contentsBuffer, contentsBuffer.length)) {
    result = contentsBuffer;
  } else {
    result = ejs.render(
      contents.toString(),
      context,
      // Setting filename by default allow including partials.
      extend({filename: filename}, tplSettings)
    );
  }
  return result;
}
module.exports = function (from, to, context, tplSettings, options) {
  context = context || {};
  tplSettings = tplSettings || {};
  this.copy(
    from,
    to,
    extend(options || {}, {
      process: function (contents, filename) {
        return render(contents, filename, context, tplSettings);
      }
    }),
    context,
    tplSettings
  );
};
上手
以下是我写的一个生成规则
import { join } from 'path';
const fs=require('fs');
export default api => {
  const  {paths} = api;
  const configPath=join(paths.absSrcPath,'generatorConfig.js');
  const absTemplatePath = join(__dirname, '../template/generators');
  return class Generator extends api.Generator {
    writing() {
      const name = this.args[0].toString();
      // assert(!name.includes('/'), `model name should not contains /, bug got ${name}`);
      const type =this.args[1]&& this.args[1].toString();
     // type即为命令后跟的参数
      switch (type) {
        case 'list':
          if(!fs.existsSync(configPath)) {
            api.log.error('新建列表模板缺少generatorConfig.js')
            return
          }
          const genConfig=require(configPath);
          this.fs.copyTpl(join(absTemplatePath, 'list.js'),join(paths.absSrcPath, `pages/${name}/${type}`, `index.js`), {
            name,
            queryFormItems:genConfig[name]['queryFormItems'],
            columns:genConfig[name]['columns']
          });
      }
      this.fs.copyTpl(join(absTemplatePath, 'model.js'), join(paths.absSrcPath, `pages/${name}`, `model.js`), {
        name
      });
      this.fs.copyTpl(join(absTemplatePath, 'index.less'), join(paths.absSrcPath, `pages/${name}`, `index.less`), {
        name
      });
      this.fs.copyTpl(join(absTemplatePath, 'service.js'), join(paths.absSrcPath, `pages/${name}`, `service.js`), {
        name
      });
    }
  };
};
添加了如下功能
- 结合项目中的目录结构约定进行目录生成(比如我们约定用service来进行接口方法管理)
- 增加在命令后面加不同参数 生成不同的特征模块(比如列表 详情)
- 增加了配置项 可以在node环境下去读取配置 再生成到代码里去(比如 antd的列表的columns)
再仿照umi-dva-plugin的流程进行命令注册和插件导出
import { join } from 'path';
export default(api, opts = {})=> {
  api.registerGenerator('dva:newPage', {
    Generator: require('./model').default(api),
    resolved: join(__dirname, './model'),
  });
}
遇到问题
在探索和上手遇到挺多问题,总结如下
1.阅读源码 加以甄别 ,因为umi-dva-plugin的代码贼多,模板功能只是其中的非核心功能,所以也是看了好几遍 发现这个功能其实和其他代码并不存在耦合 可以单独提出来
2.探索模板语法 一开始不知道是ejs 找了下copyTpl方法

然后就恍然大悟,怪不得看起来那么熟悉,顺便学了一下ejs模板<%= %>和<%- %>的区别
3.兼容性问题 遇到的一个贼奇怪的问题 node环境兼容的问题
一开始不知道 用babel转成es5了  一直报错class constructor Generator cannot be invoked without 'new
看上去就是个兼容问题 然后用web版的babel转换器 关闭preset es2015 调整node版本到6.4主要是把对象的解构赋值要转换掉 不然依赖的三方Generator可能不认
总结
现在看来其实写这个插件其实并不难,但是在当时很多知识都不了解的情况下去看,确实还是有些许棘手,了解用法和原理比较有挑战,毕竟不是自己写的代码,所以还是要加强代码方便的阅读。
项目链接
写一个umi插件 自动生成代码 解放cv的双手的更多相关文章
- Eclipse 使用mybatis generator插件自动生成代码
		Eclipse 使用mybatis generator插件自动生成代码 标签: mybatis 2016-12-07 15:10 5247人阅读 评论(0) 收藏 举报 .embody{ paddin ... 
- mybatis generator maven插件自动生成代码
		如果你正为无聊Dao代码的编写感到苦恼,如果你正为怕一个单词拼错导致Dao操作失败而感到苦恼,那么就可以考虑一些Mybatis generator这个差价,它会帮我们自动生成代码,类似于Hiberna ... 
- 使用mybatis插件自动生成代码以及问题处理
		1.pom.xml中加入依赖插件 <!-- mybatis generator 自动生成代码插件 --> <plugin> <groupId>org.mybatis ... 
- 通过eclipse mybatis generater代码生成插件自动生成代码
		Mybatis属于半自动ORM,在使用这个框架中,工作量最大的就是书写Mapping的映射文件,由于手动书写很容易出错,我们可以利用Mybatis-Generator来帮我们自动生成文件.通过在Ecl ... 
- IDEA结合mybatis插件自动生成代码
		pom文件 添加插件 <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>myb ... 
- 关于使用mybatis插件自动生成代码
		1.安装 mybatis 插件: 在 eclipse 中 点击 help-->Install New Software...--> Add --> local 选择插件中eclip ... 
- 使用mybatis-generator插件自动生成代码的步骤
		注意:首先你这个项目一定要是个maven项目 1.首先你需要在pom文件中导入相关的依赖,如下代码 <plugin> <groupId>org.mybatis.generato ... 
- 写一个TT模板自动生成spring.net下面的配置文件。
		这个是目标. 然后想着就怎么开始 1. 
- mybatis-generator自动生成代码插件
		mybatis自动生成代码(实体类.Dao接口等)是很成熟的了,就是使用mybatis-generator插件. 它是一个开源的插件,使用maven构建最好,可以很方便的执行 插件官方简介: http ... 
随机推荐
- linux日志查找方法
			grep "xxxx" *201812* | grep "xxx" | awk -F, '{if(substr($1,1,10)=="2018-12- ... 
- jquery倒计时代码
			jquery倒计时代码<pre> <span id="day_show">0天</span> <strong id="hour_ ... 
- PHP获取图片每个像素点
			PHP获取图片每个像素点<pre> $i = imagecreatefromjpeg("test.jpg"); //图片路径 for ($x = 0; $x < ... 
- PHP 导出微信公众号粉丝的方法
			PHP 导出微信公众号粉丝的方法 先 user/get 获取关注者列表 然后user/info 根据openid读取信息 以上方法认证的订阅号支持 
- 构建大型 Vue.js 项目的10条建议
			下面是我在开发大型 Vue 项目时的最佳实践.这些技巧将帮助你开发更高效.更易于维护和共享的代码. 今年做自由职业的时候,我有机会开发了一些大型 Vue 应用程序.我所说的这些项目,Vuex stor ... 
- 重写(OverRide)/重载(Overload)
			方法的重写规则 参数列表必须完全与被重写方法的相同: 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同): ... 
- C# 操作本地用户和组(基本全功能)
			今天学习了下怎么用.Net操作本地用户和组,因为目前网上还没看到一篇比较完整的文章,所以整理了下也分享出来,最后附带参考文档,方便深究的童鞋继续学习.========== 原创作品 作者:Yo ... 
- pat 1092 To Buy or Not to Buy(20 分)
			1092 To Buy or Not to Buy(20 分) Eva would like to make a string of beads with her favorite colors so ... 
- lqb 基础练习 杨辉三角形
			基础练习 杨辉三角形 时间限制:1.0s 内存限制:256.0MB 问题描述 杨辉三角形又称Pascal三角形,它的第i+1行是(a+b)i的展开式的系数. 它的一个重要性质是:三角形中的 ... 
- 【Java】面向对象之封装
			面向对象编程是对客观世界的模拟,客观世界里成员变量都是隐藏在对象内部的,外界无法直接操作和修改.封装可以被认为是一个保护屏障,防止该类的代码和数据被其他类随意访问.要访问该类的数据,必须通过指定的方式 ... 
