何为脚手架?在日常开发中,我们创建vue项目所使用的vue-cli,创建react项目的create-react-app…都是非常优秀的脚手架。

那为什么要使用脚手架来创建项目呢?因为在实际开发中,使用脚手架我们可以快速的初始化一个项目,无需自己一步一步配置,可以有效的提升开发效率。

既然已经有这么多优秀的脚手架工具了,为什么还要自己开发一个脚手架呢?其实在实际开发中,有可能这些脚手架不符合我们的实际应用,这时候就需要我们自己根据实际的需求自己去“造轮子”。

一、脚手架运行的基本流程

脚手架就是通过命令交互的方式去渲染对应的模板文件,基本流程如下:
1.通过命令行交互询问用户问题
2.根据用户回答的结果生成文件

例如我们在使用vue-cli创建项目的时候

step1: 运行创建命令

vue create cli-test

step2: 用户选择

脚手架生成Java工程_前端


脚手架生成Java工程_javascript_02

step3: 生成符合需求的项目文件

# 忽略部分文件夹
cli-test
├─ index.html
├─ src
│  ├─ App.vue
│  ├─ assets
│  │  └─ logo.png
│  ├─ components
│  │  └─ HelloWorld.vue
│  ├─ main.js
│  └─ router
│     └─ index.js
└─ package.json
二、脚手架插件

脚手架工具需要命令行交互、大型字符打印、logo/输出字符美化、loading效果…,因此需要以下插件来实现这些功能。

名称

简介

commander

命令行插件

figlet

大型字符 - 终端打印大型文字

chalk

彩色文字 - 美化终端字符显示

inquirer

命令行参数输入交互

ora

loading效果

download-git-repo

仓库下载

下面我们演示这几个插件都在my-cli项目中演示

step1: 我们创建my-cli目录并初始化项目

mkdir my-cli

step2: 初始化项目

cd my-cli && npm init

step3: 创建bin/cli.js文件

mkdir bin && touch bin/cli.js

step4: 将入口文件改成cli.js

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

step5: 编辑cli.js文件-在文件开始添加 #! /usr/bin/env node这一段代码

#! /usr/bin/env node

// #! 符号的名称叫 Shebang,用于指定脚本的解释程序
// Node CLI 应用入口文件必须要有这样的文件头
// 如果是Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755
// 具体就是通过 chmod 755 cli.js 实现修改

// 用于检查入口文件是否正常执行
console.log('my-cli hahaha')

step6: npm link 链接到全局

npm link

当我们在控制台运行my-cli命令的时候,能够输出cli.js文件的内容,说明全局链接成功了

$ my-cli
my-cli hahaha
1. commander

安装依赖

npm i commander figlet chalk inquirer ora download-git-repo

在cli.js中引入

// cli.js
#! /usr/bin/env node

let program = require('commander')

program
  .command('create <app-name>')
  .description('create a new project')
  .action(async (name, options) => {
    console.log('project name is ' + name)
  })
 
program.parse()

然后我们运行my-cli create test-xxx

$ my-cli create test-xxx
project name is test-xxx
2. figlet
// cli.js
#! /usr/bin/env node
let figlet = require('figlet')

figlet('my-cli', function (err, data) {
  if (err) {
    console.log('Something went wrong...')
    console.dir(err)
    return
  }
  console.log(data)
})

运行my-cli

$ my-cli
                             _ _ 
  _ __ ___  _   _        ___| (_)
 | '_ ` _ \| | | |_____ / __| | |
 | | | | | | |_| |_____| (__| | |
 |_| |_| |_|\__, |      \___|_|_|
            |___/
3. chalk
// cli.js
#! /usr/bin/env node
let figlet = require('figlet')

let chalk = require('chalk')

figlet('my-cli', function (err, data) {
  if (err) {
    console.log('Something went wrong...')
    console.dir(err)
    return
  }
  console.log(chalk.yellow(data))
  console.log(chalk.blue(data))
  console.log(chalk.red(data))
})

运行my-cli

脚手架生成Java工程_vue.js_03

npm 下载chalk默认下载的是5.x版本,chalk从5.0+版本开始,只支持ESModule格式,chalk是使用export default导出的,chalk之前的版本支持CommomJs格式。所以这里我们要下载5.0以下版本。

4. inquirer

这里我们模仿vue-cli创建项目的命令交互

// cli.js
#! /usr/bin/env node

let program = require('commander')
let { promisify } = require('util')
let asyncFiglet = promisify(require('figlet'))
let chalk = require('chalk')
let inquirer = require('inquirer')

// 日志打印函数
const log = (content) => console.log(chalk.yellow(content))

// 设置版本和参数
program.version(`v${require('../package.json').version}`)
program.option('-n --name <type>', 'output name')

// 打印LOGO
async function printLogo() {
  let data = await asyncFiglet('v-cli')
  log(data)
}
program
  .command('create <app-name>')
  .description('create a new project')
  .option('-f, --force', 'overwrite target directory if iy exist')
  .action(async (name, options) => {
    await printLogo()
    log('准备创建项目...')
    let answer = await inquirer.prompt([
      {
        name: 'language',
        type: 'list',
        message: 'Please pick a present:',
        choices: [
          'Default([Vue 2] babel,eslint)',
          'Default(Vue 3)([Vue 3] babel,eslint)',
          'Manually select features',
        ],
      },
    ])
    if (answer.language == 'Default([Vue 2] babel,eslint)') {
      log('您选择了Vue2版本,即将进入下载模式.')
    } else if (answer.language == 'Default(Vue 3)([Vue 3] babel,eslint)') {
      log('您选择了Vue3版本,即将进入下载模式.')
    } else {
      log('你选择了手动配置')
    }
  })

// 参数解析
program.parse(process.argv)

运行my-cli create my-app

$ my-cli create my-app
                  _ _ 
 __   __      ___| (_)
 \ \ / /____ / __| | |
  \ V /_____| (__| | |
   \_/       \___|_|_|

准备创建项目...
? Please pick a present: (Use arrow keys)
> Default([Vue 2] babel,eslint)
  Default(Vue 3)([Vue 3] babel,eslint)
  Manually select features

选择了vue2

? Please pick a present: Default([Vue 2] babel,eslint)
您选择了Vue2版本,即将进入下载模式.

选择了vue3

? Please pick a present: Default(Vue 3)([Vue 3] babel,eslint)
您选择了Vue3版本,即将进入下载模式.

选择了Manually select features

? Please pick a present: Manually select features
你选择了手动配置

和chalk的问题一样,默认是9.x版本,需要降低版本

5. download-git-repo

当我们选择vue2的时候,我们直接下载vue2的默认版本

// cli.js
#! /usr/bin/env node

...
let ora = require('ora')
const spinner = ora('loading')

...
...
    if (answer.language == 'Default([Vue 2] babel,eslint)') {
      log('您选择了Vue2版本,即将进入下载模式.')
      spinner.start()
      setTimeout(() => {
        spinner.start()
        spinner.succeed('下载成功')
      }, 3000)
    } 
...

...

运行my-cli create my-app

$ my-cli create my-app
                  _ _ 
 __   __      ___| (_)
 \ \ / /____ / __| | |
  \ V /_____| (__| | |
   \_/       \___|_|_|

准备创建项目...
? Please pick a present: (Use arrow keys)
> Default([Vue 2] babel,eslint)
  Default(Vue 3)([Vue 3] babel,eslint)
  Manually select features

选择了vue2

脚手架生成Java工程_javascript_04

和chalk的问题一样,默认是6.x版本,需要降低版本

6. download-git-repo

模板克隆

// cli.js
#! /usr/bin/env node

...
let download = require('ora')
download('https://mygitlab.com:flippidippi/download-git-repo-fixture#my-branch', 'test/tmp', { clone: true }, function (err) {
  console.log(err ? 'Error' : 'Success')
})
三、创建自己的脚手架

前面我们介绍了几种创建脚手架的插件,下面让我们来利用上面的插件来创建一款自己的插件。

1. 创建项目

参考上面的列子,先创建一个目录结构

my-cli           
├─ bin                
│  └─ cli.js      
└─ package.json

配置package.json文件

{
  "name": "my-cli",
  "version": "1.0.0",
  "description": "",
  "bin": "./bin/cli.mjs",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "chalk": "^4.1.0",
    "commander": "^9.4.1",
    "download-git-repo": "^3.0.2",
    "figlet": "^1.5.2",
    "inquirer": "^8.1.0",
    "ora": "^5.4.1"
  }
}

使用npm link 链接到全局

npm link
2. 创建启动命令

安装命令行插件commander

npm i commander -S

参考上面的例子,创建脚手架的入口文件,编辑cli.js,创建create命令,并设置脚手架的版本和参数

// cli.js
#! /usr/bin/env node

let program = require('commander')
let create = require('./create')

// 设置版本和参数
program.version(`v${require('../package.json').version}`)
program.option('-n --name <type>', 'output name')

program
  .command('create <app-name>')
  .description('create a new project')
  .option('-f, --force', 'overwrite target directory if iy exist')
  .action(async (name, options) => {
    create(name, options)
  })

// 参数解析
program.parse(process.argv)

当创建的目录已经存在时,我们询问用户是否覆盖本地目录
我们创建create.js文件并在cli.js中调用

// create.js
const path = require('path')
const fs = require('fs-extra')
let inquirer = require('inquirer')
let init = require('./init')

module.exports = async function (name, options) {
  // current cmd directory address
  const cwd = process.cwd()

  // create target directory address
  const targetAir = path.join(cwd, name)
  if (fs.existsSync(targetAir)) {
    if (options.force) {
      await fs.remove(targetAir)
    } else {
      let answer = await inquirer.prompt([
        {
          name: 'language',
          type: 'list',
          message: `Target directory ${targetAir} already exists. Pick an action: (Use arrow keys)`,
          choices: ['Overwrite', 'Cancel'],
        },
      ])
      if (answer.language === 'Overwrite') {
        await fs.remove(targetAir)
      }
      if (answer.language === 'Cancel') {
        return
      }
    }
  }
  init(name)
}

上面的代码中targetAir代表的是目标目录地址,比如我们在D盘运行my-cli create test,此时targetAir就是D:\test
之后通过fs.existsSync()方法判断当前目录下是否有test文件。

  1. 若有,判断创建命令的时候是否带有-f/–force

带有-f/–force,就直接删掉本地文件,并创建新的文件

脚手架生成Java工程_脚手架生成Java工程_05

不带-f/–force,询问用户是否覆盖

脚手架生成Java工程_javascript_06

  1. 若无,直接创建新的目录

脚手架生成Java工程_javascript_07

3. 下载模板

创建init.js文件,并在create.js中调用

// init.js
let { promisify } = require('util')
const ora = require('ora')
const download = promisify(require('download-git-repo'))
let chalk = require('chalk')
let inquirer = require('inquirer')
let asyncFiglet = promisify(require('figlet'))

// 日志打印函数
const log = (content) => console.log(chalk.yellow(content))

// 打印LOGO
async function printLogo() {
  let data = await asyncFiglet('v-cli')
  log(data)
}

module.exports = async (appName) => {
  await printLogo()
  log(`🚀 创建项目 ${appName}`)
  let answer = await inquirer.prompt([
    {
      name: 'language',
      type: 'list',
      message: 'Please pick a present:',
      choices: [
        'Default([Vue 2] babel,eslint)',
        'Default(Vue 3)([Vue 3] babel,eslint)',
        'Manually select features',
      ],
    },
  ])
  if (answer.language == 'Default([Vue 2] babel,eslint)') {
    const spinner = ora('下载中...').start()
    try {
      await download(
        'direct:https://gitee.com/zjinxiaoliang/jimo-ui.git', // 这里是随便填的笔主的一个开源项目地址
        appName,
        { clone: true }
      )
      spinner.succeed('下载完成')
      log(`
下载完成,请执行下面命令启动项目:
===========================
cd ${appName}
yarn 或者 npm init 

npm run dev
或者
yarn dev
        `)
    } catch (error) {
      log(`下载失败`, error)
      spinner.stop()
    }
  } else if (answer.language == 'Default(Vue 3)([Vue 3] babel,eslint)') {
    log('您选择了Vue3版本,即将进入下载模式.')
  } else {
    // manually(name)
  }
}

当我们执行创建项目的命令

脚手架生成Java工程_javascript_08

4. 发布到npm

当我们在使用vue-cli之类的脚手架的时候都是, 我们只需要npm全局安装之后,就能够通过命令直接创建项目。那我们应该如何发布到npm官网上呢?
step1: 在git上创建好仓库
step2: 修改package.json
因为,我发布到npm上的时候,发现my-cli已经被占用了,所有我将name改成了jimo-cli

{
  "name": "jimo-cli",
  "version": "1.0.0",
  "private": false,
  "description": "",
  "main": "/bin/cli.js",
  "bin": {
    "jimo": "./bin/cli.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "files": [
    "bin"
  ],
  "keywords": [
    "jimo-cli",
    "cli",
    "脚手架"
  ],
  "author": {
    "name": "jinxiaoliang",
    "email": "1848815766@qq.com"
  },
  "license": "ISC",
  "dependencies": {
    "chalk": "^4.1.0",
    "commander": "^9.4.1",
    "download-git-repo": "^3.0.2",
    "figlet": "^1.5.2",
    "fs-extra": "^10.1.0",
    "inquirer": "^8.1.0",
    "ora": "^5.4.1"
  }
}

step3:使用npm publish到npm官网上,发布成功之后,我们到npm官网上面看一下

脚手架生成Java工程_前端_09

step4: 我们在本地下载脚手架

脚手架生成Java工程_Vue_10

step5:然后我们在本地创建项目

脚手架生成Java工程_脚手架生成Java工程_11

到此为止,我们就已经构建好了自己的脚手架了。