市面上已经有很多很成熟很好用的脚手架,如vue-cli,为什么还会想到自己写自定义脚手架呢?

        因为可按公司的需求,编写适合公司开发的脚手架,统一规范项目结构,方便维护,优化避免互相拷贝代码,导致bug,增加不必要的开发时间,扩散减少重复代码,降低门槛,更方便快速开启新的项目。

开始编写前,需要提前,按照自己需求先定义,项目模板,并上传到github上

步骤一.创建工程化

        1.创建目录并初始化获取package.json

npm init -y

        2.在package.json 中设置在命令下执行‘构建指令:如 test-cli’ 时调用bin目录下的test-cli.js文件

//package.json
{
    ...,
    "bin": {
        "test-cli": "./bin/test-cli.js"
    },
}

        3.test-cli.js文件中使用作为入口文件,并以node环境执行次文件

#! /usr/bin/env node
require('../src/main.js')

        4.建立软链接,全局使用

npm link

二.配置指令命令

自定义脚手架需要用到一下几个模块

        commander          参数解析 --help其实就是借助了它

        inquirer                 交互式命令行工具,有它就可以实现命令行的选择功能

        ora                        加载loading

        axios                     获取仓库模板信息

        download-git-repo 在git中下载模板

        metalsmith            读取所有文件,实现模板渲染

        consolidate           统一模板引擎

        ejs                        ejs模板,生成package

可以依次安装这些模块

1.写入指令配置表mapAcitions和入口文件main.js配置指令命令

main.js(./src/main.js)

//引入commander
const program = require('commander');
//引入path
const path = require('path');
//引入配置命令表
const mapAcitions = require('../config/mapAcitions');

//使用Reflect,好处是可以遍历Symbol
Reflect.ownKeys(mapAcitions).forEach((action) => {
    const comm = mapAcitions[action];
    program.command(action)//命令指令 
        .alias(comm.alias) //命令别名
        .description(comm.description) //备注
        .action(() => {
            //自定义处理逻辑
            if (action === '*') { 
                console.log('*',comm.description)
            } else {
                //截取命令action,//写入参数process.argv.slice(3)
                //执行命令对应文件
                require(path.resolve(__dirname,action))(...process.argv.slice(3))
            }
        })
    
});

program.on('--help',()=>{
    console.log('\r\nExamples:');
    Reflect.ownKeys(mapAcitions).forEach((action)=>{
        const {examples} = mapAcitions[action];
        examples.forEach(example=>{
            console.log(example)
        })
    })
});


//获取package.js基本信息
const pak = require('../package.json');
//动态写入基本信息
program.name(pak.name)
    .usage(`<command> [option]` )
    .version(pak.version)
    .parse(process.argv);

  注意:program.parse(process.argv),是必写,还要在最后写入,它注册它前面的内容

mapAcitions(./congif/mapAcitions)

module.exports = {
    create:{
        alias:'c',//别名
        description:'create a porject (创建一个项目)',//备注
        examples:[//指令
            'test-cli create <porject-name>'
        ]
    },
    config:{
        alias:'conf',
        description:'config project variable (项目变量配置)',
        examples:[
            'test-cli config set<k><v>',
            'test-cli config get<k>'
        ]
    },
    '*':{
        alias:'',
        description:'commander not founf (没有该指令)',
        examples:[]  
    }
}

 3.编写create 指令

//创建create
const path = require('path');
//引入axios
const axios = require('axios');
//引入inquirer //node使用8.0.0旧版本用require引入
const inquirer = require('inquirer');
//引入ora //node使用5.0.0旧版本用require引入
const ora = require('ora');

//包装一下downloadGitRepo
//promisify //包装成为es6的promise方法
const {promisify} = require('util')
let downloadGitRepo = require('download-git-repo');
downloadGitRepo = promisify(downloadGitRepo);
//downloadDirextory
const downloadDirextory = `${process.env[process.platform === 'win32'?'HOME':'USERPROFILE']}./template`;


//ncp 拷贝模板
let ncp = require('ncp');
ncp = promisify(ncp);

const fs = require('fs');
//引入metalsmith 遍历所有文件目录配合json渲染,只要模板渲染都需要
const MetalSmith = require('metalsmith');
const { down } = require('inquirer/lib/utils/readline');
//引入consolidate 返回渲染函数render 统一所有模板引擎
let  {render} = require('consolidate').ejs
render = promisify(render)


//拉取仓库模板信息
const fetchRepoList = async ()=>{
    const {data} = await axios.get("仓库模板地址/repos");
    return data
}
//获取仓库版本信息
const fetchTagList = async (repo)=>{
    const {data} = await axios.get(`仓库模板地址/${repo}/tags`);
    return data
}
//临时存放目录
const download = async(repo,tag) => {
    let api = `test-cli/${repo}`;//下载项目地址
    if(tag){
        api += `${tag}`;
    }
    const dest =`${downloadDirextory}/${repo}`;
    await downloadGitRepo(api,dest) //将模板下载对应的目录中
}
//封装loading
const waitFnLoading = (fn,message) => async (...args)=>{
    //添加loading
    const spinner = ora(message);
    spinner.start();//loading开始
    const repos = await fn(...args);
    spinner.succeed();//loading结束
    return repos
}
module.exports= async(projectName)=>{
    //拉取模板信息
    let repos = await waitFnLoading(fetchRepoList,'正在获取模板信息.....')();
    //选择模板
    repos = repos.map(item=>item.name);
    const {repo} = await inquirer.prompt({
        name:'repo',//获取选择后的结果
        type:'list',//什么方式显示在命令行
        message:'please chiose a template to create project(请选择模板创建项目)',//提示信息
        choices: repos,//选择的数据 
    })
    //抓取tag列表
    let tags = await waitFnLoading(fetchTagList,'正在获取版本信息.....')(repo);
    //选择模板
    tags = tags.map(item=>item.name);
    const {tag} = await inquirer.prompt({
        name:'tag',//获取选择后的结果
        type:'list',//什么方式显示在命令行
        message:'please chiose a template to create project(请选择创建对应版本)',//提示信息
        choices: tags,//选择的数据 
    })
    //下载目录 返回一个临时存放目录
    const result = await waitFnLoading(download,'正在下载模板')(repo,tag);
    console.log(repo,tags)
    
    //判断复杂的模板
    if(!fs.existsSync(path.join(result,'ask.js'))){
        //简单模板
         //将下载的文件拷贝到当前执行命令的目录下
        await ncp(result,path.join(path.resolve(),projectName));
    }else{
        //复杂的模板需要渲染后再拷贝
        //需要用户选择,选择后编译模板
        //1.让用户填写信息
        await new Promise((resolve,reject)=>{
            MetalSmith(__dirname)//如果传入路径,会遍历当前文件的src文件
            .source(result) // 遍历result文件
            .destination(path.resolve(projectName))//编辑要去的地方
            .use(async (files,metal,done)=>{
                //files 就是现在所有的文件
                //拿到提前配置好的信息 传下去渲染
                const args = require(path.join(result,'ask.js'));
                //拿到后让用户填写 返回填写的信息
                const obj = await inquirer.prompt(args)
                const meta = metal.metadata();//获取的信息合并传入下一个use
                Object.assign(meta,obj);//合并对象
                //aks.js文件无用了
                delete files['ask.js']
                done();
            })
            .use((files,metal,done)=>{
                //根据用户信息,渲染模板
                const obj = metal.metadata();
                Reflect.ownKeys(files).forEach(async(file)=>{
                    if(file.includes("js") || file.includes("json")){
                        //文件内容
                        let content = files[file].contents.toString();
                        //判断是不是模板
                        if(constent.includes("<%")){
                            content = await render(content,obj);
                            //渲染
                            files[file].contents = Buffer.from(content)
                        }
                    }
                })
                done();
            })
            .build(err => {
                if(err){
                    reject();
                }else{
                    resolve();
                }
            })
        })
    }
}

       对于简单的项目可以直接把下载好的项目拷贝到当前执行命令的目录即可。

       npc这里也需要做的严谨一些,判断一下当前目录下是否有重名文件等。。。。还有很多细节也需要多次创建项目是否需要利用已经下载好的模板,按照项目需要编写

4.项目模板中增加ask.js

module.exports=[
    {
        type:'confirm',
        name:'private'
        message:'ths resgistery is private?'    
    },
    {
        type:'input',
        name:'author'
        message:'author?'    
    },
    {
        type:'input',
        name:'description'
        message:'description?'    
    },
    {
        type:'input',
        name:'license'
        message:'license?'    
    }
]

根据相对应的询问生成最终的package.json

7.项目模板添加下载模板可以只用ejs模板

{
      "name": "test-cli",
      "version": "0.0.1",
      "private":"<%=private&>"
      "description": "<%=description&>",,
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "bin": {
        "test-cli": "./bin/test-cli.js"
      },
      "keywords": [],
      "author": "<%=author&>",
      "license": "<%=license&>",
      "dependencies": {
        "axios": "^1.3.4",
        "commander": "^10.0.0"
      },
      "devDependencies": {
        "inquirer": "^8.0.0",
        "ora": "^5.0.0"
      }
}

核心原理就是将下载的模板文件,依次遍历根据用户填写的信息渲染模板,将渲染好的结果拷贝到执行命令的目录下

8.发布脚手架

npm addUser // 注册账号密码

npm login // 登录账号

npm publish // 发布