手把手教你使用nodejs编写一个【使用远程仓库模板,快速创建项目模块】的cli(命令行)

44 篇文章 21 订阅
23 篇文章 18 订阅

系列文章


在工作过程中,很多时候我们会遇到一些很相似的需求,这时候我们会进行【搬砖】。这时候我们经常会复制一份相似的代码,改一改就成了。

但是这样有两个问题:

  1. 首先,从其他业务模块复制过来的代码中需要删删减减,有些繁琐,效率较低;

  2. 其次,即便复制的是一个基础模板代码,也会面临手动 copy 的低效问题;

  3. 还有,一般如果同事之间用一个代码模板库,需要将之 git clone 至本地磁盘,一般手动 copy 很少会 git pull 代码,这样就会造成代码模块版本滞后。

我们要实现的就是实现一个能【读取远程仓库模板,快速创建项目模块的脚手架工具】,以下为流程图与操作动态图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JOW3Yixk-1645350844003)(/Users/HuaJi/Downloads/16.gif)]
在这里插入图片描述

实现步骤

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 --helpcreate-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 即可使用啦

源码:https://gitee.com/yanhuakang/create-modules-tools

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

__畫戟__

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值