Kwin Huang

不会搞艺术的程序员不是好设计师.

仿vue-cli搭建属于自己的脚手架

2019-10-06

什么是脚手架

脚手架就是个工具,方便我们新建项目用的工具,有了这个项目我们就能直接通过几行命令, 再通过配置就可以开发了

想想我们平时开发一个新项目, 基本上我们可以用 git clone url 来新建(复制)项目, 或者这个复制粘贴整个文件夹

首先要基本了解vue-cli(@vue/cli)是怎么工作的

脚手架的本质也是从远程下载一个模板来进行新的项目, 所以, 有什么不同呢? 就高大上啊😳, 当然不止于此, 脚手架是高级版的克隆, 它提供了交互式的命令让我们可以动态的更改模板/其他配置项, 然后用命令就可以搞定了

前置知识

commander

这是用来编写指令和处理命令行的,具体用法如下

const program = require("commander");
// 定义指令
program
  .version('0.0.1')
  .command('init', 'Generate a new project from a template')
  .action(() => {
    // 回调函数
  })
// 解析命令行参数
program.parse(process.argv);

回忆一下,我们曾用过的 vue init/create 命令就是这样声明的。

inquirer

强大的交互式命令行工具,具体用法如下

const inquirer = require('inquirer');
inquirer
  .prompt([
    // 一些交互式的问题
  ])
  .then(answers => {
    // 回调函数,answers 就是用户输入的内容,是个对象
  });

想象一下我们用 vue init webpack project-name 之后是不是会有几个交互问题,问你文件名啊、作者啊、描述啊、要不要用 eslint 啊等等之类的,就是用这个来写的。

chalk

这是用来修改控制台输出内容样式的,比如颜色啊,具体用法如下:

const chalk = require('chalk');
console.log(chalk.green('success'));
console.log(chalk.red('error'));

ora

这是一个好看的加载,就是你下载的时候会有个转圈圈的那种效果,用法如下:

const ora = require('ora')
let spinner = ora('downloading template ...')
spinner.start()

download-git-repo

看名字很明显了,这是用来下载远程模板的,支持 GitHub、 GitLab 和 Bitbucket 等,用法如下

const download = require('download-git-repo')
download(repository, destination, options, callback)

其中 repository 是远程仓库地址;destination 是存放下载的文件路径,也可以直接写文件名,默认就是当前目录;options 是一些选项,比如 { clone:boolean } 表示用 http download 还是 git clone 的形式下载。

项目搭建

有了上面的知识储备后, 我们就正式开始撸代码了

  1. 首先我们创建一个文件夹, 取名为 demo-cli
  2. 在改目录下执行 npm init -y 命令, 在生成的 package.json 文件中写入以下依赖并执行 npm install 安装, 如下
"dependencies": {
  "chalk": "^2.4.2",
  "commander": "^2.19.0",
  "download-git-repo": "^1.1.0",
  "inquirer": "^6.2.2",
  "ora": "^3.2.0"
}
  1. 新建一个 bin 文件夹, 在 bin 文件夹下新建一个无后缀名的 demo 文件, 并写上:
#!/usr/bin/env node
console.log('hello');

这个文件就是我们整个脚手架的入口文件, 我们用 node ./bin/demo 运行一下, 就可以看到在控制台打印出 hello

这里要注意开头的 #!/usr/bin/env node 这个语句必须加上,主要是为了让系统看到这一行的时候,会沿着该路径去查找 node 并执行,主要是为了兼容 Mac ,确保可执行。

bin 目录初始化

当前,bin 目录下就只有一个文件,就是入口文件 demo。所以现在我们先来编写这个文件,由于内容较少,我们直接看代码:

#!/usr/bin/env node
const program = require('commander');

program
  .version(require('../package').version, '-V, --version') // 定义当前版本
  .usage('<command> [options]') // 定义使用方法
  .command('add', 'add a new template') // 定义四个指令
  .command('delete', 'delete a template')
  .command('list', 'list all the template')
  .command('init', 'generate a new project from a template')

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

这个文件的主要作用就是定义指令,现在我们用 node ./bin/demo 运行一下,就能看到如下结果:

commander

当然,你可能会觉得每次输入 node ./bin/demo 这个命令有点麻烦,没关系,我们可以在 package.json 里面写入已下内容:

// bin 用来指定每个命令所对应的可执行文件的位置
"bin": {
  "demo": "bin/demo"
}

然后在根目录下执行 npm link(就是把命令挂载到全局的意思),这样我们每次只要输入 demo,就可以直接运行了,so cool

是不是好像有点样子了呢😁😁😁,那就让我们继续完善下 bin 目录吧!ok,让我们在 bin 目录下再新建四个文件,分别对应上面的四个指令,然后分别处理四个指令要做的事情

同样的,我们修改一下 package.json 里面的 bin 内容,如下:

"bin": {
  "demo": "bin/demo",
  "demo-add": "bin/demo-add",
  "demo-delete": "bin/demo-delete",
  "demo-list": "bin/demo-list",
  "demo-init": "bin/demo-init"
}

然后执行 npm unlink 解绑全局命令,再执行 npm link 重新把命令绑定到全局

最后顺便在根目录下新建一个 template.json 文件,里面的内容就是一个 {}。

编写具体指令

好了,一切准备就绪,接下来就让我们来写下具体的四个指令吧。

demo-add

#!/usr/bin/env node

// 交互式命令行
const inquirer = require('inquirer')
// 修改控制台字符串的样式
const chalk = require('chalk')
// node 内置文件模块
const fs = require('fs')
// 读取根目录下的 template.json
const tplObj = require(`${__dirname}/../template`)

// 自定义交互式命令行的问题及简单的校验
let question = [
  {
    name: "name",
    type: 'input',
    message: "请输入模板名称",
    validate (val) {
      if (val === '') {
        return 'Name is required!'
      } else if (tplObj[val]) {
        return 'Template has already existed!'
      } else {
        return true
      }
    }
  },
  {
    name: "url",
    type: 'input',
    message: "请输入模板地址",
    validate (val) {
      if (val === '') return 'The url is required!'
      return true
    }
  }
]

inquirer
  .prompt(question).then(answers => {
    // answers 就是用户输入的内容,是个对象
    let { name, url } = answers;
    // 过滤 unicode 字符
    tplObj[name] = url.replace(/[\u0000-\u0019]/g, '')
    // 把模板信息写入 template.json 文件中
    fs.writeFile(`${__dirname}/../template.json`, JSON.stringify(tplObj), 'utf-8', err => {
      if (err) console.log(err)
      console.log('\n')
      console.log(chalk.green('Added successfully!\n'))
      console.log(chalk.grey('The latest template list is: \n'))
      console.log(tplObj)
      console.log('\n')
    })
  })