系列文章
- 手把手教你使用nodejs编写cli(命令行)
- 手把手教你使用nodejs编写cli(命令行)——拉取远程仓库作为代码模板
- 手把手教你使用nodejs编写一个【使用远程仓库模板,快速创建项目模块】的cli(命令行)
在工作过程中,很多时候我们会遇到一些很相似的需求,这时候我们会进行【搬砖】。这时候我们经常会复制一份相似的代码,改一改就成了。
但是这样有两个问题:
首先,从其他业务模块复制过来的代码中需要删删减减,有些繁琐,效率较低;
其次,即便复制的是一个基础模板代码,也会面临手动 copy 的低效问题;
还有,一般如果同事之间用一个代码模板库,需要将之 git clone 至本地磁盘,一般手动 copy 很少会 git pull 代码,这样就会造成代码模块版本滞后。
我们要实现的就是实现一个能【读取远程仓库模板,快速创建项目模块的脚手架工具】,以下为流程图与操作动态图:
实现步骤
1. 交互式命令,获取用户选择;
2. 拉取远程仓库代码,读取仓库中的模板;
3. 根据用户选择,将选择的模板复制写入目标项目。
初始化cli项目
创建一个新项目 create-modules-tools
,并且使用 Node.js 的 esm
模块,并把 Node.js
升至 13.2.0
以上版本以支持 esm
模块。
esm
模块不支持文件扩展名的自动解析以及导入具有索引文件的目录的能力,后面需要注意不要丢掉 index.js、.js
package.json
{
...
"type": "module",
"bin": {
"create-module": "bin/index.js"
}
...
}
项目目录
create-modules-tools
├── bin
│ └── index.js # CLI执行入口文件
├── commands # 其他命令,如 --help
├── questions # 交互式命令行
├── utils # 工具函数,主要是获取脚手架项目的物理路径函数
├── .gitignore
├── package.json
├── package-lock.json
├── README.md # 使用文档
└── CHANGELOG.md # 版本记录文件
创建交互式命令
这里我们使用 inquirer 文档,安装 inquirer
npm i inquirer
1. 设置远程仓库地址 questions/remoteUrl.js
import fs from "fs";
export default (templatesDirRootPath) => {
let remoteUrl = ''
if (fs.existsSync(`${templatesDirRootPath}/defaultRemoteUrl.txt`)) {
remoteUrl = fs.readFileSync(`${templatesDirRootPath}/defaultRemoteUrl.txt`, "utf-8")
}
return {
type: 'input',
name: 'remoteUrl',
default: remoteUrl || undefined,
message: '请设置远程仓库地址',
validate(val) {
// git仓库的正则表达式 http://cn.voidcc.com/question/p-qlprjeax-kd.html
const gitRemoteUrlReg = /(\w+:\/\/)([email protected])*([\w\d\.]+)(:[\d]+){0,1}\/*(.*)/
if (!val) {
return '请设置远程仓库地址'
} else if (!gitRemoteUrlReg.test(val)) {
return '远程仓库地址格式错误,请重新输入'
} else {
return true;
}
}
}
}
2. 要创建的目录名称 questions/createDir.js
export default () => ({
type: 'input',
name: 'createDir',
message: '请输入要创建的目录名称',
validate(val) {
if (val) return true;
return '请输入要创建的目录名称'
}
})
3. 选择模板 questions/selectModules.js
import inquirer from "inquirer";
export default async (choices = []) => inquirer.prompt([
{
type: 'list',
name: 'tplModule',
message: '请选择模板',
choices
}
])
4. 交互问答入口文件 questions/index.js
import inquirer from 'inquirer'
import remoteUrl from "./remoteUrl.js";
import createDir from "./createDir.js";
export default (templatesDirRootPath) => inquirer.prompt([
remoteUrl(templatesDirRootPath), // 设置远程仓库地址
createDir(), // 要创建的目录名称
])
5. 脚手架入口文件 bin/index.js
#!/usr/bin/env node
// chalk 美化输出
import chalk from 'chalk'
import questions from '../questions/index.js'
import { getTemplatesDirRootPath } from '../utils/index.js'
import selectModules from '../questions/selectModules.js'
// 存放模板文件的目录路径
const templatesDirRootPath = getTemplatesDirRootPath()
const config = await questions(templatesDirRootPath)
console.log(chalk.blue('config:'), config);
// 先创建目标目录,检查用户输入的目录是否已存在
fs.mkdirSync(`./${config.createDir}`)
if (!fs.existsSync(templatesDirRootPath)) {
fs.mkdirSync(templatesDirRootPath)
}
执行 npm link
将该模块链接到全局npm模块中
执行 create-module
,可以得到用户输入的结果
拉取远程仓库代码,读取仓库中的模板
上面我们已经得到了远程仓库的地址,但是 Node.js 是没有能力读取远程 git 仓库中的目录的,将其clone 至本地,然后在本地读取。
这里我们应该考虑用户一般不会经常切换模板仓库,因此我们设计为:
在用户初次
git clone
后不将该本地模板仓库删除,而是保留。在用户下次使用脚手架时,我们去检查该模板是否存在于本地,如果存在,则执行git pull
,这样会更快的拉新读取。如果不存在则执行git clone
同时支持用户主动清空脚手架中的本地模板,
create-module --empty
拉取远程仓库代码
utils/index.js
import path from 'path'
import { fileURLToPath } from 'url'
import process from "process";
// 获取绝对路径
export const getRootPath = (pathUrl) => {
const __dirname = fileURLToPath(import.meta.url)
return path.resolve(__dirname, `../${pathUrl}`)
}
// 设置模板缓存目录
export const getTemplatesDirRootPath = () => {
// 存放模板文件的文件夹名称
const templatesDirPath = 'CreateModulesProjects'
const processCwd = process.cwd()
const processCwdArr = processCwd.split('/')
// 存放模板文件的目录路径
return `/${processCwdArr[1]}/${processCwdArr[2]}/${templatesDirPath}`
}
bin/index.js
// ...
// 获取远程仓库目录名称
const getGitRemoteFilename = () => {
const arr = config.remoteUrl.split('/')
return arr[arr.length - 1].split('.')[0]
}
// 远程仓库目录名称
const gitRemoteFilename = getGitRemoteFilename()
console.log(chalk.blue('gitRemoteFilename:'), gitRemoteFilename);
let getGitRemoteResult = {} // 拉取远程仓库结果
// 获取远程仓库代码
const getGitRemote = () => {
// 该远程仓库是否已经存在于本地
const exist = fs.existsSync(`${templatesDirRootPath}/${gitRemoteFilename}/.git`)
const spinners = [ora("读取中...")];
spinners[0].start();
if (exist) { // 存在,则 git pull
getGitRemoteResult = execaSync(`git`, ['config', 'pull.rebase', 'false'], {
cwd: `${templatesDirRootPath}/${gitRemoteFilename}`,
})
getGitRemoteResult = execaSync(`git`, ['pull'], {
cwd: `${templatesDirRootPath}/${gitRemoteFilename}`,
})
}
else { // 不存在,则 git clone
try {
getGitRemoteResult = execaSync(`git`, ['clone', '-b', 'master', config.remoteUrl], {
cwd: templatesDirRootPath,
})
} catch (err) {
fs.rmdirSync(`./${config.createDir}`)
console.error(err)
}
}
fs.writeFile(`${templatesDirRootPath}/defaultRemoteUrl.txt`, config.remoteUrl, err => {
if (err) console.log(err);
})
// console.log(chalk.blue('getGitRemoteResult:'), getGitRemoteResult);
// failed 一定返回,没有failed字段也代表失败
if (getGitRemoteResult.failed === true || getGitRemoteResult.failed === undefined || getGitRemoteResult.failed === null) {
spinners[0].fail("读取远程仓库失败!");
} else {
spinners[0].succeed("读取远程仓库成功!");
}
}
getGitRemote()
ora 终端 loading
上面拉取远程仓库代码子进程使用了 ora
来实现 loading
效果,如下图:
npm i ora
import ora from 'ora'
const spinners = [ora("Loading1..."), ora("Loading2...")];
// 开始第一个 loading
spinners[0].start();
setTimeout(() => {
spinners[0].succeed("Loading1 Success");
spinners[1].start();
}, 3000);
setTimeout(() => {
spinners[1].fail("Loading2 Fail");
}, 6000);
读取仓库中的模板
// ...
import selectModules from '../questions/selectModules.js'
// 读取并选择模板
const getAndSelectModule = async () => {
// 获取远程仓库中的目录
const tplDirs = fs.readdirSync(`${templatesDirRootPath}/${gitRemoteFilename}`)
console.log('tplDirs', tplDirs);
// 可选的模板
const tplModules = []
for (const item of tplDirs) {
// 筛选目录并将 .git 排除
if (fs.statSync(`${templatesDirRootPath}/${gitRemoteFilename}/${item}`).isDirectory() && item !== '.git'){
tplModules.push({
value: item,
name: item,
})
}
}
// 选择模板
const selectedModule = await selectModules(tplModules)
// 已选择的模板 selectedModule.tplModule
console.log(selectedModule);
return selectedModule
}
const selectedModule = await getAndSelectModule()
console.log('selectedModule', selectedModule);
将选择的模板复制写入目标项目
这里使用 fs-extra 复制文件
import fse from 'fs-extra'
/**
* 进行copy
* @param selectedModule 选择的模块
*/
const fsCopy = (selectedModule) => {
try {
fse.copy(`${templatesDirRootPath}/${gitRemoteFilename}/${selectedModule.tplModule}`,
`./${config.createDir}`,
(err) => {
if (err) {
console.error(err);
} else {
// git add 新创建的文件
execa(`git`, ['add', './'], { cwd: './', }, err => {
if (err) console.log(err);
})
console.log(chalk.green('创建模块成功!'))
}
})
} catch (err) {
fs.rmdir(`./${config.createDir}`)
console.error(err)
}
}
fsCopy(selectedModule)
至此,主体功能已完成。
Commands
另外,如果需要添加其他命令,如上文说的 create-modlue --help
、create-module --empty
可以在使用 process.argv
获取命令行参数进行相关逻辑,这里不再赘述了。
import fs from 'fs'
import { program } from 'commander';
import empty from "./empty.js";
const { version } = JSON.parse(await fs.readFileSync(new URL('../package.json', import.meta.url)))
program
.version(version, '-v, --version')
.option('-e, --empty', '清空本地模板缓存')
program.parse(process.argv);
const options = program.opts();
switch (true) {
case options.empty:
empty()
break;
}
commands/empty.js
import fs from "fs";
import { getTemplatesDirRootPath } from '../utils/index.js'
export default () => {
const templatesDirRootPath = getTemplatesDirRootPath()
if (fs.existsSync(templatesDirRootPath)) {
// recursive <boolean> 如果为 true,则执行递归删除。 在递归模式下,操作将在失败时重试。 默认值: false。
fs.rmSync(templatesDirRootPath, { recursive: true })
// 重新创建一个空目录
fs.mkdirSync(templatesDirRootPath)
console.log('清空本地模板缓存成功!');
process.exit(1)
} else {
console.log('清空本地模板缓存成功!');
}
}
完成
将其发布至 npm,执行 npm i create-modlues-tools -g
安装,执行 create-module
即可使用啦