搭建自己的脚手架

·  阅读 68

背景

做新的项目时,每次复制文件夹,然后修改package.json、README.md等,感觉很不“优雅”,想使用类似vue-cli,使用vue init的方式在github下载我自己的前端工程,这样显得很“优雅”。

初始化项目结构

首先你已经有了自己搭建的前端工程,也就是模板,假设起名为x-liu,并且已经上传到github。 此时新建一个新的项目,起名为x-liu-cli,我是参考vue的做法,这样即使x-liu更新,x-liu-cli不更新,也可以拉取到最新的x-liu

mkdir x-build-cli 
cd x-build-cli
npm init
复制代码

创建名为x-liu-cli的文件夹,使用npm初始化,在文件夹内创建bin目录,并创建x-liu-cli.js,此时的项目结构:

x-build-cli 
    |- bin 
    | |- x-build.js 
    |- package.json
复制代码

配置package.json

"bin": { "x-liu": "./bin/x-liu-cli.js" }
复制代码

在package.json增加"bin","x-liu"就是命令号要输入的指令,"./bin/x-liu-cli.js"是命令执行时的文件。 而发布到npm后是执行npm install x-liu-cli -g

配置x-liu-cli.js

#! /usr/bin/env node const program = require('commander');
const download = require('download-git-repo'); 
const chalk = require('chalk');
const ora = require('ora');
const inquirer = require('inquirer')
const fs= require('fs-extra')
复制代码

#! /usr/bin/env node是指定这个文件使用node执行。

需要安装的模块npm i commander download-git-repo chalk ora --save:

commander可以解析用户输入的命令。

download-git-repo拉取github上的文件。// 不支持 Promise 可以借助util.promisify

chalk改变输出文字的颜色

ora小图标(loading、succeed、warn等) fs fs.existsSync(targetAir) 目录是否已经存在 inquirer 询问用户并让用户做出选择

完整的x-liu-cli

#! /usr/bin/env node
const program = require('commander')

const chalk = require('chalk')
const figlet =require('figlet')
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((option) => {
        console.log(option)
    })

// 定义命令和参数
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)
        require('../lib/create')(name, options)
    })

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

program
    .on('--help', () => {
        // 使用 figlet 绘制 Logo
        console.log('\r\n' + figlet.textSync('x-build', {
            font: 'Ghost',
            horizontalLayout: 'default',
            verticalLayout: 'default',
            width: 80,
            whitespaceBreak: true
        }));
        // 新增说明信息
        console.log(`\r\nRun ${chalk.cyan(`roc <command> --help`)} show details\r\n`)
    })
// 解析用户执行命令传入参数
program.parse(process.argv);

复制代码

lib目录

lib 
    |- cteate.js 
    |- Generator.js 
    |- http.js
复制代码

create.js 内容

主要是判断是否覆盖原来的文件

const path= require('path')
const fs= require('fs-extra')
const inquirer = require('inquirer')
const Generator = require('./Generator')


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 {
  
        // 询问用户是否确定要覆盖
        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)
        }
      }
    }

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

  // 开始创建项目
  generator.create()
  }
复制代码

generator

选择之前预定好的模板,并远程下载


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') // 不支持 Promise
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 failed, refetch ...')
    }
}
class Generator {
    constructor(name, targetDir) {
        this.name = name
        this.targetDir = targetDir
        // 改造 download-git-repo 支持 promise
        this.downloadGitRepo = util.promisify(downloadGitRepo);
        // this.downloadGitRepo = downloadGitRepo;
    }
    // 请求接口拿到自己定义的模板
    // 从远程拉取模板数据
    // 用户选择自己想下载的模板名称
    // return 用户选择的名称
    async getRepo() {
        let repoList = await wrapLoading(getRepoList, 'waiting fetch template')
        if (!repoList) return;
        // 过滤我们需要的模板名称filter
        const repos = repoList.filter(i => i.is_template).map(item => item.name);
        // 2)用户选择自己新下载的模板名称
        const { repo } = await inquirer.prompt({
            name: 'repo',
            type: 'list',
            choices: repos,
            message: 'Please choose a template to create project'
        })

        // 3)return 用户选择的名称
        return repo;
    }
    // 下载远程模板
    // 1)拼接下载地址
    // 2)调用下载方法
    async download(repo, tag) {
        // 1)拼接下载地址
        // const requestUrl = `zhurong-cli/${repo}${tag?'#'+tag:''}`;
        const requestUrl = `json1204/${repo}`;
        // 2)调用下载方法
        await wrapLoading(
            this.downloadGitRepo, // 远程下载方法
            'waiting download template', // 加载提示信息
            requestUrl, // 参数1: 下载地址
            path.resolve(process.cwd(), this.targetDir)) // 参数2: 创建位置

    }
    async create() {
        // 1)获取模板名称
        const repo = await this.getRepo()

        // 2) 获取 tag 名称
        // const tag = await this.getTag(repo)

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

        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
复制代码

http

axios 请求github 的api拿到远程的模板 github api文档

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

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

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

module.exports = {
  getRepoList,
  getTagList
}

复制代码

上传npm

没有账号的同学去npm注册一个账号。

// 登录账号
npm login
// 上传项目
npm publish
复制代码
复制代码

上传成功之后,通过npm install x-liu-cli -g安装到全局环境中。

image.png

输入 x-liu create my

image.png

使用x-liu init [项目名]就可以从github拉取相应的文件。

到此脚手架就完成

结语

源码

参考

搭建自己的脚手架—“优雅”生成前端工程 从 0 构建自己的脚手架/CLI知识体系(万字) 从 16 个方向逐步搭建基于 vue3 的前端架构

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改