仿vue-cli写一个简易的脚手架

仿vue-cli写一个简易的脚手架

仿vue-cli写一个简易的脚手架

实际生产中搭建一个脚手架或者阅读其他脚手架源码的时候需要了解下面这些工具库

名称 简介  文档
commander 命令行自定义指令 点击查看文档
inquirer 命令行询问用户问题,记录回答结果 点击查看文档
chalk 控制台输出内容样式美化 点击查看文档
ora 控制台loading 样式  点击查看文档
figlet 控制台打印 logo 点击查看文档
easy-table 控制台输出表格  点击查看文档
download-git-repo 下载远程模版  点击查看文档
fs-extra 系统fs模块的扩展,提供了更多便利的 API,并继承了fs模块的 API  点击查看文档
cross-spawn支 持跨平台调用系统上的命令 点击查看文档

 

1. 创建项目

参照前面的例子,先创建一个简单的 Node-Cli 结构

 

配置脚手架启动文件

{
    "name": "peach-cli",
    "version": "1.0.0",
    "description": "脚手架",
    "main": "index.js",
    "bin": {
        "pec": "./bin/cli.js" //// 配置启动文件路径,pec 为别名
    },
    "scripts": {
        "compile": "babel src -d dist",
        "watch": "npm run compile -- --watch"
    },
    "author": {
        "name": "songxiaotao",
        "email": "jinqingemail@163.com"
    },
    "license": "MIT"
 
}

 为了方便开发调试,使用 npm link 链接到全局

 简单编辑一下我们的 bin/cli.js

#! /usr/bin/env node

console.log('~~~~~ peach-cli working ~~~~~')

2. 创建脚手架启动命令

首先我们要借助 commander 依赖去实现这个需求

2.1 安装依赖

$ npm install commander --save

2.2 创建命令

打开 cli.js 进行编辑

console.log('~~~~~ peach-cli working ~~~~~')
const program = require('commander')
const chalk = require('chalk');
var figlet = require('figlet');
program
//定义命令和参数
   .command('create <app-name>')
   .description('create a new project')
   .option('-f, --force', 'overwrite target directory if it exit')
   .action((name, options)=>{
       //
       console.log('name:', name, 'option:', options)
      //  require('../utils/create.js')(name, options)
   })

program
  .version(`v${require('../package.json').version}`)
  .usage('<command> [option]')
// 

// 配置 config 命令

program
//定义命令和参数
   .command('config [value]')
   .description('inspect and modify the config')
   .option('-g, --get <path>', 'get value from option')
   .option('-s, --set <path> <value>')
   .option('-d, --delete <path>', 'delete option from config')
   .action((value, options)=>{
       //
      //  console.log(value, options)
       
})

  program.parse(process.argv)

在命令行输入 pec,检查一下命令是否创建成功

 我们可以看到 Commands 下面已经有了 create [options] <app-name>,接着执行一下这个命令

 成功拿到命令行输入信息

继续补充

2.3、完善帮助信息

对比 pec --help 打印的结果,结尾处少了一条说明信息,这里我们做补充,重点需要注意说明信息是带有颜色的,这里就需要用到我们工具库里面的 chalk 来处理

// bin/cli.js

program
  // 监听 --help 执行
  .on('--help', () => {
    // 新增说明信息
    console.log(`\r\nRun ${chalk.cyan(`pec <command> --help`)} for detailed usage of given command\r\n`)
  })

2.4 打印个 Logo

如果此时我们想给脚手架整个 Logo,工具库里的 figlet 就是干这个的 

 

program
  .on('--help', () => {
    // 使用 figlet 绘制 Logo
    console.log('\r\n' + figlet.textSync('peach song', {
      font: 'Ghost',
      horizontalLayout: 'default',
      verticalLayout: 'default',
      width: 80,
      whitespaceBreak: true
    }));
    // 新增说明信息
    console.log(`\r\nRun ${chalk.cyan(`pec <command> --help`)} show details\r\n`)
  })

  program.parse(process.argv)

 

pec --help 打印出来的是个什么样子

 完整代码:

#! /usr/bin/env node

const program = require('commander')
const chalk = require('chalk');
var figlet = require('figlet');
program
//定义命令和参数
   .command('create <app-name>')
   .description('create a new project')
   .option('-f, --force', 'overwrite target directory if it exit')
   .action((name, options)=>{
       //
       console.log('name:', name, 'option:', options)
      
   })

program
  .version(`v${require('../package.json').version}`)
  .usage('<command> [option]')
// 

// 配置 config 命令

program
//定义命令和参数
   .command('config [value]')
   .description('inspect and modify the config')
   .option('-g, --get <path>', 'get value from option')
   .option('-s, --set <path> <value>')
   .option('-d, --delete <path>', 'delete option from config')
   .action((value, options)=>{
   //  console.log(value, options)
       
})
// 配置 UI命令
program
//定义命令和参数
   .command('ui')
   .description('start add open roc-cli ui')
   .option('-p, --port <port>', 'Port used for the UI Server')
   .action((options)=>{
       
      //  console.log(options)
       
})

program
  .on('--help', () => {
    // 使用 figlet 绘制 Logo
    console.log('\r\n' + figlet.textSync('peach song', {
      font: 'Ghost',
      horizontalLayout: 'default',
      verticalLayout: 'default',
      width: 80,
      whitespaceBreak: true
    }));
    // 新增说明信息
    console.log(`\r\nRun ${chalk.cyan(`pec <command> --help`)} show details\r\n`)
  })

  program.parse(process.argv)

 

2.3 执行命令

创建 utils 文件夹并在文件夹下创建 create.js

// utils/create.js

module.exports = async function (name, options) {
  // 验证是否正常取到值
  console.log('~~~ create.js', name, options)
}

在 cli.js 中使用 create.js

// bin/cli.js
...
program
//定义命令和参数 .command('create <app-name>') .description('create a new project') .option('-f, --force', 'overwrite target directory if it exit') .action((name, options)=>{ require('../utils/create.js')(name, options) })
...

执行一下 pec create song-project,此时在 create.js 正常打印了输入项目名字参数等的信息

 

 

 

3. 询问用户问题获取创建所需信息

3.1、目录是否已经存在

在创建目录的时候,需要思考一个问题:目录是否已经存在?

  1. 如果存在
    • { force: true } 时,直接移除原来的目录,直接创建
    • { force: false } 时 询问用户是否需要覆盖
  2. 如果不存在,直接创建

这里用到了 fs 的扩展工具 fs-extra,先来安装一下

# fs-extra 是对 fs 模块的扩展,支持 promise 
$ npm install fs-extra --save
// lib/create.js

const path = require('path')
const fs = require('fs-extra')

module.exports = async function (name, options) {
  // 执行创建命令

  // 当前命令行选择的目录
  const cwd  = process.cwd();
  // 需要创建的目录地址
  const targetAir  = path.join(cwd, name)

  // 目录是否已经存在?
  if (fs.existsSync(targetAir)) {

    // 是否为强制创建?
    if (options.force) {
      await fs.remove(targetAir)
    } else {
      // TODO:询问用户是否确定要覆盖
    }
  }
}

首选来安装一下 inquirer

$ npm install inquirer --save

然后询问用户是否进行 Overwrite

// utils/create.js

const path = require('path')

// fs-extra 是对 fs 模块的扩展,支持 promise 语法
const fs = require('fs-extra')
const inquirer = require('inquirer')

module.exports = async function (name, options) {
  // 执行创建命令

  // 当前命令行选择的目录
  const cwd  = process.cwd();
  // 需要创建的目录地址
  const targetAir  = path.join(cwd, name)
    fs.mkdir(`./${name}`, (err)=>{ // 创建目录 可以自动创建 也可以注释这段代码手动创建也可以
      console.log('--err--', err)
    })
// 目录是否已经存在?
  if (fs.existsSync(targetAir)) {

    // 是否为强制创建?
    if (options.force) {
      await fs.remove(targetAir)
    } 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) {
        return;
      } else if (action === 'overwrite') {
        // 移除已存在的目录
        console.log(`\r\nRemoving...`)
        await fs.remove(targetAir)
      }
    }
  }
}

 

pec create tao-project  自动创建

 已经存在,选择覆盖删除已经有的

 

执行 pec create song-project --f,可以直接看到 song-project 被移除

⚠️注意:为什么这里只做移除? 因为后面获取到模板地址后,下载的时候会直接创建项目目录

3.2 如何获取模版信息

模版远程地址已经上传仓库 github.com/peach-cli-organization

tags信息

github 提供了:

api.github.com/orgs/peach-cli-organization/repos   接口获取模板信息

api.github.com/repos/peach-cli-organizatio    接口获取版本信息

 我们在 utils目录下创建一个 http.js 专门处理模板和版本信息的获取

// utils/http.js
// 
const axios = require('axios')
axios.interceptors.response.use(res => {
    // console.log('999res.data--', res.data)
    return res.data;
})
/**
 * 获取模版列表
 * @return Promise
*/
async function getRepoList(){
    return axios.get('https://api.github.com/orgs/peach-cli-organization/repos')
}
/**
 * 获取版本信息
 * @param {string} repo 模版名称
 * 
*/
async function getTagList(repo){
    return axios.get(`https://api.github.com/repos/peach-cli-organization/${repo}/tags`)
}

module.exports = {
    getRepoList,
    getTagList
}

3.3、用户选择模板

我们专门新建一个 Generator.js 来处理项目创建逻辑

// utils/Generator.js

class Generator {
  constructor (name, targetDir){
    // 目录名称
    this.name = name;
    // 创建位置
    this.targetDir = targetDir;
  }

  // 核心创建逻辑
  create(){

  }
}

module.exports = Generator;

在 create.js 中引入 Generator 类

//utils/create.js

...
const Generator = require('./Generator')

module.exports = async function (name, options) {
  // 执行创建命令

  // 当前命令行选择的目录
  const cwd  = process.cwd();
  // 需要创建的目录地址
  const targetAir  = path.join(cwd, name)

  // 目录是否已经存在?
  if (fs.existsSync(targetAir)) {
    ...
  }

  // 创建项目
  const generator = new Generator(name, targetAir);

  // 开始创建项目
  generator.create()
}

3.4、下载远程模板

下载远程模版需要使用 download-git-repo 工具包,实际上它也在我们上面列的工具菜单上,但是在使用它的时候,需要注意一个问题,就是它是不支持 promise的,所以我们这里需要使用 使用 util 模块中的 promisify 方法对其进行 promise 化

$ npm install download-git-repo --save

 

接着来写询问用户选择模版都逻辑

// utils/Generator.js
const { getRepoList, getTagList } = require('./http')
const ora = require('ora')
const inquirer = require('inquirer');
const util = require('util')
const path = require('path')
const downloadGitRepo = require('download-git-repo');
const chalk = require('chalk');

//开始添加动画

async function wrapLoading(fn, message, ...args){
    // 使用 ora 初始化,传入提示信息 message
    const spinner = ora(message);
    // 开始加载动画
    spinner.start();
    try{
        // 执行传入方法 fn
        const result = await fn(...args);
        //  状态修改为成功
        spinner.succeed();
        return result;
    } catch(error){
        // 状态修为失败
       spinner.fail('Request fail, refetch ....', error)

    }

}

class Generator{
    constructor(name, targetDir){
        // 目录名称
        this.name = name;
        // // 创建位置
        this.targetDir = targetDir;
        // 对 download-git-repo 进行 promise 化改造
        this.downloadGitRepo = util.promisify(downloadGitRepo)
    }
    // 获取用户选择的模板
    // 1)从远程拉取模板数据
    // 2)用户选择自己新下载的模板名称
    // 3)return 用户选择的名称
    async getRepo(){
        // 
        const repoList = await wrapLoading(getRepoList, 'wait fetch template')
        if(!repoList) return;
        const repos = repoList.map(item => item.name)
       
        const { repo } = await inquirer.prompt({
           name: 'repo',
           type: 'list',
           choices: repos,
           message: 'Please choose a template to create project' 
        })
        //
        return repo
    }
   // 获取用户选择的版本
  // 1)基于 repo 结果,远程拉取对应的 tag 列表
  // 2)用户选择自己需要下载的 tag
  // 3)return 用户选择的 tag

  async getTag(repo) {
    // 1)基于 repo 结果,远程拉取对应的 tag 列表
    const tags = await wrapLoading(getTagList, 'waiting fetch tag', repo);
    if (!tags) return;
    
    // 过滤我们需要的 tag 名称
    const tagsList = tags.map(item => item.name);

    // 2)用户选择自己需要下载的 tag
    const { tag } = await inquirer.prompt({
      name: 'tag',
      type: 'list',
      choices: tagsList,
      message: 'Place choose a tag to create project'
    })

    // 3)return 用户选择的 tag
    return tag
  }
    // 下载远程模板
    // 1)拼接下载地址
    // 2)调用下载方法
    async download(repo, tag){
        // 1)拼接下载地址
        const requestUrl=`peach-cli-organization/${repo}${tag?'#'+tag:''}`;
      
        // // 2)调用下载方法
        await wrapLoading(
            this.downloadGitRepo, // 
            'waiting download template', // 
            requestUrl, //
            path.resolve(process.cwd(), this.targetDir) // 
        )
      
    }

    // 核心创建逻辑
    async create(){
       const repo = await this.getRepo()
        // 2) 获取 tag 名称
        const tag = await this.getTag(repo)
            // 3)下载模板到模板目录
        await this.download(repo, tag)
        // console.log('create-----getloadRes', getloadRes)
        console.log('用户选择了,repo=' + repo + ',tag='+ tag)
        // 4)模板使用提示
        console.log(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`)
        console.log(`\r\n  cd ${chalk.cyan(this.name)}`)
        console.log('  npm run dev\r\n')

    }
}

module.exports = Generator;

 

 

 

4、npm发布

创建或验证<username>

首先执行下npm adduser,

输入相应的

Username、

Password、

Email: (this IS public)

 

·······Logged in as 您的Username on https://registry.npmjs.org/.

如果on后面不是https://registry.npmjs.org/,而是其他的镜像,比如我们大家常见的淘宝镜像
······ http://registry.npm.taobao.org/

那么首先替换成原来的,替换成原来执行如下命令:
·······npm config set registry https://registry.npmjs.org/

最后,替换完毕再执行npm adduser、npm publish

邮箱记得验证否则报错: 

 403 Forbidden - PUT https://registry.npmjs.org/peach-cli - You do not have permission to publish "peach-cli". Are you logged in as the correct user?

package 的包也不能重复

发布
"name": "song-peach-cli",

验证

打开npmjs官网

 测试

 终于可以成功了哈

 

github.com 查看源码

注意:关于模版下载相关知识点:

1、接口模版的和tag的接口

建立一个自己organization项目组管理模版

【手把手撸一个脚手架】第三步, 获取 github 项目信息

一篇文章搞定Github API 调用 (v3)

 2、打tag以及上传远程地址

打tag以及上传远程地址

posted @ 2021-09-18 15:09  pikachuWorld  阅读(389)  评论(0编辑  收藏  举报