一,为什么要搭建?

一般我们构建项目,很多时候都是去github或者码云找一套比较干净的模板进行二次开发,找来的模板中很多时候并不完全是符合我们个人需求的,要删除某些文件或者配置,或者缺少一些自己常用的依赖和方法,所以,这个时候,我们就可以通过构建一套自己的脚手架,在每次需要使用到的时候,直接在命令行运行自己脚手架的命令安装,就能得到符合自己的项目开发环境。

搭建脚手架的目的就是快速的搭建项目的基本结构并提供项目规范和约定。目前日常工作中常用的脚手架有 vue-cli、create-react-app 等等,都是通过简单的初始化命令,完成内容的快速构建。

个人梳理脚手架运行流程
  1. 通过命令行,询问获取用户的选项或者输入
  2. 根据用户的输入,去拉取远端仓库准备好的模板文件
  3. 下载模板文件,在用户当前文件位置生成对应项目文件

二,搭建自己的脚手架

1 , 初始化项目

在计算机某一磁盘内,比如在 D盘

D: mkdir yuobey-cli

D: cd yuobey-cli

# 生成 package.json 文件

yuobey-cli: npm init

这个时候会询问你构建项目的一些基本配置,如果需要直接生成 输入 npm init -y 即可

这个时候,会在yuobey中新建出一个package.json文件,我们对该文件进行一些改动

{
  "name": "yuobey-cli",
  "version": "1.0.0",
  "description": "a pages vue project",
  "main": "index.js",
  "bin": {
    "yuobey": "./bin/cli.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "tanqf",
  "license": "ISC"
}

上面代码中,我们把入口文件cli.js放到了bin目录下,并定义了yuobey 为别名,所以我们新建bin目录,在bin/cli.js中添加代码

//cli.js
#! /usr/bin/env node
console.log('🎉 welcome to use yuobey-cli create yuor pages vue project !')

使用 npm link 链接到全局 直接在当前目录命令行中输入 npm link

image.png

这个时候,我们可以根据提示,去本地文件中看一下生成的文件
image.png

进入node_modules,可以看到你的cli文件

image.png

这个时候,npm 已经帮我们把你定义的yuobey 别名添加为可以运行的命令脚本
在命令行中输入看一下效果
image.png

2, 安装自定义命令行指令 commander

npm install commander # yarn add commander

在bin/cli.js中添加代码

#! /usr/bin/env node
console.log('🎉 welcome to use yuobey-cli create yuor pages vue project !')
const program = require('commander');

program
  // 定义命令和参数
  .command('create <app-name>') 
  .description('create a new project')
  // -f or --force 为强制创建,如果创建的目录存在则直接覆盖
  .option('-f, --force', 'overwrite target directory if it exist')
  .action((name, options) => {
    // 打印执行结果
      console.log('name:',name,'options:',options)
  })

program
  // 配置版本号信息
  .version(`v${require('../package.json').version}`)
  .usage('<command> [option]')

// 解析用户执行命令传入参数
program.parse(process.argv);

命令行输入打印查看效果
image.png

如果需要带参数 ,如 加上 -f 或者 —force查看效果

image.png

3, 使用inquirer处理用户跟命令行间的交互问题

npm install inquirer

在lib下创建create.js文件文件

inquirer 支持promise方式调用,看以下代码

const path = require('path')

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

// 解决命令行交互的问题
const inquirer = require('inquirer')

//引入文件创建逻辑文件
const Generator = require('./Generator')

module.exports = async function (name, options) {
  // 验证是否正常取到值
  console.log('>>> create.js', name, options)
  // 执行创建命令

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

  //判断目录是否存在
  if (fs.existsSync(targetDir)) {
    //是否强制创建
    if (options.force) {
      await fs.remove(targetDir)
    } else {
      //询问用户是否覆盖
      let { action } = await inquirer.prompt([
        {
          name: 'action',
          type: 'list',
          message: '目标目录已经存在。选择一个操作:',
          choices: [
            {
              name: '删除',
              value: 'overwrite',
            },
            {
              name: '取消',
              value: false
            }
          ]
        }
      ])
      if(!action){
        return
      }else{
        //移除已经存在的目录 
        console.log('\r\删除中...')
        await fs.remove(targetDir)
      }
    }
  }

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

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

在cli.js中使用,查看效果

program
  // 定义命令和参数
  .command('create <app-name>') 
  .description('create a new project')
  // -f or --force 为强制创建,如果创建的目录存在则直接覆盖
  .option('-f, --force', 'overwrite target directory if it exist')
  .action((name, options) => {
    // 打印执行结果
         require("../lib/create.js")(name,options)
  })

上面代码中,使用到了fs的扩展 fs-extra ,来进行对文件的操作

npm install fs-extra

执行查看一下效果:
image.png

看上图,由于执行命令前,我先创建了demo文件夹,所以会提示选择删除操作。

4, github创建项目放置模板

github上创建项目,然后点击发布版本,发布版本的时候会提示创建标签,跟着操作,或者百度一下就可以了。
image.png

我们看一下版本

image.png

github 提供了对应api 获取releasestags

tags

[](https://api.github.com/repos/TQF980522/vue-pages-project/tags)https://api.github.com/repos/TQF980522/vue-pages-project/tags

releases

[](https://api.github.com/orgs/TQF980522/repos)https://api.github.com/orgs/TQF980522/repos

5, cli拉取github项目到本地

在lib下创建generator.js和http.js文件

其中http.js 主要是获取版本和标签的api文件,需要先安装axios

npm install axios
// lib http 

// 通过 axios 处理请求
const axios = require('axios')

axios.interceptors.response.use(res => {
  return res.data;
})

/**
 * 获取模板列表
 * @returns Promise
 */
async function getRepoList() {
  return axios.get('<https://api.github.com/orgs/TQF980522/repos>')
}

/**
 * 获取版本信息
 * @param {string} repo 模板名称
 * @returns Promise
 */
async function getTagList(repo) {
  return axios.get(`https://api.github.com/repos/TQF980522/${repo}/tags`)
}

module.exports = {
  getRepoList,
  getTagList
}

接着我们创建generator.js 文件来处理项目创建的逻辑

首先我们封装一个加载loading的方法

// lib/Generator.js
// 处理项目创建逻辑
const { getRepoList, getTagList  } = require('./http')
//加载动画需要引入的类,node本身自带,若无请自行安装
//https://www.npmjs.com/package/ora
const ora = require('ora')

// 添加加载动画
async function generatorLoading(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 failed, refetch ...')
  }
}

接着,我们创建一个类,对创建项目进行处理

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

    // 对 download-git-repo 进行 promise 化改造
    this.downloadGitRepo = util.promisify(downloadGitRepo);
  }

    //创建逻辑
  // 1)获取模板名称
  // 2)获取 tag 名称
  // 3)下载模板到模板目录
  async create(){
    // 1)获取用户选择的模板名称
    const repo = await this.getRepo()
    console.log('用户选择了,repo=' + repo)
    // 2) 获取 tag 名称
    const tag = await this.getTag(repo)
    console.log('用户选择了,repo=' + repo + ',tag=' + tag)

    // 3)下载模板到模板目录
    await this.download(repo, tag)

    // 4)模板使用提示
    console.log(`\r\n创建项目 ${chalk.cyan(this.name)} 成功!`)
    console.log(`\r\n  cd ${chalk.cyan(this.name)}`)
    console.log(' npm run dev\r\n')
  }
}

我们编写一下第一步获取模板名称

  // 获取用户选择的模板
  // 1)从远程拉取模板数据
  // 2)用户选择自己新下载的模板名称
  // 3)return 用户选择的名称
  async getRepo() {
    // 1)从远程拉取模板数据
    const repoList = await generatorLoading(getRepoList, 'waiting fetch template');
    if (!repoList) return;

    // 过滤我们需要的模板名称
    const repos = repoList.map(item => item.name);

    // 2)用户选择自己新下载的模板名称
    const { repo } = await inquirer.prompt({
      name: 'repo',
      type: 'list',
      choices: repos,
      message: '选择你需要创建的模板:'
    })
    // 3)return 用户选择的名称
    return repo;
  }

第二步,根据用户选择的模板名称去获取对应的版本

  // 获取用户选择的版本
  // 1)基于 repo 结果,远程拉取对应的 tag 列表
  // 2)用户选择自己需要下载的 tag
  // 3)return 用户选择的 tag
  async getTag(repo) {
    // 1)基于 repo 结果,远程拉取对应的 tag 列表
    const tags = await generatorLoading(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: '请选择创建的版本:'
    })

    // 3)return 用户选择的 tag
    return tag
  }

第三步,下载远程模板

下载远程模板我们用到download-git-repo 该插件不支持promise 我们需要使用node工具类对其进行promise化

const downloadGitRepo = require('download-git-repo') // 不支持 Promise
class Generator{
  constructor(name,targetDir){
    //目录名称
    this.name = name
    //创建位置
    this.targetDir = targetDir

    // 对 download-git-repo 进行 promise 化改造
    **this.downloadGitRepo = util.promisify(downloadGitRepo); //进行promise化**
  }
}

  // 下载远程模板
  // 1)拼接下载地址
  // 2)调用下载方法
  async download(repo, tag) {
    // 1)拼接下载地址 需要改为你正确的git地址 
    const requestUrl = `TQF980522/${repo}${tag ? '#' + tag : ''}`;
    // 2)调用下载方法
    await generatorLoading(
      this.downloadGitRepo, // 远程下载方法
      '正在下载模板...', // 加载提示信息
      requestUrl, // 参数1: 下载地址
      path.resolve(process.cwd(), this.targetDir)) // 参数2: 创建位置
  }

最后在create.js 询问用户操作完成后 调用一下generator

  // 主要代码
  //创建项目
  const generator = new Generator(name, targetDir);
  // 开始创建项目
  generator.create()

三,最后

本文很多程度上是根据 原文 中的提示操作后总结出的内容,原文有更多知识点可以学习。

另外,有一些依赖文中并没有重点讲解,分别是chalk 美化输出文本

安装 npm install chalk 文中用的版本是 "chalk": "4.1.1" 使用过高版本 可能导致 require('chalk') 失败,原因是高版本只支持import 方式引入使用 不支持require

*具体用法参考原文或者官网

另外一个插件 figlet ,支持根据输入文本打印logo,使用方法:

const figlet = require('figlet');

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

yuobey
1 声望0 粉丝