vue|简易版vue-cli实现二三事儿|技术点评

436 阅读5分钟

前言

创建一个脚手架,本质上就是实现一个全局包,只不过其职能是根据配置项创建项目

实现思路

  1. 配置可执行命令 可以基于commander
  2. 要实现脚手架 先做一个命令行交互的功能 可以基于inquirer
  3. 项目模板下载 可以基于 download-git-repo
  4. 根据用户的选择动态的生成内容 可以基于metalsmith

具体实现

首先构建npm包的基本配置
创建可执行的脚本 并指明运行环境是node

/bin/start.js

#! /usr/bin/env node
将项目执行脚本指向创建的脚本

配置package.json中的bin字段

将包链接到全局下,以直接用命令方式执行
npm link

此时 这个包就变成了全局包,执行命令为name属性对应的值,执行时会默认执行指定的脚本文件

继而配置可执行命令

安装依赖

npm i command

在执行脚本start.js中实现逻辑

// 1、配置可执行命令 commander 
const program  = require('commander');


// 配置文件相关
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,opts)=>{
            console.log(opts);
            // console.log(value,cleanArgs(cmd));
        })

// 创建ui
program
        .command('ui')
        .description('start and open wzy-cli ui')
        .option('-p,--port <port>','Port used for the UI Server')
        .action(cmd => {
            console.log(cmd);
        })
// 查看版本  `wzy-cli --version`
program
        .version(`zhufeng-cli ${require('../package.json').version}`)
        .usage(`<command> [option]`)

// 当用户输入`wzy-cli --help`时 执行回调 提示用户可以查看具体命令详情
program.on('--help',function (){
    console.log();
    console.log(`Run ${chalk.cyan('wzy-cli <command> --help ')} show details>`);
    console.log();
})

// 解析命令执行的参数
program.parse(process.argv);

中间如果存在交互式配置,比如新建项目时目标文件路径已经存在,则需问询用户是否进行覆盖,则可以使用inquier

npm i inquier

start.js中逻辑

const path = require('path');
const fs = require('fs-extra');
const Inquirer = require('inquirer');

// 需要支持强制创建功能  创建项目
program
        .command('create <app-name>')
        .description('create a new project')
        .option('-f, --force','overwrite target directory if it exists')
        .action((name,opts) => {
            const cwd = process.cwd(); // 获取当前命令执行时的工作目录
            const targetDir = path.join(cwd,projectName); // 目标目录
            async function overWrite() {
                 // 先移除
                return await fs.remove(targetDir)
            }
            // 首先判断项目是否已经存在
            if(fs.existsSync(targetDir)){
                // 继而判断是否是强制覆盖模式
                if(opts.force){
                   await overWrite()
                }else {
                    // 提示用户是否确定覆盖
                    let {action} = await Inquirer.prompt([
                        {
                            name: 'action',
                            type: 'list', // 还有很多其他类型
                            message: 'Target directory already exited Pick an action',
                            choices: [
                                {
                                    name: 'Overwrite', value: 'overwrite'
                                },
                                {name: 'cancel', value: false}
                            ]
                        }
                    ]);

                    console.log(action);
                    if(!action){
                        return
                    }else if(action === 'overwrite') {
                        await overWrite()
                    }
                }
            }

        })
功能完善:选择指定模板(仓库和tag)

架子已经搭建起来了,其实我们这时候就是在不同的命令下去执行不同的逻辑处理,重点关注实现

  • 创建项目以及根据用户的选择动态的生成项目的内容

承接上文,我们很自然能想到,目前只需要实现下载模板的逻辑就可以了,我们可以抽离出来一个创建类,用于下载模板项目,大体思路如下:

// 1. 先拉取当前组织下的模板
		let repo = await this.fetchRepo();
// 2. 在通过模板找到版本号  
		let tag = await this.fetchTag();

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

// 4. 编译模板

相应的,我们只需要找到模板项目存储的平台对外暴露的API进行下载即可,中间需要做两步

  • 实现选择功能,下载模板列表后,让用户自行选择下载哪个模板
  • 实现失败重发功能,下载时可能遇到网络等不可控影响导致下载失败,此时要添加loading态并重新发起下载请求

第一点,我们依然可以使用inquirer

第二点,我们可以使用一个第三方库ora,其作用是添加loading态,重传的实现我们可以进行tryCatch检测。

Show me Code

Creator.js

const inquirer = require("inquirer");
const { fetchRepoList } = require("./request");
const { wrapLoading } = require("./util");

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

    async fetchRepo(){
        // 新增loading 以及 失败重新获取
        let repos = await wrapLoading(fetchRepoList,'waiting fetch template');
        // let repos = await fetchRepoList();
        // // console.log(repos,'仓库列表');
        if(!repos) return;
        repos = repos.map(item => item.name);
        let { repo } = await inquirer.prompt({
            name: 'repo',
            type: 'list',
            choices: repos,
            message: 'please choose a template to create a project'
        })
        // // console.log(repo);
    }

    async fetchTag(){

    }

    async download(){

    }

    async create(){
        // 1. 先拉取当前组织下的模板
        let repo = await this.fetchRepo();
        // 2. 在通过模板找到版本号  
        let tag = await this.fetchTag();

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

        // 4. 编译模板

    }
}

module.exports = Creator;

util.js

const ora = require('ora')
// 挂起函数
export async function sleep(n) {
    return new Promise((resolve,reject) => {
        setTimeout(resolve,n)
    })
}

// loading + 失败重新发起fetch
export async function wrapLoading(fn,message) {
    const spinner = ora(message);
    spinner.start(); // 开始加载

    try {
        let repos = await fn();
        spinner.succeed(); // 成功
        return repos;
    } catch (error) {
        spinner.fail('request failed, refetch...')
        await sleep(1000);
        console.log(error);
        return wrapLoading(fn,message);
    }   
}
功能完善:下载选择好的模板至指定位置

此处从github上下载仓库我们可以借助一个第三方包download-git-repo

npm i download-git-repo

然后在下载函数中执行如下逻辑

  1. 拼接处下载路径
  2. 将资源下载到指定的位置 (后续亦可增加缓存功能)
Show me Code

Creator.js

 // 根据仓库和tag下载指定模板
    async download(repo,tag){
        // 1. 拼接处下载路径
        let requestUrl = `zhu-cli/${repo}${tag ? '#' + tag : ''}`;
        // 2. 将资源下载到指定的位置 (后续可以增加缓存功能 w-todo)
        await wrapLoading(this.downloadGitRepo,'waiting down repo', requestUrl,path.resolve(process.cwd(),`${repo}@${tag}`));

        return this.target;
    }

总结

至此,我们就完成了一个简易版的cli工具,可以看到,其实也就是包的使用+思路的理清,完整版代码的仓库地址,如果对您有任何帮助吧,赏小菜个star吧,谢谢阅读

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情