如何创建自己的cli脚手架

现代前端开发中已经离不开各种脚手架的使用了,例如vue-cli和create-react-app等脚手架,而我们在日常开发中都会在这些脚手架的基础上做一些升级改造,例如集成一些常用的工具、网络请求封装、状态管理、特定布局等等,然后在后续的新项目中复用这些基础架构。为了方便管理和使用,我们可以将其抽离出来形成一个项目模版,通过类似vue-cli脚手架的方式来创建一个新项目,接下来本文就介绍一下如何动手创建一个自己的脚手架工具。

一、创建项目

新建一个项目文件夹,这里我将其命名为easy-cli,然后在该目录下执行:npm init -y生成package.json文件。

为了能够使用全局命令,我们需要在package.json里增加bin字段。

json 复制代码
{
  "name": "easy-cli",
  "version": "1.0.0",
  // ...
  "bin": {
    "easy-cli": "./src/index.js"
  }
}

增加bin字段后我们就可以像下面这样全局使用命令:

bash 复制代码
easy-cli xxx

现在我们还无法执行命令,因为我们还没创建bin字段所指向的文件,接下来我们创建src/index.js

js 复制代码
#! /usr/bin/env node

console.log('hello')

第一行代码#! /usr/bin/env node是必须的,它用来告诉系统使用node环境来执行该文件。

然后执行命令:npm link将其链接到全局变量,接下来我们就可以使用easy-cli命令了。

可以看到js文件被执行成功了。

二、定义版本

首先需要安装commander库,commander可以帮助我们处理命令行。

bash 复制代码
npm i commander --save

commander内部做了封装,使用命令--version查看版本时会输出我们在version方法里定义的值。

js 复制代码
#! /usr/bin/env node

const program = require('commander')
const pkg = require('../package.json')

program.version(`v${pkg.version}`) // 定义当前版本

program.parse(process.argv) // 解析用户执行命令传入参数

三、命令行交互

我们希望在使用脚手架时通过如下步骤来创建一个新项目项目:

  • 首先执行easy-cli create命令。
  • 用户输入项目名称。
  • 用户选择模板。
  • 将用户选择的模板copy到本地。

在最后一步这里,一种选择是将项目模板放在脚手架里,然后将其对应的目录复制到本地。另一种选择是将项目模板放在git仓库里,然后在用户选择后将其下载到本地。这里我们选择第二种方案,这样当项目模板有改动时不需要频繁的去脚手架里改动。

1、获取项目名称

这里我们需要先安装inquirer,由于inquirer9.0以上版本无法使用require,所以安装时我们选择安装8版本的inquirer

bash 复制代码
npm i inquirer@8 --save
js 复制代码
#! /usr/bin/env node

const program = require('commander')
const inquirer = require('inquirer')
const pkg = require('../package.json')

program.version(`v${pkg.version}`) // 定义当前版本

program
  .command('create')
  .description('创建模版')
  .action(async () => {
    const { projectName } = await inquirer.prompt({
      type: 'input',
      name: 'projectName',
      message: '请输入项目名称:'
    })
    console.log(projectName)
  })
 
program.parse(process.argv) // 解析用户执行命令传入参数

项目名称我们需要用户手动输入,所以prompt里的type的值设置为input,除此之外type值还有list(列表选择)confirm(单选确认)等,name是设置结果返回值对应的key名称,设置为projectName则返回结果里通过projectName获取。

2、选择模板

js 复制代码
const program = require('commander')
const inquirer = require('inquirer')
const pkg = require('../package.json')

program
  .command('create')
  .description('创建模版')
  .action(async () => {
    const { projectName, template } = await inquirer.prompt([
      {
        type: 'input',
        name: 'projectName',
        message: '请输入项目名称:'
      },
      {
        type: 'list',
        name: 'template',
        message: '请选择模版:',
        choices: [
          {
            name: 'vue-admin',
            value: 'https://github.com:PanJiaChen/vue-element-admin'
          },
          {
            name: 'react-admin',
            value: 'https://github.com:marmelab/react-admin'
          }
        ]
      }
    ])
    console.log('projectName===', projectName)
    console.log('template===', template)
  })
 

prompt也支持传入数组的形式,这里我们可以将输入项目和选择模板的操作依次传入,choices接收的数组对象包含一个name和value,name用于显示,选择后会将对应的value值返回。

3、下载模版到本地

安装download-git-repo:

bash 复制代码
npm i download-git-repo --save
js 复制代码
const program = require('commander')
const inquirer = require('inquirer')
const pkg = require('../package.json')
const downloadGitRepo = require('download-git-repo')

program
  .command('create')
  .description('创建模版')
  .action(async () => {
    const { projectName, template } = await inquirer.prompt([
      {
        type: 'input',
        name: 'projectName',
        message: '请输入项目名称:'
      },
      {
        type: 'list',
        name: 'template',
        message: '请选择模板:',
        choices: [
          {
            name: 'vue-admin',
            value: 'https://github.com:PanJiaChen/vue-element-admin'
          },
          {
            name: 'react-admin',
            value: 'https://github.com:marmelab/react-admin'
          }
        ]
      }
    ])
    console.log('projectName===', projectName)
    console.log('template===', template)
    
    // 获取目标文件夹
    const dest = path.join(process.cwd(), projectName)
    downloadGitRepo(template, dest, (err) => {
      if (err) {
        console.log('下载模板失败' + err.message)
        return
      }
      console.log('下载模板成功')
    })
  })

通过process.cwd将项目模板下载到命令行当前目录下。

运行命令后可以看到项目模板已经成功下载下来了,一个自制的脚手架已经基本完成了。接下来我们做一些优化。

4、优化:判断项目是否已存在

将项目模版下载到本地前,我们可以做一下优化判断,如果用户输入的项目名称在本地目录中已经存在,则弹出选择让用户确认是否删除覆盖。

接下来我们安装第三方包fs-extra方便进行文件操作:

bash 复制代码
npm i fs-extra --save
js 复制代码
const program = require('commander')
const inquirer = require('inquirer')
const pkg = require('../package.json')
const downloadGitRepo = require('download-git-repo')
const fs = require('fs-extra')

program
  .command('create')
  .description('创建模版')
  .action(async () => {
    const { projectName, template } = await inquirer.prompt([
      {
        type: 'input',
        name: 'projectName',
        message: '请输入项目名称:'
      },
      {
        type: 'list',
        name: 'template',
        message: '请选择模板:',
        choices: [
          {
            name: 'vue-admin',
            value: 'https://github.com:PanJiaChen/vue-element-admin'
          },
          {
            name: 'react-admin',
            value: 'https://github.com:marmelab/react-admin'
          }
        ]
      }
    ])
    
    // 获取目标文件夹
    const dest = path.join(process.cwd(), projectName)
    if (fs.existsSync(dest)) {
      const { isExist } = await inquirer.prompt({
        type: 'confirm',
        name: 'isExist',
        message: '您输入的项目名称已存在,是否覆盖?',
      })
      // 如果覆盖就删除文件夹,否则退出
      isExist ? fs.removeSync(dest) : process.exit(1)
    }
    downloadGitRepo(template, dest, (err) => {
      if (err) {
        console.log('下载模板失败' + err.message)
        return
      }
      console.log('下载模板成功')
    })
  })

5、优化:增加loading

安装ora

bash 复制代码
npm i ora@5 --save
js 复制代码
const ora = require('ora')

const loading = ora('正在下载中...')
loading.start()
downloadGitRepo(template, dest, (err) => {
  if (err) {
    loading.fail('创建模版失败' + err.message)
    return
  }
  loading.succeed('创建模版成功')
})

6、优化:修改package.json

我们下载下来的项目模板里,package.json里的nameversion记录的都是项目模板里的信息。

通常我们需要把name改成我们新创建的的项目名称,version从1.0.0开始,所以我们可以简单的借助node的文件操作能力进行修改。

js 复制代码
downloadGitRepo(template, dest, (err) => {
  if (err) {
    loading.fail('创建模版失败' + err.message)
    return
  }
  loading.succeed('创建模版成功')
  
  // 修改package.json
  const pkgPath = path.join(process.cwd(), projectName, 'package.json')
  const pkgContent = fs.readFileSync(paths, 'utf-8')
  const pkgJson = JSON.parse(pkgContent)
  
  pkgJson.name = projectName
  pkgJson.version = '1.0.0'
  
  fs.writeFileSync(paths, JSON.stringify(pkgJson, null, 2))
})

四、发布npm

上面的工作完成后就可以选择发布到npm了,发布npm的步骤比较简单,这里就不多赘述了。

注册一个npm账号,然后npm loginnpm publish

五、感谢

本次分享到这里就结束了,感谢您的阅读,如果本文对你有什么帮助的话,别忘了动动手指点个赞❤❤❤!

相关推荐
四喜花露水8 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
前端Hardy17 分钟前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
洛卡卡了42 分钟前
从单层到 MVC,再到 DDD:架构演进的思考与实践
架构·mvc
web Rookie1 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
Au_ust1 小时前
css:基础
前端·css
帅帅哥的兜兜1 小时前
css基础:底部固定,导航栏浮动在顶部
前端·css·css3
yi碗汤园1 小时前
【一文了解】C#基础-集合
开发语言·前端·unity·c#
就是个名称1 小时前
购物车-多元素组合动画css
前端·css
编程一生2 小时前
回调数据丢了?
运维·服务器·前端
乌恩大侠2 小时前
O-RAN Fronthual CU/Sync/Mgmt 平面和协议栈
5g·平面·fpga开发·架构