引言
最近在寫一個中台項目,使用的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
可能不認
總結
現在看來其實寫這個插件其實並不難,但是在當時很多知識都不了解的情況下去看,確實還是有些許棘手,了解用法和原理比較有挑戰,畢竟不是自己寫的代碼,所以還是要加強代碼方便的閱讀。