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

237 阅读2分钟

很多小伙伴一直很纠结什么是脚手架?其实核心功能就是创建项目初始文件,那么问题来了,市面上脚手架不够用?为什么还要自己写?

只要提到脚手架你就会想到,vue-cli,create-react-app,dva-cli,他们的特点不用多说那就是专一,但是在公司中你会发现以下一系列问题

  • 业务类型多
  • 多次造轮子,项目升级等问题
  • 公司代码规范,无法统一

很多时候我们开发需要新建项目,把已有项目代码复制一次,保留基础能力(但是这个过程非常琐碎而又耗时),那我们可以自己定制化模版,自己实现一个属于自己的脚手架,来解决这个问题

1.必备模块

我们先从大家众所周知的vue-cli入手,先来看看他用了那些npm包来实现

  • commander:参数解析,--help就是借助了他
  • inquirer:交互式命令行工具,有他就可以实现
  • download-git-repo 将模版下载下来

将包关联成全局

先看好目录结构

image.png image.png 1.先创建可执行的脚本 #! /usr/bin/env node

创建bin文件夹,创建zhu文件

#! /usr/bin/env node


console.log('执行了')

2.配置package.json 中bin字段

{
  "name": "zhu",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin": "./bin/zhu",
  -----

3.npm link 链接到本地环境(默认以name为基准)

  • link 相当于将当前本地模块链接到npm目录下,这个npm目录可以直接访问,所以当前包就可以直接访问了
npm link 

最后执行可以看到

image.png

2.解析命令传入的参数

我们都知道vue可以执行 vue --help,vue config,vue create等命令,那我们第一步就是来实现命令

1.先下载commander

npm install commander

2.利用commander中方法的来写一些命令,先实现版本的查看zhu -V命令

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

//解析命令传入的参数
program.parse(process.argv)

3.实现zhu create命令

program
    .command('create <app-name>')  //命令
    .description('config a new project') //描述
    .option('-f, --force', 'overwrite target if it exists') //是否强制覆盖
    .action((name, cmd) => {
        //调用create模块去实现
        require('../lib/create')(name, cmd) //去调用创建方法
        // console.log(name, cmd) //需要提取这个cmd的属性
    })

4.实现zhu config命令

program
    .command('config [value]')
    .description('inspect and modify ')
    .option('-g --get <path>', 'get value from option')
    .option('-s --set <path> <value>')
    .option('-d --delete <path>')
    .action((value, cmd) => {
        //调用config模块
        console.log(value, cmd)
    })

5.实现zhu ui命令

program
    .command('ui')
    .description('start and open zhufeng-cli')
    .option('-p --port <port>', 'Port use for UI Server')
    .action((value, cmd) => {
        console.log(value, cmd)
    })

6.我们这个时候可以通过 zhu --help来查看命令

执行了 //这是我们的测试代码
Usage: zhu <command>  [option] //这是用来描述命令的参数

Options:
  -V, --version                output the version number
  -h, --help                   display help for command

Commands: //这是我们自己写的命令
  create [options] <app-name>  config a new project
  config [options] [value]     inspect and modify
  ui [options]                 start and open zhufeng-cli
  help [command]               display help for command
Run zhufeng-cli

3.创建项目

先判断当前目录下面有重复的项目没有,有重名的项目就先删掉后,再开始创建

  • crete.js
const path = require('path')
const fs = require('fs-extra')
const Inquirer = require('inquirer')
let Creator = require('./Creator')
module.exports = async function (projectName, options) {
    console.log(projectName, options)

    const cwd = process.cwd() //获取当前命令执行的工作目录
    const targetDir = path.join(cwd, projectName)
    if (fs.existsSync(targetDir)) {
        if (options.force) { //如果强制创建,则删除
            //强制创建
            await fs.remove(targetDir)
        } else {
            //提示用户是否确定覆盖
            const { 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('Removeing')
                await fs.remove(targetDir)
            }
        }
    }
    //创建项目
    const creator = new Creator(projectName, targetDir)
    creator.create()
}

我们脚手架的创建分成3部 1.先去拉取当前组织下面的模版 2.再通过模版找到版本号 3.最后下载模版 那么我们的思路就很清楚了 先下载对应的包

npm install  download-git-repo ora axios

Creator.js

module.exports = class Creator {
    constructor(projectName, targetDir) {
        this.name = projectName
        this.target = targetDir
        //通过util.promisify转换成为promise请求
        this.downloadGitRepo = util.promisify(downloadGitRepo)
    }
    //真实开始创建
    async create() {
        // 1 先去拉取当前组织下面的模版
        let repo = await this.fetchRepo()
        console.log('repo', repo)

        //2 再通过模版找到版本号
        let tag = await this.fetchTag(repo)

        //3.下载
        let downloadUrl = await this.download(repo, tag)

    }

    async fetchRepo() {
    }
    async fetchTag(repo) {
  
    }
    async download(repo, tag) {
 
    }

}

wrapLoading 制作一个等待loading

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

async function wrapLoading(fn, message, ...args) {
    const spinner = ora(message)
    spinner.start()
    try {
        const res = await fn()
        spinner.succeed()
        return res
    } catch (error) {
        spinner.fail('Request failed, retrying')
        await sleep(1000)
        return wrapLoading(fn, message, ...args)
    }
}

request.js 拉取的地址

//通过axios来获取结果


const axios = require('axios')

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

async function fetchRepoList() {
    //
    return axios.get('https://api.github.com/orgs/zhu-cli/repos')
}
async function fetchTagList(repo) {
    return axios.get(`https://api.github.com/repos/zhu-cli/${repo}/tags`)
}

module.exports = {
    fetchRepoList,
    fetchTagList
}

fetchRepo拉取当前组织下面的模版

  async fetchRepo() {
        //失败重新获取
        let repos = await wrapLoading(fetchRepoList, 'wating fetch template')
        if (!repos) {
            return
        }
        repos = repos.map(item => item.name)
        let { repo } = await Inquirer.prompt({ //找到对应的模版下载
            name: 'repo',
            type: 'list',
            message: '请选择要下载的模版',
            choices: repos
        })
        console.log('repo', repo)
        return repo
    }

fetchTag 通过模版找到对应的版本

   async fetchTag(repo) {
        let tags = await wrapLoading(fetchTagList, 'wating fetch tag', repo)
        if (!tags) return
        tags = tags.map(item => item.name)
        let { tag } = await Inquirer.prompt({
            type: 'tag',
            name: 'list',
            message: '请选择要下载的版本',
            choices: tags
        })
        return tag
    }

download下载模版

 async download(repo, tag) {
        //1.需要拼接处一个下载路径
        let requestUrl = `zhu-cli/${repo}${tag ? '#' + tag : ''}`
        //2.把资源下载到某个路径下(后续可以增加缓存功能,应该下载到系统目录中,稍后可以在ejs handlerbar 去渲染模版 最后生成结果,再输出)
        //请求路径和目标目镜
        this.downloadGitRepo(requestUrl, path.resolve(process.cwd(), `${repo}@${tag}`))
        return this.target
    }