记一次脚手架搭建

355 阅读5分钟

无意中刷掘金看到几篇关于脚手架搭建的文章,并且之前已经有意向自己搭一个像vue-cli的脚手架:很好奇人家 怎么 vue init template 就可以像命令符一样一步一步下去,然后就可以下载到模板。

这里自己去试着去看了一下vue-cli v2版本的源码结合一些资料,简单写了一个用脚手架的形式拉取github的项目

安装
npm i -g g-xsk-cli
使用

初始化

gxc
// 按提示说明走

删除下载项目

gxc del <projectName>
// 或 gxc d <projectName>

接下来就来讲一下写成上面这个东西的步骤

前置知识

  • 强大的交互式命令行工具:inquirer
  • 编写指令和处理命令行的的工具:commander
  • 修改控制台输出内容样式的工具:chalk
  • 可执行shell命令的node模块: child_process
  • 好看的加载的转圈圈效果:ora
  • 网络请求库 : axios

建议先去创建文件去玩一下上面几个工具,挺有意思的!

在源码的example文件里也有我之前尝试过的,可以直接运行

package.json -> bin属性

命令定义属性

"bin": {
    gxc: './bin/index.js'
}

并且 /bin/index.js,行首加入一行 #!/usr/bin/env node 指定当前脚本由node.js进行解析

#!/usr/bin/env node
// 开始写

这就是为什么vue-cli能直接跑 vue init template?

秘密就在这里

npm中,在package中设置bin,npm就会对vue创建软连接,路径为node_modules/.bin/vue,而应用
在运行时npm就会把node_modules/.bin加入系统变量,运行时就可以直接 vue init <template>

具体可以看这里

这里我们在调试时,是主动加入的,所以需要 npm lint软连接一波,这样就可以直接运行了

gxc

// 也可以用node ./bin/index.js执行调试
将脚手架上传npm

如何上传脚手架,很简单

1.npm login    // 登录 npm账号
2.npm publish  // 设置好版本,上传即可

注意 在下载依赖的时候

用 npm i -S inquirer commander chalk ora axios 创建,加入到

"dependencies": {
    "axios": "^0.19.2",
    "chalk": "^4.0.0",
    "commander": "^5.1.0",
    "inquirer": "^7.1.0",
    "ora": "^4.0.4"
},

因为最后我们全局安装的时候:npm i -g g-sxk-cli 他会自动把dependencies这些依赖下载下来

注意 使用的时候,注意先解绑我们本地的软连接:npm unlink,然后再使用全局下载的 gxc

解析代码

代码还是比较简单的,基本就是一步一步走下去

先来看入口文件

#!/usr/bin/env node
const commander = require('../lib/commander')
const inquirer = require('../lib/inquirer')
const build = require('../lib/run/build')

// 解析参数
commander()
  // 获取结果
  .then(inquirer)
  // 构建
  .then(build)

步骤还是比较明确的,接来下的主要步骤都在 lib文件下面,继续...

commander参数处理

主要定义了两个命令

  • gxc:下载项目
  • gxc d :删除项目

其他的:

  • gxc -v:版本
  • gxc -h(默认的):获取帮助
// 解析process.argv 参数
const { program } = require('commander')
const { version } = require('../../package.json')
const del = require('../run/delete')

module.exports = function () {
  return new Promise((resolve) => {
    // 拉取项目
    program
      .version(version, '-v, --version')
      .description('下载github项目')
      .action(resolve)

    // 删除项目
    program
      .version(version, '-v, --version')
      .command('del <projectName>')
      .alias('d')
      .description('下载github项目')
      .action((projectName) => {
        del(projectName)
      })

    program.parse(process.argv)
  })
}

inquirer设置交互命令

先看执行文件:inquirer/index.js

第一步:获取用户输入的github账户对应的所有公共仓库

第二步:抛出用户选择的仓库

const getRepos = require('./getRepos')
const chooseRepo = require('./chooseRepo')

module.exports = async function () {
  const repos = await getRepos()
  const answer = await chooseRepo(repos)
  return answer
}

注意:github API 在 githubApi/index.js中

更多api可以点击这里

getRepos

获取用户输入的github所有公共仓库

  1. 检查之前是否输入过github账户名,如果为否则走第二步,为是则询问用户是否沿用之前的账户名,用户回答是则直接走第三步,用户回答否则走第二步
  2. 要求用户输入账户名,拿到结果走下一步
  3. 拉取仓库列表,请求成功就创建缓存用户名和仓库文件,请求失败则先判断用户是否沿用账户名,如果是则获取上次缓存的仓库文件为结果返回,否则进入下面的错误处理
const inquirer = require('inquirer')
const { repos } = require('../../githubApi')
const util = require('../../util')
const chalk = require('chalk')
const ora = require('ora') // loading效果

// 获取仓库值
module.exports = () => {
  let useOldUser = false; // 是否沿用之前账户名

  return new Promise((resolve) => {
  
    // 第一步
    util.fileIsExist('user.txt')
      .then(() => {
        // 询问是否需要换账户
        inquirer.prompt([{
          type: 'confirm',
          name: 'change',
          message: '监测到您之前的输入过账户名,是否沿用之前账户名?'
        }]).then((answer) => {
          if (useOldUser = answer.change) {
            util.readFile('user.txt').then((user) => {
              requestRepos({ user: user.toString() })
            })
          } else {
            getUser(requestRepos)
          }
        })
      })
      .catch(getUser.bind(null,requestRepos))
      
    // 第二步
    // 获取用户名
    function getUser (cb) {
      inquirer.prompt([{
        type: 'input',
        name: 'user',
        message: '请输入的您的github账户名'
      }]).then(cb)
    }
    
    // 第三步
    // 获取仓库列表
    function requestRepos (answer) {
      const spinner = ora(chalk.yellow('正在拉取项目,请稍等...')).start()
      repos(answer.user)
      .then((res) => {
        spinner.stop('')
        // 判断是否有仓库存在
        if (res.length) {
          resolve(res)
        } else {
          util.consoleBlue('您没有项目可以下载!')
          process.exit(1)
        }
        // 缓存用户名和仓库列表
        util.createFile('user.txt', answer.user)
        util.createFile('repos.txt', JSON.stringify(res))
      })
      .catch(err => {
        spinner.stop('')
        // 尝试获取之前的json输出
        if (useOldUser) {
          util.fileIsExist('repos.txt').then(() => {
              util.readFile('repos.txt').then((res) => {
                resolve(JSON.parse(res))
              }).catch(err => {
                util.consoleRed(err.toString())
                process.exit(1)
              })
            })
        }
        if (/connect ECONNREFUSED | connect ECONNRESET/.test(err.toString())) {
          util.consoleRed(`\n Github当前网络连接不上,请过一会重试!\n ${err.toString()}`)
        } else {
          util.consoleRed(err.toString())
          process.exit(1)
        }
      })
    }
  })
}

chooseRepo

拿到用户选择的仓库,抛出

const inquirer = require('inquirer')
const util = require('../../util')

module.exports = function (repos) {
  // 处理成prompt需要的格式
  const choices = repos.map(item => ({
    name: item.name + ':' + item.description,
    value: item.clone_url
  }))

  return new Promise((resolve) => {
    inquirer.prompt([
      {
        type: 'list',
        name: 'url',
        message: '请选择github项目',
        choices
      }
    ]).then((answers) => {
      const projectName = choices.filter(item => item.value === answers.url)[0].name
      util.consoleBlue(`您即将要下载的项目是:${projectName}`)
      resolve(answers)
    }).catch(error => {
      if(error.isTtyError) {
        util.consoleRed('无法在当前环境中呈现提示')
      } else {
        util.consoleRed(error.toString())
      }
      process.exit(1)
    })
  })
}

build构建下载仓库

文件路径在 command/build.js

用child_process 开启一个子进程,运行git clone 下载

// 执行下载
const exec = require('child_process').exec
const ora = require('ora') // loading效果
const chalk = require('chalk')
const util = require('../../util')

module.exports = function (answers) {
  const spinner = ora(chalk.yellow('正在下载...')).start()
  const buildExec = exec(`git clone ${answers.url}`, (err) => {
    if (err) {
      spinner.fail(chalk.red('下载中断'))
      util.consoleRed(err.toString())
      process.exit(1)
    } else {
      spinner.succeed(chalk.green('下载完成!'))
    }
  })
}

总结

至此结束,了解了一些工具模块的功能使用,以及自己实现了一个脚手架,感觉挺有成就感的。

参考

《前端那些事》从0到1开发简单脚手架

前端如何搭建一个成熟的脚手架

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