给我们小团队撸个脚手架 —— 通过脚手架下载模板

在第一篇文章【手把手撸脚手架 —— 初识脚手架】中,我们已经搭建了一个简易的脚手架框架,现在我们就实现通过脚手架来下载模板创建一个项目,并把这个脚手架发布到npm。

commander

在之前我们用的是yargs,今天我们换commander来试试:

yarn add commander
复制代码
const commander = require('commander');
const pkg = require("../package.json");

const program = new commander.Command();

program
  .version(pkg.version)
  .parse(process.argv)
复制代码

image.png

至于yargs和commander的区别,大家可以自行google,对我而言就像是js和jquery,commander要简洁一点。

yargscommander的区别不大,可以去查看官方文档。我在文末也附上一些基础用法,可自行选择查看。

inquirer

我们在使用vue-cli的过程中,一定都见过这个询问界面,作为脚手架和用户通讯的操作方式,这就是用inquirer实现的。

image.png

yarn add inquirer
复制代码
const inquirer = require('inquirer');
inquirer
  .prompt([
    {type: 'input', message: 'your name', name:'yourName', default: 'finget', 
         validate: function(v) {
            return v.startsWith('f')
        }
    },
    {type: 'rawlist', message:'选择性别', name:'sex',  choices: ['男','女'], default:'男'},
    {type: 'checkbox', message:'选择技术栈', name:'skill',  choices: [
        {key:'vue', value:'vue'},
        {key:'react', value:'react'},
        {key:'javascript', value:'js'},
        {key:'html', value:'h5'},
        {key:'css', value:'css'},
    ]}
  ])
  .then(answers => {
    // Use user feedback for... whatever!!
    console.log(answers);
  })
  .catch(error => {
          console.log(error)
    if(error.isTtyError) {
      // Prompt couldn't be rendered in the current environment
    } else {
      // Something else went wrong
    }
  });
复制代码

image.png

inquirer.prompt可以接收一个数组或者对象,每一个question主要有以下几个属性可用:

  • type (input, number, confirm, list, rawlist, expand, checkbox, password, editor)
  • name 变量名称
  • message 提示信息
  • validate [Function] 验证 return true 才能回车生效
  • default 默认值
  • transform [Function] 类似placehoder,只是展示
  • filter [Function] 会改成最后输入的值
  • choices [Array] 选项

更多的属性和具体使用demo可以查看官方文档

我觉得这个inquirer在使用上还是比较简单的,大家可以把它想象成一个动态表单,type设置表单类型,name就是表单绑定的属性,choices就是下拉选项,message就是placeholder,validate就是表单验证…诸如此类!

download-git-repo下载模板

  • 新建一个bin/command-create.js文件
  • inquirer实现一个create命令的询问操作
  • semver检验版本号是否合法(这个插件用起来很简单,主要功能就是对版本后的校验比对,可以看一下官方文档
// bin/command-create.js
const inquirer = require('inquirer');
const semver = require('semver');
const download = require('download-git-repo');
const createCommand = async function(projectName, destination, cmdObj) {
  console.log(projectName, destination)
  const object = await inquirer
    .prompt([{
        type: 'input',
        name: 'projectName',
        message: '请输入项目名称',
        default: projectName,
        validate: function(v) {
          // 规则一:输入的首字符为英文字符
          // 规则二:尾字符必须为英文或数字
          // 规则三:字符仅允许-和_两种
          // Declare function as asynchronous, and save the done callback
          const done = this.async();
          // Do async stuff
          setTimeout(function() {
            if (!/^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]*|[_][a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])*$/.test(v)) {
              done(`请输入合法名称:
                  规则一:输入的首字符为英文字符
                  规则二:尾字符必须为英文或数字
                  规则三:字符仅允许-和_两种
                `);
              return;
            }
            done(null, true);
          }, 0);
        },
        filter: (v) => {
          return v
        }
      },
      { type: 'input', message: '请输入作者名称', name: 'authorName' },
      { type: 'input', message: '请输入版本号', name: 'version',
        default:'1.0.0',
        validate: function(v) {
          const done = this.async();
          // Do async stuff
          setTimeout(function() {
            if (!!!semver.valid(v)) {
              done(`请输入合法版本号`);
              return;
            }
            done(null, true);
          }, 0);
        },
        filter: (v) => {
          if(semver.valid(v)) {
            return semver.valid(v)
          } else {
            return v
          } 
        } 
      },
    ])
  console.log(object)
}
module.exports = createCommand;
复制代码

这里我们简单获取三个值:projectName,version,authorName.

// bin/index.js
const createCommand = require('./command-create.js')
// command 注册命令
const create = program.command('create [projectName]'); // <必填> [可选]

create
  .description('create a project')
  .action((projectName, destination, cmdObj) => {
    createCommand(projectName, destination, cmdObj)
  })
复制代码

image.png

在拿到这些信息后我们就可以下载模板了,我们这里使用的是vue3+elementPlus+ts写的一个后台系统模板github.com/FinGet/vue3…

const createCommand = async function(projectName, destination, cmdObj) {
  // console.log(projectName, destination)
  const projectInfo = await inquirer
    .prompt([{
     ...
     ....
  downloadTemplate(projectInfo);
}
function downloadTemplate(projectInfo) {
  if(projectInfo.projectName) {
    download('github:FinGet/vue3_elementPlus_ts',projectInfo.projectName, function (err) {
      if(!err) {
        console.log('下载完成!');
      } else {
        console.log('下载失败!');
      }
    }) 
  }
}
复制代码

到这里这个脚手架基本就算完成了,可以下载模板了。接下来做点优化!

写入projectInfo

我们用inquirer拿到了用户自定义的项目信息,这时我们就该把对应信息写入package.json中。

  • fs.readFile读取package.json
  • 修改对应字段
  • fs.writeFile重写package.json
function editFile({ version, projectName, authorName }) {
  console.log(version, projectName, authorName)
  return new Promise((resolve, reject) => {
      // 读取文件
    fs.readFile(`${process.cwd()}/${projectName}/package.json`, (err, data) => {
      if (err) throw err;
      // 获取json数据并修改项目名称和版本号
      let _data = JSON.parse(data.toString())
      _data.name = projectName;
      _data.version = version;
      _data.author = authorName;
      let str = JSON.stringify(_data, null, 4);
      // 写入文件
      fs.writeFile(`${process.cwd()}/${projectName}/package.json`, str, function (err) {
        if (err) throw err;
      })
      resolve()
    });
  })
};
复制代码

ora做一点小动画

ora都是一些简单的api调用,就可以实现动画效果,可自行查看www.npmjs.com/package/ora

yarn add ora
复制代码
const ora = require('ora');

function downloadTemplate(projectInfo) {
  const spinner = ora('正在下载模板...').start();
  const {projectName, version,authorName} = projectInfo;
  if(projectName) {
    download('github:FinGet/vue3_elementPlus_ts',projectName, function (err) {
      if(!err) {
        editFile({version, projectName, authorName }).then(() => {
          spinner.succeed('下载完成');
        });
      } else {
        spinner.fail(`下载失败!`);
      }
    }) 
  }
}
复制代码

image.png

发布脚手架

发布脚手架比较简单,只需要执行npm publish就好了。

image.png

注意事项:

  • 没有npm账号的话需要去官网www.npmjs.com/ 注册一个
  • 第一次发布插件,需要登录npm login
  • 插件的名称就是package.json中的那么,如果发布失败,可能是名称冲突了。

发布成功之后就可以用npm安装然后使用了!

image.png

附录:commander基础用法

name,usage,version,option,parse

option('-n, --name <items1> [items2]', 'name description', 'default value')

// 手动实例化一个commander
const program = new commander.Command();

program
  .name(Object.keys(pkg.bin)[0])
  .usage("<command> [options]")
  .version(pkg.version)
  .option('-d, --debug', '是否开启调试模式', false)
  .option('-e, --env <envName>', '获取环境变量名称')
  .parse(process.argv)
  
const options = program.opts();
console.log(options.debug);
复制代码

image.png

commander

commander命令注册有两种方式:

  • Command API 注册命令
  • addCommand API 注册命令

command API 注册命令

// command 注册命令
const clone = program.command('clone <source> [destination]'); // <必填> [可选]

clone
  .description('clone a repository')
  .option('-f, --force', '是否强制克隆')
  .action((source, destination, cmdObj) => {
    console.log(source, destination, cmdObj)
  })

program.parse(process.argv)
复制代码

image.png

addCommand API 注册命令

// addCommand 注册子命令
const service = new commander.Command('service');

service
  .command('start [port]')
  .action((port) => {
    console.log(port)
  })
service
  .command('stop [port]')
  .action((port) => {
    console.log(port)
  })
program.addCommand(service);
复制代码

这里执行的是test-cli service start 8080

image.png

它相当于是 又在my-cli下生成了一个新的service子命令,有分组的那种感觉,可以把同一类命令,放在一个子命令下。但是多个命令不能链式的注册!

监听未知命令

program.on('command:*', function(obj) {
  console.log(obj);
  console.error('未知的命令:' + obj[0])
  const availableCommands = program.commands.map( cmd => cmd.name())
  console.log('可用命令:'+ availableCommands.join(','))
})
复制代码

image.png

最后

本文只是实现了一个小小的模板下载工具,还有很多地方可以优化,例如:

  • 判断当下载目录是否已存在同名文件
  • 实现强制下载
  • debug 模式
  • 像vue-cli一样安装其他的插件
  • 怎么让项目自动安装依赖并启动

放上仓库地址gitee.com/finget/test…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享