主頁 > 企業開發 > 寫一個umi插件 自動生成代碼 解放cv的雙手

寫一個umi插件 自動生成代碼 解放cv的雙手

2020-10-09 13:00:52 企業開發

引言

最近在寫一個中臺專案,使用的reactumi框架,
各種增刪改查,基本是串列頁 新建頁 詳情頁這種頁面
為了避免不必要的簡單重復(主要是想偷懶) 于是想去實作自己的一個代碼生成器

簡單探索

首先,在官網上看到了官方寫的一個生成器

<img src="https://img.uj5u.com/2020/10/09/134199091256241.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是一個常規的dvamodel
里面的<%= 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可能不認

總結

現在看來其實寫這個插件其實并不難,但是在當時很多知識都不了解的情況下去看,確實還是有些許棘手,了解用法和原理比較有挑戰,畢竟不是自己寫的代碼,所以還是要加強代碼方便的閱讀,

專案鏈接

戳我查看

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/164487.html

標籤:JavaScript

上一篇:Vue組件應用

下一篇:獲取頁面元素

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • IEEE1588PTP在數字化變電站時鐘同步方面的應用

    IEEE1588ptp在數字化變電站時鐘同步方面的應用 京準電子科技官微——ahjzsz 一、電力系統時間同步基本概況 隨著對IEC 61850標準研究的不斷深入,國內外學者提出基于IEC61850通信標準體系建設數字化變電站的發展思路。數字化變電站與常規變電站的顯著區別在于程序層傳統的電流/電壓互 ......

    uj5u.com 2020-09-10 03:51:52 more
  • HTTP request smuggling CL.TE

    CL.TE 簡介 前端通過Content-Length處理請求,通過反向代理或者負載均衡將請求轉發到后端,后端Transfer-Encoding優先級較高,以TE處理請求造成安全問題。 檢測 發送如下資料包 POST / HTTP/1.1 Host: ac391f7e1e9af821806e890 ......

    uj5u.com 2020-09-10 03:52:11 more
  • 網路滲透資料大全單——漏洞庫篇

    網路滲透資料大全單——漏洞庫篇漏洞庫 NVD ——美國國家漏洞庫 →http://nvd.nist.gov/。 CERT ——美國國家應急回應中心 →https://www.us-cert.gov/ OSVDB ——開源漏洞庫 →http://osvdb.org Bugtraq ——賽門鐵克 →ht ......

    uj5u.com 2020-09-10 03:52:15 more
  • 京準講述NTP時鐘服務器應用及原理

    京準講述NTP時鐘服務器應用及原理京準講述NTP時鐘服務器應用及原理 安徽京準電子科技官微——ahjzsz 北斗授時原理 授時是指接識訓通過某種方式獲得本地時間與北斗標準時間的鐘差,然后調整本地時鐘使時差控制在一定的精度范圍內。 衛星導航系統通常由三部分組成:導航授時衛星、地面檢測校正維護系統和用戶 ......

    uj5u.com 2020-09-10 03:52:25 more
  • 利用北斗衛星系統設計NTP網路時間服務器

    利用北斗衛星系統設計NTP網路時間服務器 利用北斗衛星系統設計NTP網路時間服務器 安徽京準電子科技官微——ahjzsz 概述 NTP網路時間服務器是一款支持NTP和SNTP網路時間同步協議,高精度、大容量、高品質的高科技時鐘產品。 NTP網路時間服務器設備采用冗余架構設計,高精度時鐘直接來源于北斗 ......

    uj5u.com 2020-09-10 03:52:35 more
  • 詳細解讀電力系統各種對時方式

    詳細解讀電力系統各種對時方式 詳細解讀電力系統各種對時方式 安徽京準電子科技官微——ahjzsz,更多資料請添加VX 衛星同步時鐘是我京準公司開發研制的應用衛星授時時技術的標準時間顯示和發送的裝置,該裝置以M國全球定位系統(GLOBAL POSITIONING SYSTEM,縮寫為GPS)或者我國北 ......

    uj5u.com 2020-09-10 03:52:45 more
  • 如何保證外包團隊接入企業內網安全

    不管企業規模的大小,只要企業想省錢,那么企業的某些服務就一定會采用外包的形式,然而看似美好又經濟的策略,其實也有不好的一面。下面我通過安全的角度來聊聊使用外包團的安全隱患問題。 先看看什么服務會使用外包的,最常見的就是話務/客服這種需要大量重復性、無技術性的服務,或者是一些銷售外包、特殊的職能外包等 ......

    uj5u.com 2020-09-10 03:52:57 more
  • PHP漏洞之【整型數字型SQL注入】

    0x01 什么是SQL注入 SQL是一種注入攻擊,通過前端帶入后端資料庫進行惡意的SQL陳述句查詢。 0x02 SQL整型注入原理 SQL注入一般發生在動態網站URL地址里,當然也會發生在其它地發,如登錄框等等也會存在注入,只要是和資料庫打交道的地方都有可能存在。 如這里http://192.168. ......

    uj5u.com 2020-09-10 03:55:40 more
  • [GXYCTF2019]禁止套娃

    git泄露獲取原始碼 使用GET傳參,引數為exp 經過三層過濾執行 第一層過濾偽協議,第二層過濾帶引數的函式,第三層過濾一些函式 preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'] (?R)參考當前正則運算式,相當于匹配函式里的引數 因此傳遞 ......

    uj5u.com 2020-09-10 03:56:07 more
  • 等保2.0實施流程

    流程 結論 ......

    uj5u.com 2020-09-10 03:56:16 more
最新发布
  • 使用Django Rest framework搭建Blog

    在前面的Blog例子中我們使用的是GraphQL, 雖然GraphQL的使用處于上升趨勢,但是Rest API還是使用的更廣泛一些. 所以還是決定回到傳統的rest api framework上來, Django rest framework的官網上給了一個很好用的QuickStart, 我參考Qu ......

    uj5u.com 2023-04-20 08:17:54 more
  • 記錄-new Date() 我忍你很久了!

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 大家平時在開發的時候有沒被new Date()折磨過?就是它的諸多怪異的設定讓你每每用的時候,都可能不小心踩坑。造成程式意外出錯,卻一下子找不到問題出處,那叫一個煩透了…… 下面,我就列舉它的“四宗罪”及應用思考 可惡的四宗罪 1. Sa ......

    uj5u.com 2023-04-20 08:17:47 more
  • 使用Vue.js實作文字跑馬燈效果

    實作文字跑馬燈效果,首先用到 substring()截取 和 setInterval計時器 clearInterval()清除計時器 效果如下: 實作代碼如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta ......

    uj5u.com 2023-04-20 08:12:31 more
  • JavaScript 運算子

    JavaScript 運算子/運算子 在 JavaScript 中,有一些運算子可以使代碼更簡潔、易讀和高效。以下是一些常見的運算子: 1、可選鏈運算子(optional chaining operator) ?.是可選鏈運算子(optional chaining operator)。?. 可選鏈操 ......

    uj5u.com 2023-04-20 08:02:25 more
  • CSS—相對單位rem

    一、概述 rem是一個相對長度單位,它的單位長度取決于根標簽html的字體尺寸。rem即root em的意思,中文翻譯為根em。瀏覽器的文本尺寸一般默認為16px,即默認情況下: 1rem = 16px rem布局原理:根據CSS媒體查詢功能,更改根標簽的字體尺寸,實作rem單位隨螢屏尺寸的變化,如 ......

    uj5u.com 2023-04-20 08:02:21 more
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 08:01:50 more
  • 如何在 vue3 中使用 jsx/tsx?

    我們都知道,通常情況下我們使用 vue 大多都是用的 SFC(Signle File Component)單檔案組件模式,即一個組件就是一個檔案,但其實 Vue 也是支持使用 JSX 來撰寫組件的。這里不討論 SFC 和 JSX 的好壞,這個仁者見仁智者見智。本篇文章旨在帶領大家快速了解和使用 Vu ......

    uj5u.com 2023-04-20 08:01:37 more
  • 【Vue2.x原始碼系列06】計算屬性computed原理

    本章目標:計算屬性是如何實作的?計算屬性快取原理以及洋蔥模型的應用?在初始化Vue實體時,我們會給每個計算屬性都創建一個對應watcher,我們稱之為計算屬性watcher ......

    uj5u.com 2023-04-20 08:01:31 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:01:10 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:00:32 more