手撸一个脚手架

作者: yoona幻尘 | 来源:发表于2019-10-20 12:14 被阅读0次

    懒人成就世界

    最近总是忙于帮别人解决问题,大家隔着电脑屏幕聊啊聊,总是不够真实,驴头不对马嘴,浪费时间。得,还是请对方把代码端上来,跑一跑看看啥错误。服务到位的朋友会去掉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

    相关文章

      网友评论

        本文标题:手撸一个脚手架

        本文链接:https://www.haomeiwen.com/subject/qmiomctx.html