手撸一个脚手架

懒人成就世界

最近总是忙于帮别人解决问题,大家隔着电脑屏幕聊啊聊,总是不够真实,驴头不对马嘴,浪费时间。得,还是请对方把代码端上来,跑一跑看看啥错误。服务到位的朋友会去掉node_modules丢个压缩包过来,不那么讲究的童鞋就整个项目全都丢过来,几十上百m,啊,100k的小水管要下到地老天荒,费事!么得办法,还是得想个办法,省点事。说来也巧,同事在搞个基于lerna的工具脚手架,为了方便,直接采用的vue-cli的模式,这提醒了我,我也可以搞个脚手架,方便你我他呀,那就干脆手撸一个脚手架吧。

说干咱就干了。首先说下想法,项目很多,那我肯定是希望一个命令就能帮我下好项目,但是这样也比较low,git也能做到,那就做的比git多一点吧,很多项目master分支都是个摆设,通常dev或develop才是项目的真实代码,那我这个脚手架就要支持分支选择,最后一想,干脆,node_modules我也给你下了吧。这样,这个脚手架的大致想法就齐活啦。

开干!

npm init肯定是first step,生成了package.json之后,就开始考虑需要啥依赖了,首先涉及到shell,不管是git操作还是下载依赖都需要执行sh,自然就想到了shell.js, 不过查阅了一下资料,node原生就提供sh执行工具child_process,允许我们创建子进程去操作,并且既有异步也有同步的,可以返回promise。说到脚手架,其实一直很好奇,那些和使用者进行的交互是如何做到的,以前C++java可以等待用户输入然后执行下一步,js这样还真的比较少见,查了资料,我发现,很多是使用co库去做的,乍看co,好像没看见过,但是一看用法,哎,不对,这哥们眼熟。

co(function* (){
   let data1 = yield readFile('path1')
   console.log(data1)//显示path1的文件的内容
   let data2 =  yield readFile('path2')
   console.log(data2)//显示path2的文件内容
})

co基于generator函数,相当于generator函数的一个自动执行器,如上,yield执行完之后,co自动执行了next() 指向下一个console函数,简单理解就很像async/await,阮一峰有几篇讲异步同步的文章,从头看到尾的话应该会很有收获,附在文尾。

有了异步转同步还不够,我们要能获取用户输入呀!

var name = yield prompt('username: ');
var pass = yield password('password: ');
var desc = yield multiline('description: ');
var ok = yield confirm('are you sure? ');

看到上面这块是不是就很眼熟啦,让你输入用户名,密码,多行描述,是否确认,获取用户输入的功能就是靠co-prompt来给我们提供的。co搭配co-prompt再加上child_process,我们执行构建的需求就差不多完成了。

'use strict'

// const exec = require('child_process').exec
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const co = require('co')
const prompt = require('co-prompt')
const chalk = require('chalk')


async function nextSh(sh1, sh2) {
  console.log(chalk.white('\n 开始拉取代码...'))
  const { error } = await exec(sh1);
  if(error) {
    console.log(error)
    process.exit()
  }else {
    console.log(chalk.green('\n √ 拉取代码成功!'))
    console.log(chalk.green('\n 开始install...'))
  }
  const { error : error1  } = await exec(sh2);
  if(error1) {
    console.log(error1)
    process.exit()
  }else {
    console.log(chalk.green('\n √ 构建完成!'))
    process.exit()  
  } 
}

module.exports = () => {
  // generator函数
   co(function *(){
    // 处理用户输入的交互
    let name = yield prompt('项目名:')
    let gitUrl = yield prompt('Git地址:')
    let branch = yield prompt('分支是(默认是master):') 
    let install = yield prompt('使用yarn还是npm或是其他进行install(默认是npm):')

    branch = branch || 'master'

    install = install || 'npm'

    let sh1 = `git clone -b ${branch} ${gitUrl} ${name}`
    let sh2 = `cd ${name} && ${install} install`

    console.log(chalk.white('\n 开始拉取代码...'))

    exec(sh1, (error) => {
      if(error){
        console.log(error)
        process.exit()
      }
      console.log(chalk.green('\n √ 拉取代码成功!'))
      console.log(chalk.white('\n 开始install...'))

      exec(sh2, (error) => {
        if(error){
          console.log(error)
          process.exit()
        }
        console.log(chalk.green('\n √ 构建完成!'))
        process.exit() 
      })

    })

    // nextSh(sh1, sh2)
  })
}

chalk是一个控制字体显示颜色的库,也可以使用另一个spin库,更美观一点,后续应该会加上。不过这不是重点,上面的代码中,实现了两种执行exec的方式,嵌套和async/await,不过async/await需要对child_process进行util.promisify包装,这样它的返回才是一个promise。理论上,co的这一套也是可以被async/await去取代的,正所谓万法皆通,正是这个道理。

执行文件我们写好了,怎么把它挂载到node命令上去呢?那来写个命令文件吧。目前Commandernode.js命令行界面的完整解决方案,具体它的用法可以去查阅官网。

#!/usr/bin/env node --harmony

'use strict'

process.env.NODE_PATH = __dirname + '/../node_modules/'

const program = require('commander')

// 获取version
program.version(require('../package').version)

program.usage('<command>')

program
    .command('init')
    .description('构建一个已有git项目')
    .alias('i')
    .action(()=>{
      // 执行init
      require('../command/init')()
    })

  //  必须加上这些,才可以执行commands
    program.parse(process.argv)

    if (!program.args.length) {
        program.help()
    }

commander一定要执行parse命令,process.argv中包含program中传入的argsoptions,这个不被执行,那commander没有意义。

npm如何publish我就不在这里赘述了,网上很多。有一点要说下,我们开发的过程中npm指向的仓库可能是淘宝或是其他的源,发布时就会报错,执行下npm config set registry [http://registry.npmjs.org/](http://registry.npmjs.org/)就好了。
最后的最后,我们想像执行vue-cli init一样,直接initPack i就执行命令,还需要在package .json中修改bin

{
  "name": "huanchen-cli",
  "version": "1.0.3",
  "description": "自制clidemo",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "cli"
  ],
  "author": "1540226204@qq.com",
  "license": "ISC",
  "repository": {
    "type": "git",
    "url": "https://github.com/fatehuanchen/huanchen-cli.git"
  },
  "bin": {
    "initPack": "bin/initPack"
  },
  "dependencies": {
    "chalk": "^2.4.2",
    "co": "^4.6.0",
    "co-prompt": "^1.0.0",
    "commander": "^2.19.0"
  }
}

可以看的,bin下的initPack指向的是当前项目下bin目录下的文件,当我们下载cli时,就会自动把initPack挂载到全局路径上,就可以直接指向initPack i了。

一个完整的脚手架就撸好了,使用也非常简单,懒也要有懒的收获。


image.png

[ 代码传送门 ] (https://github.com/fatehuanchen/huanchen-cli.git)
相关文章: 阮一峰教学:http://www.ruanyifeng.com/blog/2015/05/async.html

推荐阅读更多精彩内容