模仿vue-cli,手写一个脚手架

vue-cli

在vue的开发的过程中,经常会使用到vue-cli脚手架工具去生成一个项目。在终端运行命令vue create hello-world后,就会有许多自动的脚本运行。

  • 为什么会这样运行呢?
  • 我们自己是否也能写一个脚手架工具?
    带着这样的疑问,我们先来看看vue-cli。

解读vue-cli

首先我们可以来到vue-cli的安装目录:
mac用户来到路径:/usr/local/lib/node_modules 可以看到(windows可以自行到全局安装的目录下查看)


/usr/local/lib/node_modules.png

此时用我们的编辑器 打开@vue文件:

目录结构.png

lib内放的是具体各种配置和各种类,对于我们来说,这个目录内的就是所谓的业务逻辑。
bin内放的就是脚本命令的入口,调用lib的入口,入口在package.json内红框定义。我们姑且先放下业务逻辑,来看看这个入口文件。

现在,我们先放下所有的疑虑,我们打开bin/vue.js。
我们可以看到以下内容:

bin_vue_js1.png

bin_vue_js2.png

看完之后有什么感觉?咦?怎么跟终端内输出的好像?没错,就是这样,这就是我们使用vue-cli的时候具体的命令。我们去终端输入vue
发现了吗,终端内的具体命令全在vue.js内定义过了。

program
  .command('create <app-name>')
  .description('create a new project powered by vue-cli-service')
program
  .command('add <plugin> [pluginOptions]')
  .description('install a plugin and invoke its generator in an already created project')
program
  .command('invoke <plugin> [pluginOptions]')

image.png

好了,剩下代码有兴趣的可以自行打开对应目录读下去,可以学习人家优秀的设计思想。对于本文来说,剩下的许多东西都是业务代码了。我们开头的疑问为什么会这样运行呢?已经了解了一个大概了。我们现在来看看我们自己是否也能写一个脚手架工具?,答案是肯定的。开始动手吧。

手写一个自己的脚手架

先来看看可能需要用到哪些npm的包:

  • commander:参数解析
  • inquirer:交互式命令行工具,有他就可以实现命令行的选择功能
  • chalk:输出文本颜色,为了美丽~
    后续可能随着模块的增加,会出现更多需要的包

1. 创建项目

npm init -y # 初始化package.json

2. 创建文件目录

  • 在package.json内添加“bin”
  • bin下的文件没有格式,且第一行必须是#! /usr/bin/env node
    文件目录.png

3. 链接包到全局

npm link # // 取消链接 npm unlink

有时候可能需要在上面命令后面拼接 --force,mac权限问题记得前面sudo
可以去目录:/usr/local/lib/node_modules 查看,发现我们多了一个,同时在这时候终端输入一下试试,我们在package.json下叫的name叫superman-cli,所以我们的命令就是叫superman-cli:

链接全局.png

验证.png

好了,基础配置初始化的工作全部结束了!

4. 第一个命令

首先安装包commander

npm install commander --save

在目录bin/superman内

#! /usr/bin/env node

// console.log(1)
const program = require('commander')

program
  .version(`Version is ${require('../package.json').version}`)
  .description('从0开始 手写脚手架')
  .usage('<command> [options]')

  program
  .parse(process.argv)
测试版本命令.png

测试有效!

然后我们在前面Version命令下加入代码:

program
  .command('create <app-name>')
  .description('create a new project')
  .option('-f, --force', 'Overwrite target directory if it exists')
  .option('-c, --clone', 'Use git clone when fetching remote preset')
  .action((name, cmd) => {
    console.log('name', name)
    console.log('cmd', cmd)
  })

create命令.png

仔细对比我们的代码合终端的输出,我们就可以看到我们写的很多东西都生效了。接下来我们就优化一下.action下的参数,毕竟一大堆也不好处理:

program
  .command('create <app-name>')
  .description('create a new project')
  .option('-f, --force', 'Overwrite target directory if it exists')
  .option('-c, --clone', 'Use git clone when fetching remote preset')
  .action((name, cmd) => {
    const options = cleanArgs(cmd)
    console.log(options)
  })
function camelize (str) {
  return str.replace(/-(\w)/g, (_, c) => c ? c.toUpperCase() : '')
}
function cleanArgs (cmd) {
  const args = {}
  // console.log(cmd)
  cmd.options.forEach(o => {
    const key = camelize(o.long.replace(/^--/, ''))
    // console.log(key)
    // console.log(cmd[key])
    // console.log(typeof cmd[key])
    if (typeof cmd[key] !== 'function' && typeof cmd[key] !== 'undefined') {
      args[key] = cmd[key]
    }
  })
  return args
}

具体不懂的也可以像我注释的console.log一样,慢慢看就明白了


参数处理.png

我们第一个命令已经完成一大半了,接下来就是我们这个create命令具体干什么事情。(在这个文件里,我们只管命令,就像vue-cli一样,这也是我们需要学习的地方,模块如何去处理)

// 在上面.action内补充一行代码
  .action((name, cmd) => {
    const options = cleanArgs(cmd)
    console.log(options)
    require('../lib/create')(name, options)
  })

同时去lib下创建文件create.js


const path = require('path')
// const fs = require('fs-extra')
async function create (projectName, options) {
  console.log(projectName, options)
  const cwd = process.cwd(); // 获取当前命令执行时的工作目录
  const targetDir = path.join(cwd,projectName); // 目标目录
  console.log(targetDir)
}

module.exports = (...args) => {
  return create(...args)
}

继续执行superman-cli create hello -f,我们可以得到,force:true,如果新建的话,将来的目录会是/Users/chenjing/hello

image.png

接下来我们尝试创建目录hello,不过我们需要考虑几个问题:

  • 是否已经存在目录hello了?(使用fs-extra包)
  • 若存在是要删除覆盖还是停止操作?(这里就需要用到插件inquirer啦,进行选择)
npm install fs-extra --save 
npm install inquirer --save

直接上代码:

const path = require('path')
const fsextra = require('fs-extra')
const fs = require('fs')
const Inquirer = require('inquirer')
async function create (projectName, options) {
  console.log(projectName, options)
  const cwd = process.cwd(); // 获取当前命令执行时的工作目录
  const targetDir = path.join(cwd,projectName); // 目标目录
  console.log(targetDir)
  if (fsextra.existsSync(targetDir)) {
    if (options.force) {// 如果强制创建 ,删除已有的
      await fsextra.remove(targetDir);
      console.log('删除成功')
      createDir(projectName)
    } else {
      let { action } = await Inquirer.prompt([
        {
          name: 'action',
          type: 'list',
          message: 'Target directory already exists Pick an action:',
          choices: [
            {name:'Overwrite',value:'overwrite'},
            {name:'Cancel',value:false}
          ]
        }
      ])
      if (!action) {
        console.log('取消操作')
        return
      } else if (action === 'overwrite') {
        console.log(`\r\nRemoving....`);
        await fsextra.remove(targetDir)
        console.log('删除成功')
        createDir(projectName)
      }
    }
  } else {
    createDir(projectName)
  }
}
function createDir (projectName) {
  fs.mkdir(`./${projectName}`, function (err) {
    if (err) {
      console.log('创建失败')
    } else {
      console.log('创建成功')
    }
  })
}

module.exports = (...args) => {
  return create(...args)
}

看效果,先来的一个空目录


空目录.png
superman-cli create hello
superman-cli create hello // 再次
superman-cli create hello -f // 覆盖
create.png
再次create.png
覆盖.png

4. 小结

本篇的源码github地址

并不是说我们手写脚手架就到此结束了,只是要完整实现一个功能不是一两篇文章可以搞定的。不过我相信写到这里,动手能力强的一定也能体验一把手动模仿vue-cli的爽了。至于能写出什么牛C的脚手架,真的就是个人需求和业务代码堆加。当然可以发散思维后续还可以做许多许多事情。强烈建议阅读vue-cli。或者其他脚手架的源码,都在目录:/usr/local/lib/node_modules 下面,看源码真的是学习最直接的方法了,甚至copy人家的代码到自己的cli内执行。其乐无穷