模仿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内执行。其乐无穷

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269