手拉手CLI工具开发(开始)

398 阅读2分钟

1.什么是CLI

CLI(Command Line Interface的缩写面),即命令行界面,是指可在用户提示符下键入可执行指令的界面。常用的vue-clicreate-react-appexpress-generator 等都是cli工具。

2.开始

本篇文章对应的项目地址: (github.com/DIVINER-onlys/…)

创建一个efox-cli目录,并进入该目录

mkdir efox-cli && cd efox-cli

执行

npm init

生成的package.json如下,新增bin,用于存放一个可执行文件,如下

// package.json

{
  "name": "efox-cli",
  "version": "1.0.0",
  "description": "efox-cli",
  "bin": {
    "efox-cli": "bin/efox.js"
  },
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "sam",
  "license": "ISC"
}


bin字段安装时,将可执行文件(bin/efox.js)链接到当前项目的./node_modules/.bin

  • 全局安装,npm将会使用指定符号链接(efox-cli)把这些文件链接到 /usr/local/bin/
  • 本地安装,会链接到./node_modules/.bin/

新建文件bin/efox.js,

// bin/efox.js

#!/usr/bin/env node
// 解决了不同的用户node路径不同的问题,可以让系统动态的去查找node来执行你的脚本文件
// https://github.com/jingzhiMo/jingzhiMo.github.io/issues/15
console.log('谢邀,人在家,刚下飞机')

接下来我们配置下efox-cli成为可执行命令,用于执行bin/efox.js文件,

执行npm install -gnpm link将当前项目安装到全局环境,这样就可以直接使用efox-cli来运行文件了

在package.json的script字段中添加脚本名:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "efox": "efox-cli"
  },

通过输入npm run efox,也可以输出内容

到这里,雏形已经出来

3.实现

首先献上整个实现的流程图,我们看着流程图来一步一步实现

检查node版本

我们需要semver(语义化版本控制模块),chalk(支持修改控制台中字符串的样式 字体样式、字体颜色、背景颜色),执行

npm i semver chalk

在package.json加入

// package.json

"engines": {
    "node": ">=8"
  }

检查node版本, 在bin/efox.js加入

// bin/efox.js

const semver = require('semver') // 语义化版本控制模块
const chalk = require('chalk') // 支持修改控制台中字符串的样式 字体样式、字体颜色、背景颜色
const package = require('../package.json')

// 检查node版本
function checkNodeVersion(wanted, id) {
  if(!semver.satisfies(process.version, wanted)) {
    console.log(chalk.red.bold.bgBlack(
      `You are using Node ${process.version}, but this version of ${id} requires Node ${wanted}.\nPlease upgrade your Node version.`
    ))
    process.exit(1)
  }
}
checkNodeVersion(package.engines.node, 'efox')

node版本低于8时

定义指令

明确我们的目标,我们的指令是 efox-cli c name -p efox -m nextjs -f

  • c:创建指令
  • -p: 指定项目组
  • -m:指定模板
  • -f:强制覆盖同名文件夹 定义指令我们需要用到 commander(命令行工具),ora(实现node.js命令行环境的loading效果,和显示各种状态的图标等)执行
npm i commander ora

由于commander各项使用npm上说明很详细,这里不多做介绍,在bin/efox.js加入

// bin/efox.js

const program = require('commander') // 命令行工具
const ora = require('ora') // 实现node.js命令行环境的loading效果,和显示各种状态的图标等

program
  .version(package.version, '-v, --version')
  .usage('<command> [options]')

program
  .command('create [app-name]')
  .alias('c')
  .description('基于efox-cli创建一个项目')
  .option('-p, --project <project>', '选择项目组')
  .option('-m, --module <module>', '选择项目组内的目标目录')
  // .option('-d, --dest', '直接在当前目录生成项目')
  // .option('-g, --gitClone <repository>', '使用远程git项目作为模板')
  .option('-f, --force', '覆写同名文件夹生成项目')
  .action((name, cmd) => {
    const options = cleanArgs(cmd)
    if (process.argv.includes('-g') || process.argv.includes('--git')) {
      options.forceGit = true
    }
    console.log('执行指令内容', name, options)
    // 下面这个逻辑后面会说
    // require('../lib/create')(name, options)
  })
  
program
  .parse(process.argv)
  
  
// commander passes the Command object itself as options,
// extract only actual options into a fresh object.
function cleanArgs(cmd) {
  const args = {}
  cmd.options.forEach(o => {
    const key = camelize(o.long.replace(/^--/, ''))
    // if an option is not present and Command has a method with the same name
    // it should not be copied
    if (typeof cmd[key] !== 'function' && typeof cmd[key] !== 'undefined') {
      args[key] = cmd[key]
    }
  })
  return args
}

// 小驼峰转换
function camelize(str) {
  return str.replace(/-(\w)/g, (_, c) => c ? c.toUpperCase() : '')
}

执行 efox-cli c test -p efox -m nextjs -f

这里我们已经完成对指令的定义,并且通过指令执行后获取相应的数据

下期后面会处理通过这些参数来加载脚手架,也就是require('../lib/create')(name, options)这个逻辑

我们还可以定义其他指令,比如info

这里需要执行 npm i envinfo

// bin/efox.js

program
  .command('info')
  .description('查看当前运行环境')
  .action((cmd) => {
    console.log(chalk.green.bold('\nEnvironment Info:'))
    const spinner = ora('Start loading system configuration...').start()
    // eninfo 获取系统的信息,设备信息,浏览器,node版本等
    require('envinfo').run(
      {
        System: ['OS', 'CPU'],
        Binaries: ['Node', 'Yarn', 'npm'],
        Browsers: ['Chrome', 'Edge', 'Firefox', 'Safari'],
        npmPackages: '/**/{*efox*,@efox/*,*vue*,@vue/*/}',
        npmGlobalPackages: ['@efox/efoxcli']
      },
      {
        showNotFound: true,
        duplicates: true,
        fullTree: true
      }
    ).then(res => {
      console.log(chalk.green(res))
      spinner.succeed('Loading system configuration is complete')
    })
  })

输入 efox-cli info查看系统信息

还可以定义参数问题,如只执行efox-cli输出help信息

// bin/efox.js

// output help information on unknown commands
program
  .arguments('[command]')
  .action((cmd) => {
    if (!process.argv.slice(2).length) {
      program.outputHelp()
      return
    }
    program.outputHelp()
    console.log(chalk.red(`\n未知命令:${chalk.yellow(cmd)}, 帮助请输入: ${chalk.green('efox-cli --help')}\n`))
  })
  
// add some useful info on help
program.on('--help', () => {
  console.log(`  使用 ${chalk.cyan.green.bold(`efox-cli <command> --help`)} 命令来查询对应的使用方式`)
})

program.commands.forEach(c => c.on('--help', () => console.log()))

4.最后

如果本文对你有帮助的话,给本文点个赞吧

下期马上来