手把手實(shí)現(xiàn)一個(gè)web代碼模板快速生成CLI工具
前言
在上一篇文章中,我們實(shí)現(xiàn)了一個(gè)web工程通用腳手架工具,目的是快速搭建項(xiàng)目。在實(shí)際開發(fā)中,特別是后臺(tái)管理系統(tǒng),有很多相似的代碼實(shí)現(xiàn)。于是乎,我們可以繼續(xù)實(shí)現(xiàn)一個(gè)快速生成web代碼模板的工具,告別復(fù)制/粘貼。
基本流程
基本思路其實(shí)很簡單,就是通過命令調(diào)取定義好的模板,然后生成代碼文件:

項(xiàng)目結(jié)構(gòu)
xman-tcli
├─?bin
│??└─?xmant.js
├─?command
│??├─?createFile.js
│??└─?createManyFiles.js
├─?config
│??└─?fileList.js
├─?templates
│??├─?index.js
│??├─?js
│??│??├─?reactClassJSX.js
│??│??└─?reactFuncJSX.js
│??└─?ts
│?????├─?reactClassTSX.js
│?????├─?reactFuncTS.js
│?????└─?reactFuncTSX.js
├─?utils
│??└─?index.js
├─?.gitignore
├─?LICENSE
├─?package.json
└─?README.md
具體實(shí)現(xiàn)
很多依賴的用處上一篇文章已經(jīng)提及,所以這篇文章就不會(huì)過多的介紹。
初始化項(xiàng)目
可以用 npm init 進(jìn)行創(chuàng)建,也可以根據(jù)下面列出的 package.json 進(jìn)行修改。
{
??"name":?"xman-tcli",
??"version":?"1.0.0",
??"description":?"web-cli工具,可以快速創(chuàng)建template",
??"bin":?{
????"xmant":?"bin/xmant.js"
??},
??"scripts":?{
????"test":?"echo?\"Error:?no?test?specified\"?&&?exit?1"
??},
??"repository":?{
????"type":?"git",
????"url":?"https://github.com/XmanLin/xman-tcli.git"
??},
??"keywords":?[
????"cli"
??],
??"author":?"xmanlin",
??"license":?"MIT",
??"dependencies":?{
????"chalk":?"^4.1.2",
????"clui":?"^0.3.6",
????"commander":?"^8.2.0",
????"figlet":?"^1.5.2",
????"handlebars":?"^4.7.7",
????"inquirer":?"^8.1.5",
????"update-notifier":?"^5.1.0"
??}
}
編寫bin/xman.js
#!/usr/bin/env?node
const?{?program?}?=?require('commander');
program
?.version(require('../package').version,?'-v,?--version');
?
program.parse(process.argv);?//?這里是必要的
if?(!program.args.length)?{
?program.help();
}
在當(dāng)前xmant-cli目錄下,執(zhí)行 npm link 后,就可以在本地對腳手架工具進(jìn)行調(diào)試了。
然后在當(dāng)前目錄下執(zhí)行:
xmant?-v

說明工程初步搭建成功。
通過命令快速創(chuàng)建單個(gè)代碼模板
編寫模板
代碼模板可以根據(jù)實(shí)際項(xiàng)目進(jìn)行抽離,這里利用幾個(gè)簡單的模板作為例子。
templates/js/reactClassJSX.js
????return?`
import?*?as?React?from?'react';
export?class?${className}?extends?React.Component{
????constructor(props){
????????super(props);
????????this.state?=?{}
????}
????componentDidMount(){
????}
????render()?{
????????return?(
????????????
????????)
????}
}
????`?
}
templates/js/reactFuncJSX.js
module.exports?=?function?(funcName)?{
????return?`
import?React,?{useEffect,?useState}?from?'react';
const?${funcName}?=?(props)?=>?{
????
????return?(
????????
????)
}
export?default?${funcName};
????`?
}
templates/ts/reactClassTSX.js
module.exports?=?function?(className)?{
????return?`
import?*?as?React?from?'react';
interface?Props?{}
interface?State?{}
export?class?${className}?extends?React.Component{
????constructor(props:?Props){
????????super(props);
????????this.state?=?{}
????}
????componentDidMount(){
????}
????render()?{
????????return?(
????????????
????????)
????}
}
????` ?
}
templates/ts/reactFuncTS.js
module.exports?=?function?(funcName)?{
????return?`
export?const?${funcName}?=?()?=>?{
????
}
????`?
}
templates/ts/reactFuncTSX.js
module.exports?=?function?(funcName)?{
????return?`
import?React,?{useEffect,?useState}?from?'react';
const?${funcName}?=?(props:?any)?=>?{
????
????useEffect(()?=>?{
????????
????},[])
????
????return?(
????????
????)
}
export?default?${funcName};
????`?
}
模板定義好之后,通過 index.js 統(tǒng)一導(dǎo)出。
templates/index.js
const?reactClassJSX?=?require('./js/reactClassJSX');
const?reactFuncJSX?=?require('./js/reactFuncJSX');
const?reactClassTSX?=?require('./ts/reactClassTSX');
const?reactFuncTSX?=?require('./ts/reactFuncTSX');
const?reactFuncTS?=?require('./ts/reactFuncTS');
//?命名規(guī)范:name由“-”鏈接,前面為模板名,后面為創(chuàng)建后文件的后綴
module.exports?=?[
????{
????????name:?'reactClass-jsx',?src:?reactClassJSX
????},
????{
????????name:?'reactFunc-jsx',?src:?reactFuncJSX
????},
????{
????????name:?'reactClass-tsx',?src:?reactClassTSX
????},
????{
????????name:?'reactFunc-tsx',?src:?reactFuncTSX
????},
????{
????????name:?'reactFunc-ts',?src:?reactFuncTS
????}
]
這里的“命名規(guī)范”,目的是為了后面創(chuàng)建文件時(shí)得到相應(yīng)的后綴。
創(chuàng)建工具函數(shù) utils/index.js:
module.exports?=?{
????getFileSuffix:?(name)?=>?{
????????if(typeof?name?===?'string')?{
????????????return?name.split('-')[1]
????????}
????}
}
編寫創(chuàng)建文件邏輯
準(zhǔn)備工作就緒,接下來就是文件創(chuàng)建的邏輯 command/createFile.js:
//?創(chuàng)建單個(gè)文件
const?templates?=?require('../templates/index');
const?chalk?=?require('chalk');
const?inquirer?=?require('inquirer');
const?fs?=?require("fs");
const?utils?=?require('../utils/index');
module.exports?=?()?=>?{
????inquirer.prompt([
????????{
????????????name:?'templateName',
????????????type:'list',
????????????message:?'請選擇你想要生成的代碼模板:',
????????????choices:?templates
????????},
????????{
????????????name:?'filename',
????????????type:'input',
????????????message:?'請輸入代碼文件中類名或方法名:',
????????????validate:?function?(value)?{
????????????????if?(value.length)?{
????????????????????return?true;
????????????????}?else?{
????????????????????return?'請輸入代碼文件中類名或方法名';
????????????????}
????????????},
????????}
????])
????.then(answers?=>?{
????????const?templateName?=?answers.templateName;
????????const?filename?=?answers.filename;
????????templates.forEach((item)?=>?{
????????????if(item.name?===?templateName)?{
????????????????const?suffix?=?utils.getFileSuffix(item.name)
????????????????const?file?=?`./index.${suffix}`
????????????????//?檢驗(yàn)當(dāng)前文件夾下是否有同名文件
????????????????fs.access(file,?function(err)?{
????????????????????if(!err)?{
????????????????????????console.log('創(chuàng)建失?。?,?chalk.yellow('文件已存在'))
????????????????????}?else?{
????????????????????????fs.writeFile(file,?item.src(filename),?function(err)?{
????????????????????????????if(err)?{
????????????????????????????????console.log('創(chuàng)建失?。?,?chalk.red(err))
????????????????????????????}?else?{
????????????????????????????????console.log(chalk.green(`創(chuàng)建文件成功!${file}`));
????????????????????????????}
????????????????????????})
????????????????????}
????????????????})
????????????}
????????})
????})
}
這里需要注意的是:如果不在文件創(chuàng)建之前檢查當(dāng)前文件夾下是否有同名文件的話,原有的同名文件將被覆蓋。
編寫命令
最后就是命令的編寫 bin/xman.js:
#!/usr/bin/env?node
const?{?program?}?=?require('commander');
...
program
?.command('create')
?.description("Create?a?file")
?.alias('c')
?.action(()?=>?{
??require('../command/createFile')()
?});
?
...
調(diào)試
在當(dāng)前項(xiàng)目文件夾下執(zhí)行 npm link --force , 然后隨便找個(gè)文件下執(zhí)行 xmant c:

打開我們新創(chuàng)建的文件看看:

也可以選擇其他模板創(chuàng)建試試。
通過命令快速批量創(chuàng)建代碼模板
如果我們想一次性創(chuàng)建大量的代碼模板呢?當(dāng)然還是通過命令批量的創(chuàng)建文件。
這里的思路:通過讀取配置文件,然后進(jìn)行批量創(chuàng)建。
編寫配置文件
//?說明:?
//?folder:?文件夾名,可以嵌套,用?“/”分隔
//?fileName:?文件名
//?funcName:?類名或函數(shù)名
// template:?用到的文件模板
module.exports?=?[
????{
????????folder:?'./home',
????????fileName:?'index',
????????funcName:?'Home',
????????template:?'reactFunc-tsx'
????},
????{
????????folder:?'./home/compnent',
????????fileName:?'index',
????????funcName:?'Compnent',
????????template:?'reactFunc-tsx'
????},
????{
????????folder:?'./home/service',
????????fileName:?'index',
????????funcName:?'service',
????????template:?'reactFunc-ts'
????},
????{
????????folder:?'./news',
????????fileName:?'index',
????????funcName:?'News',
????????template:?'reactFunc-tsx'
????},
????{
????????folder:?'./news/service',
????????fileName:?'index',
????????funcName:?'service',
????????template:?'reactFunc-ts'
????}
]
這里用到的文件模板就是我們之前編寫好的模板。
編寫批量創(chuàng)建文件邏輯
根據(jù)配置文件進(jìn)行文件夾和文件的批量創(chuàng)建 command/createManyFiles.js:
//?批量創(chuàng)建文件
const?chalk?=?require('chalk');
const?inquirer?=?require('inquirer');
const?fs?=?require('fs');
const?path?=?require('path');
const?utils?=?require('../utils/index');
const?fileList?=?require('../config/fileList');
const?templates?=?require('../templates/index');
const?clui?=?require('clui');
const?Spinner?=?clui.Spinner;
const?status?=?new?Spinner('正在創(chuàng)建...');
//?遞歸創(chuàng)建目錄?同步方法
function?mkdirsSync(dirname)?{
????if?(fs.existsSync(dirname))?{
????????return?true;
????}?else?{
????????if?(mkdirsSync(path.dirname(dirname)))?{
????????????fs.mkdirSync(dirname);
????????????console.log(chalk.green(`創(chuàng)建目錄成功-${dirname}`));
????????}
????}???
}
module.exports?=?()?=>?{
????inquirer.prompt([
????????{
????????????name:?'choices',
????????????type:'list',
????????????message:?'請確認(rèn)配置好模板批量生成列表',
????????????choices:?['yes',?'no']
????????}
????])
????.then(answers?=>?{
????????const?choices?=?answers.choices
????????if(choices?===?'yes')?{
????????????//?批量創(chuàng)建目錄
????????????fileList.forEach(item?=>?{
????????????????if(item.folder)?{
????????????????????mkdirsSync(`${item.folder}`)
????????????????}
????????????})
????????????//?批量創(chuàng)建文件
????????????fileList.forEach(item?=>?{
????????????????templates.forEach(tpl?=>?{
????????????????????if(item.template?===?tpl.name)?{
????????????????????????const?suffix?=?utils.getFileSuffix(item.template)
????????????????????????const?fileName?=?`${item.fileName}.${suffix}`
????????????????????????fs.writeFile(`${item.folder}/${fileName}`,?tpl.src(item.funcName),?function(err)?{
????????????????????????????if(err)?{
????????????????????????????????console.log('創(chuàng)建失?。?,?chalk.red(err))
????????????????????????????}?else{
????????????????????????????????console.log(chalk.green(`創(chuàng)建文件成功!${fileName}`));
????????????????????????????}
????????????????????????})
????????????????????}
????????????????})
????????????})
????????}
????})
}
編寫命令
最后編寫 bin/xman.js:
#!/usr/bin/env?node
const?{?program?}?=?require('commander');
...
program
?.command('create-many')
?.description("Create?many?folders?and?files")
?.alias('cm')
?.action(()?=>?{
??require('../command/createManyFiles')()
?});
?
...
調(diào)試
在當(dāng)前項(xiàng)目文件夾下執(zhí)行 npm link --force , 然后隨便找個(gè)文件下執(zhí)行 xmant cm:

看一下我們批量創(chuàng)建的文件和文件夾:

總結(jié)
關(guān)于快速創(chuàng)建代碼模板的方法有很多,有VSCode插件,也有CLI工具,更有做成 lowcode/nocode 平臺(tái)的方式等等。本文這種方式的好處在于足夠靈活,我們可以根據(jù)具體的需求進(jìn)行靈活的改造,使得更適用。
