创建自己的脚手架命令工具

vue脚手架有自己的模板管理工具;vue-cli,我们也可以自己写一款自己的模板管理脚手架,通过使用自己的命令,安装相应的模板

初始化项目

  • npm初始化一个空项目
  • 创建bin/cli.js文件
#! /usr/bin/env node
//测试
console.log('sx')
  • 创建lib/index.js文件为入口文件
  • 修改package.json
{
  "name": "sx",
  "version": "1.0.0",
  "description": "",
  "main": "lib/index.js",
  "bin": "bin/cli.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

  • 控制台输入 npm link 将本项目链接到全局
  • 直接输入项目名:sx即可运行cli.js
    • 输出 ‘sx’

配置相关指令

我们设想,输入 sx后,必定跟随一定的指令,如npm 中输入: npm -h会查看提示,输入npm -V会查看版本一样
我们借助commander包来实现我们的需求

  • npm i commander -D
  • 原生node中通过process.argv接受参数,但是比较不方便,我们把接受的参数传递给commander,可以实现快速上手
  • cli中修改如下:
#! /usr/bin/env node

const {program} = require("commander")
const {version} = require("../package.json")

program
    .version(version)
    .parse(process.argv)
  • 此时输入sx -h就有相关提示,输入sx-V,就会显示版本号
  • commander给我们集成了这几种常用的命令,接下来,我们需要创建自己的指令,指令和上述代码类似,但是每一个指令都这么写显然太费事费力,好的方式是将指令写成一个集合,然后遍历生成指令
#! /usr/bin/env node

const {program} = require("commander")
const {version} = require("../package.json")

const actionMap = {
    // 指令名为键,指令的相关配置作为值
    'create':{
        //别名
        alias:'crt',
        description:'初始化项目',//描述
        examples:['sx create <projectname>']//给用户的提示
    }
}

//遍历指令
Reflect.ownKeys(actionMap).forEach(aname=>{
    program
        .command(aname) //需要绑定的指令名称
        .alias(actionMap[aname].alias)//绑定的指令的别称
        .description(actionMap[aname].description)//绑定描述
        .action(()=>{
            //指令出发后执行什么操作
            console.log(aname)
        })
    })

//将指令绑定的提示命令中
program.on("--help",()=>{
    Reflect.ownKeys(actionMap).forEach(aname=>{
        actionMap[aname].examples.forEach(item=>{
            console.log(`${aname}:${item}`)
        })
    })
})

program
    .version(version)
    .parse(process.argv)
  • 此时,我们输入sx -h 会看到我们自定义指令的提示,输入sx create pp,会触发create指令的操作,输出’create’

实现相关命令

出发相关的命令后,需要执行相关的操作,这里依然以create创建项目为例实现

  • 引入lib/index文件,将命令传入进去,实现命令的详细功能:let mainFn = require("…")
  • 命令接收后,触发改为 mainFn(aname,process.argv.slice(3))
    • mainFn为lib/index导入模块
    • process.argv为一个用户输入数组,用户输入的内容从数组的第三位开始记录的,前两位为项目地址
  • lib/index.js文件中集合所有命令的操作并不合适,因为命令太多容易造成代码杂乱,所以最好还是再次因此不筒的命令模块
//  lib/index.js

module.exports = function(aname,argv){
    //引入指令相对应的模块,传入用户输入的操作
    require('./'+aname)(...argv)
}
  • 创建lib/create.js
const axios = require("axios")

module.exports = function(proname){
    console.log(proname)
}
  • 此时,输出的proname为我们通过命令创建的项目名,即输入sx create pp,输出 pp

拉取模板

创建项目后,并不能直接拉取项目,我们需要考虑一下信息:

  • 需要用户选择项目类型,
  • 选择项目版本号等一些信息,
  • 用户在命令行做交互,我们使用inquirer包实现:npm i inquirer -D
  • 安装项目会有时间等待,这时候需要做交互,npm o ora -D
  • 我们的设想是:
    • 当用户输入create命令,让用户选择要下载的模板
    • 用户选择好后开始提示下载
    • 下载完后提示成功
    • 接着交互让用户选择版本号
    • 接着提示下载

可以看到,提示下载和提示完成和拉取模板文件等都是单独的功能,且可以调用多次,所以我们可以尝试将这些步骤分别封装成模块

  • 封装耗时等待方法
//为了是传入的函数也能传入参数,采用柯里化的写法
const addLoading = function (fn) {
  return async function (...args) {
    //接收一个函数,在函数执行前后分别提示
    let spinner = ora("开始下载模板选项");
    spinner.start();
    try {
      let ret = await fn(...args);
      spinner.succeed('下载成功')
      return ret
    } catch {
        spinner.fail('下载失败')
    }
  };
};
  • 封装模板选择函数
    • 首先模板选项从哪里下载?
    • github中个人用户可以通过/repos的形式查看自己有多少个仓库,并且每个仓库的信息以json形式继承在一起
    • 我们只需要把自己配置好的模板发布成仓库,再通过github下载即可获得各个模板的信息
    • 模板如何发布后期会写一篇文章(占位)
    • 这里使用 一位老师授课时使用的模板文件集成地址:https://api.github.com/users/zcegg/repos
const fetchRepoList = async function(){
    const {data} = await axios.get("https://api.github.com/users/zcegg/repos")
    //文件形式大家可以点开看一下,我们获取每个模板的名称,后期供用户选择
    let repos = data.map(item=>item.name)
    return repos

}
  • 根据选择的模板获取指定的版本号
const fetchTagList = async function(reponame){
    const {data} = await axios.get(`https://api.github.com/repos/zcegg/${reponame}/tags`)
    let repos = data.map(item => item.name)
    return repos
}

  • 导出的模块中,分别调用这些函数,实现相应的逻辑
//获取模板列表
    let repos = addLoading(fetchRepoList)()
    //根据模板列表设置用户交互
    let { tmpname } = inquirer.prompt({
        //用户交互配置
        type:'list',//列表选择类型
        name:'tmpname',//接收用户回应的对象名
        message:'请选择目标仓库',
        choices:repos //所提供的选项
    })
    //接收到用户的选择之后,开始获取该选择下的模板的版本号
    let tags = addLoading(fetchTagList)(tmpname)
    //有的模板有多个版本号,此时需要用户选择哪一个,有的模板则只有一个版本号,直接下载即可
    if(tags.length){
        //如果有多个版本号,继续和用户交互
        let {tagv} = inquirer.prompt({
            type:'list',
            name:'tagv',
            message:'请选择模板版本号',
            choices:tags
        })
        //开始下载
    }else {
        let {isDownload} = inquirer.prompt({
            type:'confirm',
            name:'isDownload',
            message:"不存在多个版本,是否直接下载"
            
        })
        if(isDownload){
            //开始下载
        }
        else {
            return;
        }
    }

完善下载模块

  • 模板下载到系统的缓存目录下,以便下次下载的时候直接从本地下载
  • 借助 let downloadFn = require(‘download-git-repo’) 模块实现从github下载仓库
const downloadRepo = async function(repo,tag){
    //定义缓存目录
    let cacheDir = `${process.env[process.platform == 'win32'?'USERPROFILE':'HOME']}/.temp`
    let api = `zcegg/${repo}`
    if(tag){
        api+=`#${tag}`
    }
    //定义模板下载后的输出路径
    let dest = path.resolve(cacheDir,repo)
    let spinner = ora('开始下载')
    spinner.start()
    let flag = fs.existsSync(dest)
    if(flag){
        spinner.stop()
        return dest

    }else {
        await downloadFn(api,dest)
        spinner.succeed('下载结束')
        return dest
    }
}

渲染模板

  • 模板下载完毕后需要对模板进行渲染并创建到相应的工程下
  • 渲染过程中可能会有交互,如询问项目版本号,作者等信息,如果有这些询问,则需要保留询问答案,将模板渲染到指定位置
  • 借助metalsmith实现模板的读取和复制
  • 借助consolidate实现渲染
  • npm i metalsmith -D
  • npm i consolidate ejs -D
let Metalsmith = require('metalsmith')
let {render} = require('consolidate').ejs
  • 在模板文件中,我们的问题列表可能会在que.js中,所以要判断是否有这个文件,没有则直接将模板复制到要创建的目录下,有的话需要先渲染模板,再复制到相应位置
 //渲染模板并将模板复制到创建的项目下
    if(fs.existsSync(path.join(dest,'que.js'))){
        //存在问题列表,则渲染数据
        console.log("正在渲染模板")
        await new Promise((resolve,reject)=>{
            Metalsmith(__dirname) //传入一个参数(语法规定)
                .source(dest) //传入要从哪里渲染
                .destination(path.resolve(proname)) //渲染到哪里
                .use(async (files,metal,done)=>{
                    //use函数是对读取的文件夹里的文件的操作,可以链式调用,执行完后调用done函数结束操作
                    //files会将所有文件夹里的数据以文件名为键,文件内容为值读成一个json数据表
                    // metal可以将数据进行往后传递
                    //获取问题列表
                    let quesArr = require(path.resolve(dest,'que.js'))
                    //交互询问
                    let ans = await inquirer.prompt(quesArr)
                    //保留答案并传递给下一个ues
                    let meta = metal.metadata()
                    Object.assign(meta,ans)
                    //将files中的que.js有关内容删除,后期会根据files进行渲染到指定工程下
                    delete files['que.js']
                    done() //表示这个操作结束
                })
                .use((files,metal,done)=>{
                    // 在这个操作中渲染模板
                    // 获取数据
                    let data = metal.metadata()
                    Reflect.ownKeys(files).forEach(  async  file=>{
                        if(file.includes('.js')||file.includes('.json')){
                            //存在.js文件或者.json文件,就需要查看其内容,是否有ejs模板痕迹,有则拿接收的数据进行渲染
                            let contents = files[file].contents.toString()
                            if(contents.includes("<%")){ 
                                //渲染内容
                              let  content = await render(contents,data)
                                //将原内容替换成渲染好的内容
                                files[file].contents = Buffer.from(content)
                            }
                        }
                    })
                    done()
                })
                //build执行复制操作,将files内容以此写入到指定项目中
                .build((err)=>{
                    if(err){
                        reject()
                    }
                    resolve()
                })
        })
    }else {
        console.log('不需要渲染数据')
        //借助ncp模块,直接复制
        // npmi ncp -D
        ncp(dest,proname)
    }
    console.log('下载完成')

  • 输入指令,即可完成模板的拉取
  • 在这里插入图片描述
  • 注意所以异步操作必须要等待执行,即使用 await
  • create.js完整代码
const axios = require("axios");
const inquirer = require("inquirer");
const ora = require("ora");
const fs = require('fs')
let downloadFn = require("download-git-repo")
const { promisify} = require('util');
const path = require("path");
let Metalsmith = require('metalsmith');
const ncp = require('ncp');
const cons = require("consolidate");
let {render} = require('consolidate').ejs

downloadFn = promisify(downloadFn)
//为了是传入的函数也能传入参数,采用柯里化的写法
const addLoading = function (fn) {
  return async function (...args) {
    //接收一个函数,在函数执行前后分别提示
    let spinner = ora("开始下载模板选项");
    spinner.start();
    try {
      let ret = await fn(...args);
      spinner.succeed('下载成功')
      return ret
    } catch {
        spinner.fail('下载失败')
    }
  };
};

const fetchRepoList = async function(){
    const {data} = await axios.get("https://api.github.com/users/zcegg/repos")
    //文件形式大家可以点开看一下,我们获取每个模板的名称,后期供用户选择
    let repos = data.map(item=>item.name)
    return repos

}

const fetchTagList = async function(reponame){
    const {data} = await axios.get(`https://api.github.com/repos/zcegg/${reponame}/tags`)
    let repos = data.map(item => item.name)
    return repos
}

const downloadRepo = async function(repo,tag){
    // 定义缓存目录,通过判断系统来找到相应系统的缓存路径
    let cacheDir = `${process.env[process.platform == 'win32'?'USERPROFILE':'HOME']}/.tmp`
    //按照download-git-repo格式定义api
    let api = `zcegg/${repo}`
    if(tag){
        api+=`#${tag}`
    }
    //定义模板下载后的输出路径
    let dest = path.resolve(cacheDir,repo)
    let spinner = ora('开始下载模板')
    spinner.start()
    //判断该路径是否存在
    if(fs.existsSync(dest)){
        //存在则直接使用缓存即可
        spinner.stop()
        return dest
    }else {
        await downloadFn(api,dest)
        spinner.succeed('模板下载成功')
        return dest
    }

}
module.exports = async function (proname) {
    //获取模板列表
    let repos = await addLoading(fetchRepoList)()
    
    //根据模板列表设置用户交互
    let { tmpname } = await inquirer.prompt({
        //用户交互配置
        type:'list',//列表选择类型
        name:'tmpname',//接收用户回应的对象名
        message:'请选择目标仓库',
        choices:repos //所提供的选项
    })
  
    //接收到用户的选择之后,开始获取该选择下的模板的版本号
    let tags =await addLoading(fetchTagList)(tmpname)
    //有的模板有多个版本号,此时需要用户选择哪一个,有的模板则只有一个版本号,直接下载即可
    let dest = null
    if(tags.length){
        //如果有多个版本号,继续和用户交互
        let {tagv} =await inquirer.prompt({
            type:'list',
            name:'tagv',
            message:'请选择模板版本号',
            choices:tags
        })
        //开始下载
      dest = await downloadRepo(tmpname,tagv)
    }else {
        let {isDownload} =await inquirer.prompt({
            type:'confirm',
            name:'isDownload',
            message:"不存在多个版本,是否直接下载"
            
        })
        if(isDownload){
            //开始下载
          dest = await downloadRepo(tmpname)
        }
        else {
            return;
        }
    }
    //渲染模板并将模板复制到创建的项目下
    if(fs.existsSync(path.join(dest,'que.js'))){
        //存在问题列表,则渲染数据
        console.log("正在渲染模板")
        await new Promise((resolve,reject)=>{
            Metalsmith(__dirname) //传入一个参数(语法规定)
                .source(dest) //传入要从哪里渲染
                .destination(path.resolve(proname)) //渲染到哪里
                .use(async (files,metal,done)=>{
                    //use函数是对读取的文件夹里的文件的操作,可以链式调用,执行完后调用done函数结束操作
                    //files会将所有文件夹里的数据以文件名为键,文件内容为值读成一个json数据表
                    // metal可以将数据进行往后传递
                    //获取问题列表
                    let quesArr = require(path.resolve(dest,'que.js'))
                    //交互询问
                    let ans = await inquirer.prompt(quesArr)
                    //保留答案并传递给下一个ues
                    let meta = metal.metadata()
                    Object.assign(meta,ans)
                    //将files中的que.js有关内容删除,后期会根据files进行渲染到指定工程下
                    delete files['que.js']
                    done() //表示这个操作结束
                })
                .use((files,metal,done)=>{
                    // 在这个操作中渲染模板
                    // 获取数据
                    let data = metal.metadata()
                    Reflect.ownKeys(files).forEach(  async  file=>{
                        if(file.includes('.js')||file.includes('.json')){
                            //存在.js文件或者.json文件,就需要查看其内容,是否有ejs模板痕迹,有则拿接收的数据进行渲染
                            let contents = files[file].contents.toString()
                            if(contents.includes("<%")){ 
                                //渲染内容
                              let  content = await render(contents,data)
                                //将原内容替换成渲染好的内容
                                files[file].contents = Buffer.from(content)
                            }
                        }
                    })
                    done()
                })
                //build执行复制操作,将files内容以此写入到指定项目中
                .build((err)=>{
                    if(err){
                        reject()
                    }
                    resolve()
                })
        })
    }else {
        console.log('不需要渲染数据')
        //借助ncp模块,直接复制
        // npmi ncp -D
        ncp(dest,proname)
    }
    console.log('下载完成')

};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

问也去

创作不易,感谢支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值