写一个 cli 命令行工具来管理自己的项目模板

1,553 阅读2分钟

在我们使用 vue 开发项目的时候,一般都是使用命令行直接生成项目模板,eg:

$ yarn create vite-app <project-name>
$ cd <project-name>
$ yarn
$ yarn dev
$ yarn create vite-app <project-name>

// 等同于
$ yarn global add create-vite-app
$ create-vite-app <project-name>

如果我们想自己弄一个 cli 工具来管理自己的项目模板,该怎么弄呢?在这里写一个简单 cli 工具通过命令行来拉取已定义的模板。


初建项目

创建一个目录,然后初始化:

mkdir test-cli // 创建目录

cd test-cli // 进入目录
 
npm init -y // 初始化npm

然后安装一些需要用到的 npm 包:

npm install commander download-git-repo ora handlebars figlet clear chalk -s

注册全局命令

我们需要在系统环境上注册一个全局命令。

新增 bin/index.js 文件:

mkdir bin
cd bin
touch index.js

// bin/index.js
#!/usr/bin/env node
// 指定文件解释器
// 告诉操作系统,当执行这个文件的时候,调用系统环境下的node解释器

console.log('test 111 ...')

在 package.json 新增 bin 命令:

{
  "bin": {
    "tcli": "./bin/index.js"
  }
  // ......
}

然后执行 npm link,等执行完成之后,在命令行输入 tcli 就能执行 ./bin/index.js 文件了。

npm link 是将本地包进行临时的全局安装,相当于 npm install -g xxx,会引起全局污染,有可能会全局安装失败,需要管理员权限。

image.png

创建node命令

#!/usr/bin/env node
// 引入自定义命令组件
const program = require('commander')

// 设置版本号
program.version(require('../package').version)

// 设置 init 命令,name 为参数
program.command('init <name>')
      .description('init project') // 设置init命令的描述
      .option('-s, --session', 'session param') // 设置参数
      .action((name, params) => { // 执行逻辑
        console.log('params => ', name)
        console.log('params.session => ', params.session)
      })

// 命令的执行其实是 commander 解析 process.argv 参数来启动
program.parse(process.argv);

image.png

下载模板

修改 program.action 方法:

program.command('init <name>')
    .description('init project')
    .option('-s, --session', 'session param')
    .action(require('../lib/init'))

新建 lib/init.js

// lib/init.js
const { promisify } = require('util') // 将函数转为promise格式
const figlet = promisify(require('figlet')) // 字体包

const clear = require('clear') // 控制台清屏
const chalk = require('chalk') // 修改控制台中字符串的样式
const log = ctx => console.log(chalk.green(ctx)) // 封装日志输出

module.exports = async name => {
  clear()
  const data = await figlet('Hello ' + name)
  log(data)
}

kid.gif

新建一个 lib/download.js 文件,处理模板下载逻辑:

// lib/download.js
const { promisify } = require('util')

/**
 * repo 下载的模板路径
 * desc 下载的目标路径地址
 */
module.exports.clone = async function(repo, desc) {
  const download = promisify(require('download-git-repo')) // 下载组件
  const ora = require('ora')
  const process = ora(`⏳下载...... ${repo}`) // 用于显示加载效果,类似页面的 loading 效果
  process.start()
  await download(repo, desc)
  process.succeed()
}

修改 lib/init.js,封装 spawn 命令,在 init 初始化加入下载模板,安装依赖

// 新增 clone 引入
const { clone } = require('./download') 

// 封装 spawn 命令
// 子进程输出流引入主进程输出流
const spawn = async (...args) => {
  const { spawn } = require('child_process')

  const options = args[args.length - 1]
  // 如果系统为windows,则需要修改 shell 为 true
  if (process.platform === 'win32') {
    // 设置 shell 为 true,以隐式的调用 cmd
    options.shell = true
  }

  return new Promise(resolve => {
    const proc = spawn(...args)
    // proc(子) -> process(主)
    proc.stdout.pipe(process.stdout) // 标准输出
    proc.stderr.pipe(process.stderr) // 标准错误输出
    proc.on('close', resolve)
  })
}

module.exports = async name => {
  // 打印欢迎页面
  clear()
  const data = await figlet('Hello ' + name)
  log(data)

  log('🚀创建项目 ' + name)
  await clone('github:554246839/project-template', name)

  // 安装依赖
  log('💣安装依赖......')
  await spawn('yarn', ['install'], { cwd: `./${name}` })
  log(`
    👌安装完成
    To get start
    ======================
      cd ${name}
      yarn dev
    ======================
  `)
}

功能到这已经差不多完成了,看下效果。

在其它地方新建一个目录,然后执行下载安装:

GIF-3.gif

如果想下载不同的模板,有两种方式:

inquirer 组件

inquirer 是一个用户与命令行交互的工具。新建 lib/confirm.js:

// lib/confirm.js
const inquirer = require("inquirer")

module.exports.prompt = () => {
  return inquirer.prompt([
    {
      type: 'confirm',
      name: 'simple',
      message: 'use simple',
      default: true
    }
  ])
}

// lib/init.js
// 新增
const { prompt } = require('./confirm')

// 修改
module.exports = async (name, params) => {
  // 打印欢迎页面
  clear()
  const data = await figlet('Hello ' + name)
  log(data)

  log('🚀创建项目 ' + name)
  const ans = await prompt()
  console.log(ans, 'ans')
  await clone('github:554246839/project-template', name)
  // xxx

GIF.gif

options 参数

新建 template.js 模板管理文件:

// lib/template.js
module.exports.templates = {
  project1: 'github:554246839/project-template',
  project2: 'github:554246839/project-template',
  project3: 'github:554246839/project-template'
}

// lib/init.js
// 新增
const { templates } = require('./template')

function getParam(params) {
  let p = Object.keys(params)
  for (let i = 0; i < p.length; i++) {
    if (params[p[i]]) {
      return p[i]
    }
  }
}
// 修改
module.exports = async (name, params) => {
  // 打印欢迎页面
  clear()
  const data = await figlet('Hello ' + name)
  log(data)

  log('🚀创建项目 ' + name)
  await clone(templates[getParam(params) || 'project1'], name) // 修改

  // 安装依赖
  log('💣安装依赖......')
  await spawn('yarn', ['install'], {cwd: `./${name}`})
  log(`
    👌安装完成
    To get start
    ======================
      cd ${name}
      yarn dev
    ======================
  `)
}

GitHub:github.com/554246839/t… master 分支