node 自定义cli + npm发布

225 阅读3分钟

1.自定义cli优势

  1. 规范所有项目的文件夹,文件的命名规范
  2. 规范第三方库的版本
  3. 规范所有配置信息
  4. 规范内部git的提交标准与代码风格
  5. 规范所有自定义的工具,内置方法的版本

2.脚手架执行流程

  1. 通过提示输入 项目的关键信息
  2. 拉取下载项目模板
  3. 根据输入关键字替换配置文件与描述
  4. 自动执行install方法安装
  5. 自动打开浏览器

3. 程序入口

{ 
  "main": "index.js",
  "bin": {
    "mycli": "index.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  }, 
}
  1. 通过package.json 的 main 我们可以指定库的入口
  2. 在bin下面定义的命令,如bin:{ "mycli": "index.js"},就是在命令行直接执行mycli ,会对应执行的js文件
  3. 通过npm link,就可以在本地终端 直接使用 mycli命令, 当然发布到网上,通过npm -g 安装后也是一样的效果

4.前置工具

  • commander : 命令行解决方案
  • chalk:命令行彩色文字
  • figlet:命令行艺术字
  • inquirer : 命令行输入交互,提供多种问答方式
  • ora : loading效果
  • download-git-repo : 远程下载
  • handlebars : 模板引擎替换html的占位内容 {{name}}
  • ejs : 模板引擎替换html的占位内容,<%= name %>替换
  • clear 清屏
  • open : 打开浏览器

5.开发cli

  1. 构建结构与依赖
mkdir mycli
cd mycli
npm init -y
npm i commander download-git-repo ora handlebars figlet clear chalk open -s
  1. 设置启动命令 创建启动脚本 bin/run.js
#!/usr/bin/env node 
//  #!是shebang命令 /usr/bin/env 用来告诉用户到path目录下去寻找node,#!/usr/bin/env node 可以让系统动态的去查找node
//可以通过 which node  查找当前node的安装位置 /usr/local/bin/node
const program = require("commander") 
// const init = require("../lib/init")
//定义版本 -v 响应
program.version(require("../package.json").version)

//< >和[ ]分别代表必填和选填 ,在action 就会返回对应的录入参数 name
program.command("init <name>").description("init project").action(require("../lib/init"))

// init()
//开始监听输入
program.parse(process.argv)

对应 package.json 修改 新增如下

"bin": {
"mycli": "./bin/run.js"
},

3. 定制初始化界面 lib/init.js


const {promisify} = require("util")
const figlet = promisify(require('figlet'))
const chalk = require("chalk")
const clear = require("clear")
const clone = require("../lib/download")
//添加颜色输出控制台
const colorLog = content =>  console.log(chalk.redBright(content))

module.exports = async name => {
    clear()//清屏 
    const data = await figlet("welcome to jason cli")//把文字转化成输出
    colorLog(data)//打印
    //name 为刚刚init 创建的 文件夹名称
    await clone('github:mjsong07/myComponent', name)
}

  1. 封装下载代码逻辑 lib/download.js

const {promisify} = require("util")
const ora = require("ora")
module.exports = async (repo,filderPath) => {
    const download = promisify(require("download-git-repo"))
    //进程提示
    const process = ora(`downloading ${repo}`)
    process.start()
    await download(repo,filderPath)
    process.succeed();
}
  1. 新增依赖安装与自动打开网页,运行命令 优化 init.js 代码
const { promisify } = require('util')
const figlet = promisify(require('figlet'))
const clear = require('clear')
const chalk = require('chalk')
const { clone } = require('./download')
const spawn = async (...args) => {
    const { spawn } = require('child_process')
    return new Promise(resolve => {
        const proc = spawn(...args)
        proc.stdout.pipe(process.stdout)
        proc.stderr.pipe(process.stderr)
        proc.on('close', () => {
            resolve()
        })
    })
}
const log = content => console.log(chalk.green(content))
module.exports = async name => {
    // 打印欢迎画面
    clear()
    const data = await figlet('Welcome')
    log(data)
    // 创建项目
    log(`🚀创建项目:` + name)
    // 克隆代码  
    await clone('github:mjsong07/myComponent', name)
    log('安装依赖')
    await spawn('npm', ['install'], { cwd: `./${name}` })
    log(`
👌安装完成:
To get Start:
===========================
    cd ${name}
    npm run serve
===========================
            `)

    const open = require('open')
    open('http://localhost:8080')
    await spawn('npm', ['run', 'serve'], { cwd: `./${name}` })
}

bin\serve.js 启动服务

const spawn = (...args) => {
    const { spawn } = require('child_process');
    const proc = spawn(...args)
    proc.stdout.pipe(process.stdout)
    proc.stderr.pipe(process.stderr)
    return proc
}

module.exports = async () => {
    const watch = require('watch')
    let process
    let isRefresh = false
    watch.watchTree('./src', async (f) => {
        if (!isRefresh) {
            isRefresh = true
            process && process.kill()
            await require('./refresh')()
            setTimeout(() => { isRefresh = false }, 5000)
            process = spawn('npm', ['run', 'serve'])
        }
    })
}

bin\refresh.js 刷新路由代码

const fs = require('fs')
const handlebars = require('handlebars')
const chalk = require('chalk')
module.exports = async () => {
    // 获取页面列表
    const list =
        fs.readdirSync('./src/views')
            .filter(v => v !== 'Home.vue')
            .map(v => ({
                name: v.replace('.vue', '').toLowerCase(),
                file: v
            }))
    compile({
        list
    }, './src/router.js', './template/router.js.hbs')

    // 生成菜单
    compile({
        list
    }, './src/App.vue', './template/App.vue.hbs')

    /**
     * 
     * @param {*} meta 
     * @param {*} filePath 
     * @param {*} templatePath 
     */
    function compile(meta, filePath, templatePath) {
        if (fs.existsSync(templatePath)) {
            const content = fs.readFileSync(templatePath).toString()
            const reslut = handlebars.compile(content)(meta)//使用模板引擎替换内容
            fs.writeFileSync(filePath, reslut)
        }
        console.log(chalk.red(`🚀${filePath} 创建成功`))
    }
}

发布代码到npm

./publish.sh

#!/usr/bin/env bash
npm config get registry # 检查仓库镜像库
npm config set registry=https://registry.npmjs.org
echo '请进行登录相关操作:'
npm login # 登陆
npm whoami # 查看登录用户
echo "-------publishing-------"
npm publish # 发布
npm config set registry=https://registry.npmmirror.com # 还原本地淘宝源地址
echo "发布完成"
exit

执行: sh ./publish.sh

6.扩展

1. 使用ejs进行模板特殊字符替换

vue-template.vue.ejs

<template>
  <div class="<%= data.lowerName %>">
    <h2>{{ message }}</h2>
  </div>
</template>

<script>
  export default {
    name: "<%= data.name %>",
    components: {
    },
    mixins: [],
    props: {
    },
    data: function() {
      return {
        message: "Hello <%= data.name %>"
      }
    },
    created: function() {
    },
    mounted: function() {
    },
    computed: {
    },
    methods: {
    }
  }
</script>

<style scoped>
  .<%= data.lowerName %> {
    
  }
</style>

util\utils.js 工具类


// 编译模板
const compiler = (templateName, data) => {
    // 根据用户执行的命令,拿到指定路径的模板,进行渲染创建
    const templateCurrentPath = `../templates/${templateName}`;
    const templateAbsolutePath = path.resolve(__dirname, templateCurrentPath);
    // 读取HTML 标签
    return new Promise((resolve, reject) => {
        ejs.renderFile(templateAbsolutePath, { data }, {}, (err, result) => {
            if (err) {
                reject(err);
                return;
            }
            resolve(result);
        });
    });
};


// 递归创建不存在的文件
const createNotFileName = (pathName) => {
    if (fs.existsSync(pathName)) {
        return true;
    } else {
        // 找到当前路径的父路径
        if (createNotFileName(path.dirname(pathName))) {
            fs.mkdirSync(pathName);
            return true;
        }
    }
};

// 写入文件夹操作
const writeToFile = (pathName, result) => {
    //先判断文件夹是否存在,不存在则创建
    const dirname = path.dirname(pathName);
    if(!fs.existsSync(dirname)){
        fs.mkdirSync(dirname);
    }
    // path: 目标文件夹的绝对路径(只支持绝对路径)
    return fs.promises.writeFile(pathName, result);
};
module.exports = {
    compiler,
    writeToFile,
    createNotFileName,
    changeJson,
};

测试替换代码

// create VUE component template
async function createVueComponentTemplateAction(project, filepath) => {
    // 编译 ejs 模板
    const result = await compiler("vue-template.vue.ejs", {
        name: project,
        lowerName: project.toLowerCase(),
    });
    // 将 result 写入 .vue 文件中
    const targetPath = path.resolve(filepath, `${project}.vue`);
    // write in file
    await writeToFile(targetPath, result);
};