手撸一个自己的前端脚手架

101 阅读3分钟
背景: 为什么需要自己开发一个脚手架,vue-cli、create-react-app,dva-cli,他们的特点不用多说拿就是专一!但是在实际公司的业务中你会发现以下一系列问题!
- 业务类型多
- 多次造轮子,项目升级等问题
- 公司代码规范,无法统一
很多时候我们开发时需要新建项目,把已有的项目代码复制一遍,保留基础能力。(但是这个过程非常琐碎而又耗时)。拿我们可以自己定制化模板,自己实现一个属于自己的脚手架。来解决这些问题。

一、项目初始化

1. 新建一个文件夹my-cli

npm init -y // 初始化项目

2. 新建bin/my文件

// 注意my不用写后缀,通过#!来指定由什么程序来解析
// mac系统
#! node
console.log('my-cli')
// windows系统
#! /user/bin/env node
console.log('my-cli')

3. 修改package.json

{
  "name": "my",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin": "./bin/my",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

效果:执行my

image.png

4.起别名

// package.json中
"bin": {
    "my": "./bin/my",
    "my-cli": "./bin/my"
  },
 // 执行命令
 npm link --force

image.png

二、使用command,自定义命令

2.1 command命令初体验

// 安装
npm i commander 
// 配置基本参数
// 1. 配置可执行命令 commander
const program = require('commander')

program
  .version(`my-cli@${require('../package.json').version}`)
  .usage(`<command> [option]`)
// 解析用户执行命令传入的参数
program.parse(process.argv)

image.png

2.2 command命令设置项目名称,是否-force强行覆盖

// 2. 核心功能: 创建项目 ; 更改配置文件; UI界面
//  项目名称
program
  .command('create <app-name>')
  .description('create a new project')
  .option('-f,--force', 'overrite target directory if it exists')
  .action((name, cmd) => {
    console.log(name, cmd)
  })

image.png

2.3 command命令设置config

// 项目config -s set -g get -d delete
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, cmd) => {
    console.log(value, cmd)
  })

image.png

2.5 on help时候的提示

chalk自定义提示框颜色

// 安装chlk
npm i chalk@3.0.0
// help提示
program.on('--help', function () {
  console.log()
  console.log(`Run ${chalk.cyan('my-cli <command> --help')}  show detail`)
  console.log()
})

image.png

三、开始写具体的实现方法

3.1 如何新建项目文件夹

新建lib/create.js 这个主要是创建项目目录的,这里需要两个第三方的包,fs-extra支持异步,inquirer写命令提示框的包

// 安装
npm i fs-extra
npm i inquirer@7.0.0
// create.js
// 创建目录前的提示,是否覆盖
const path = require('path')
const fs = require('fs-extra')
const Inquirer = require('inquirer')

module.exports = async function (projectName, options) {
  // 创建目录
  const cwd = process.cwd()
  const targetDir = path.join(cwd, projectName)

  if (fs.existsSync(targetDir)) {
    if (options.force) {
      await fs.remove(targetDir)
    } 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 },
          ],
        },
      ])
      console.log(action)
    }
  }
}

if (!action === 'overwrite') {
        return
      } else {
        await fs.remove(targetDir)
      }

image.png

3.2 如何拉取创建项目的git模板

以下已vue模板为例,通过axios调用接口,获取模板,通过Inquirer给用户提供选项: 在lib/request.js下

const axios = require('axios')

axios.interceptors.response.use(res => res.data)
async function fetchRepositoryList() {
  return axios.get('https://api.github.com/orgs/zhu-cli/repos')
}

module.exports = {
  fetchRepositoryList,
}

在lib/Creator.js中

const { fetchRepositoryList } = require('./request')
const Inquirer = require('inquirer')

class Creator {
  constructor(projectName, targetDir) {
    this.name = projectName
    this.target = targetDir
  }

  async fetchRepository() {
    const repositoryList = await fetchRepositoryList()
    if (!repositoryList) return
    const repositoryNameList = repositoryList.map(item => item.name)
    let { repository } = await Inquirer.prompt({
      name: 'repository',
      type: 'list',
      choices: repositoryNameList,
      message: 'please choose a template to create project',
    })
    console.log(`choice`, repository)
  }
}

module.exports = Creator

下面就由两个vue的模板供用户选择了:

image.png

3.3 如何优化下载模板等待时间和重试机制

使用ora(5.x.x版本)开启命令行loading的效果,使用sleep和promise方式递归调用实现重试 lib/utils.js

const ora = require('ora')

async function sleep(time) {
  return new Promise((resolve, reject) => setTimeout(resolve, time))
}

async function loading(fn, message) {
  const spinner = ora(message)
  spinner.start()
  try {
    const repositoryList = await fn()
    spinner.succeed()
    return repositoryList
  } catch (error) {
    spinner.fail('request failed, refetch...')
    await sleep(1000)
    return loading(fn, message)
  }
}

module.exports = {
  loading,
}

// Creator.js
const repositoryList = await loading(fetchRepositoryList, 1000)

成功的时候,有个loading和success的✅

image.png

image.png

写错请求模板的地址就会触发重试机制了。可以手动终止,也可以自己写个计数器,多少次后不重试了,这块就自己发挥了。

image.png

3.4 如何拉载对应模板的tag号

// request.js
async function fetchTagList(repo) {
  console.log(`https://api.github.com/repos/zhu-cli/${repo}/tags`, 'tags....')
  return axios.get(`https://api.github.com/repos/zhu-cli/${repo}/tags`)
}
  async fetchTags(repo) {
    const tagList = await loading(fetchTagList, 1000, repo)
    if (!tagList) return
    const tagNameList = tagList.map(item => item.name)
    let { tag } = await Inquirer.prompt({
      name: 'tag',
      type: 'list',
      choices: tagNameList,
      message: 'please choose a tag to create  project',
    })
    return tag
  }
// Creator.js
  async download(repo, tag) {
    console.log(repo, tag)
    let requestUrl = `zhu-cli/${repo}${tag ? '#' + tag : ''}`
    await this.downloadGitRepo(requestUrl, process.cwd())
    return this.target
  }

  async create() {
    let repo = await this.fetchRepository()
    let tag = await this.fetchTags(repo)
    this.download(repo, tag)
  }
  // 找到模板, 找到tag号,使用download-git-repo的包去下载就好了
  const downloadGitRepo = require('download-git-repo')

image.png