• 基于node.js的cli工具,保姆级实现教程


    介绍

    基于nodejs的cli工具

    实现教程

    创建项目

    新建一个文件夹,根目录下执行指令 npm init -y 自动创建package.json文件,初始化项目。
    package.json中新增一条"type": "module" 配置引用的文件作为ES模块进行加载。package.json文件内容如下:

    {
      "name": "zyf-cli",
      "version": "1.0.0",
      "description": "基于nodejs的cli工具",
      "main": "index.js",
      "type": "module",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "repository": {
        "type": "git",
        "url": "git@gitee.com:wendZzoo/zyf-cli.git"
      },
      "keywords": [],
      "author": "",
      "license": "ISC"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    根目录下新建index.js,作为入口文件。cli工具的整体的实现思路:

    1. 创建文件夹
    2. 创建入口文件
    3. 创建package.json
    4. 安装依赖

    使用fs模块可以实现如上思路,index文件代码如下:

    import fs from "fs";
    fs.mkdirSync("./demo");
    fs.writeFileSync("./demo/index.js", "index");
    fs.writeFileSync("./demo/package.json", "package");
    // 4 todo 安装依赖
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如上代码实现了,在根目录下创建了一个demo文件夹,在demo中新建了index.js,写入index, 新增package.json写入package

    这时候在根目录下执行指令node index.js 就可以发现自动新建了一个demo文件夹,修改package中操作指令 "test": "rm -rf ./demo && node index.js" 之后就可以直接使用npm run testyarn test 作为测试指令,这就是个cli工具的雏形。

    ejs创建模板文件

    demo中的index和package文件中写入的内容,需要是我们提前定义的好模板,方便用户使用时候可以直接看到生成好的代码。

    这里使用ejs,ejs 官方文档

    https://ejs.bootcss.com/

    npm install esj 安装ejs

    根目录下新建template文件夹,里面新建index.ejs, 安装vscode插件EJS language support可以让ejs代码高亮,这里就是最终想对用户展现的内容,代码如下:

    const koa = require('koa')
    <% if (static) { %>
        const Router = require('koa-router')
      <% } %>
    <% if (static) { %>
        const serve = require('koa-static')
      <% } %>
        const app = new koa()
    <% if (static) { %>
        app.use(serve(__dirname + "/static"))
    <% } %>
    <% if (router) { %>
        const router = new Router()
        router.get("/", (ctx) => {
            ctx.body = 'Hello, koa-setup-test'
        })
        app.use(router.routes())
    <% } %>
        app.listen(8080, () => {
            console.log('open server localhost:8080')
        })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    根目录下新建 createIndexTemplate.js 封装index文件的模板内容生成方法,读取index.ejs中内容,通过ejs的render方法将生成的代码写入demo下的index.js, 代码如下:

    import ejs from 'ejs'
    import fs from 'fs'
    export default () => {
        const indexTemplate = fs.readFileSync("./template/index.ejs")
        const code = ejs.render(indexTemplate.toString(), {
            static: true,
            router: true
        })
        return code
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    改造一下index.js, 引入createIndexTemplate,代码如下:

    import fs from "fs";
    import createIndexTemplate from './createIndexTemplate.js'
    fs.mkdirSync(getRootPath());
    fs.writeFileSync(`${getRootPath()}/index.js`, createIndexTemplate());
    fs.writeFileSync(`${getRootPath()}/package.json`, "package");
    // 4 todo 安装依赖
    function getRootPath() {
        return "./demo";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    终端里执行指令yarn test, 查看demo下index内容,createIndexTemplate中修改static: false, router: true,再次执行指令,会发现demo下index内容有变化,koa-static相关代码不再显示。

    相同的,改造一下demo下package.json里写入的代码。
    template下新建package.ejs文件,代码如下:

    {
        "name": "<%= packageName %>",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "type": "module",
        "scripts": {
          "test": "echo \"Error: no test specified \" && exit 1"
        },
        "keywords": [],
        "author": "",
        "license": "ISC",
        "dependencies": {
          "koa": "^2.13.1"
          <% if (middleware.router) { %>
            ,"koa-router": "^10.0.0"
          <% } %>
          <% if (middleware.static) { %>
            ,"koa-static": "^5.0.0"
          <% } %>
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    新建createPackageTemplate.js, 代码如下:

    import ejs from 'ejs'
    import fs from 'fs'
    export default (config) => {
        const packageTemplate = fs.readFileSync("./template/package.ejs")
        const code = ejs.render(packageTemplate.toString(), {
            packageName: config.packageName,
            middleware: config.middleware
        })
        return code
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    改造index, 修改内容如下:

    import createPackageTemplate from "./createPackageTemplate.js";
    const inputConfig = {
        packageName: "demo",
        port: 8080,
        middleware: {
            static: true,
            router: false,
        },
    };
    fs.writeFileSync(`${getRootPath()}/index.js`, createIndexTemplate(inputConfig));
    fs.writeFileSync(`${getRootPath()}/package.json`, createPackageTemplate(inputConfig));
    function getRootPath() {
        return path.resolve(process.cwd(), inputConfig.packageName)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    抽离了输入的配置项inputConfig, 也要相应的修改一下createIndexTemplate中传入的参数

    createIndexTemplate.js修改内容如下:

    export default (config) => {
        const indexTemplate = fs.readFileSync("./template/index.ejs")
        const code = ejs.render(indexTemplate.toString(), {
            middleware: config.middleware
        })
        return code
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那相应的也要修改一下中的传入参数,修改的内容如下:

    const koa = require('koa')
    <% if (middleware.static) { %>
        const Router = require('koa-router')
      <% } %>
    <% if (middleware.static) { %>
        const serve = require('koa-static')
      <% } %>
        const app = new koa()
    <% if (middleware.static) { %>
        app.use(serve(__dirname + "/static"))
    <% } %>
    <% if (middleware.router) { %>
        const router = new Router()
        router.get("/", (ctx) => {
            ctx.body = 'Hello, koa-setup-test'
        })
        app.use(router.routes())
    <% } %>
        app.listen(8080, () => {
            console.log('open server localhost:8080')
        })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    动态生成配置

    通过inquirer插件交互式生成inputConfig

    inquirer 文档:

    https://www.npmjs.com/package/inquirer#documentation

    npm install inquirer 安装inquirer

    修改index.js,代码如下:

    import fs from "fs";
    import inquirer from "inquirer";
    import createIndexTemplate from "./createIndexTemplate.js";
    import createPackageTemplate from "./createPackageTemplate.js";
    const r = await inquirer.prompt([
        /* Pass your questions in here */
        {
            type: "input",
            name: "packageName",
            message: "set package name",
            validate(val) {
                if (val) return true;
                return "please enter package name";
            },
        },
        {
            type: "input",
            name: "port",
            default() {
                return 8080;
            },
            message: "set port number",
        },
        {
            type: "checkbox",
            name: "middleware",
            choices: [{ name: "koaStatic" }, { name: "koaRouter" }],
        },
    ]);
    console.log(r);
    // 以下代码暂 注释
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    测试,终端执行node index.js, 出现交互式配置

    配置项如果变多时,代码放在index就很不便于维护,根目录下新建question文件夹封装配置项
    question下新建index.js,代码如下:

    import inquirer from "inquirer";
    import packageName from "./packageName.js";
    import port from "./port.js";
    import middleware from "./middleware.js";
    export default () => {
        return inquirer.prompt([
            /* Pass your questions in here */
            packageName(),
            port(),
            middleware(),
        ]);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    新建packageName.js, 代码如下:

    export default () => {
        return {
            type: "input",
            name: "packageName",
            message: "set package name",
            validate(val) {
                if (val) return true;
                return "please enter package name";
            },
        };
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    新建middleware.js, 代码如下:

    export default () => {
        return {
            type: "checkbox",
            name: "middleware",
            choices: [{ name: "koaStatic" }, { name: "koaRouter" }],
        };
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    新建port.js, 代码如下:

    export default () => {
        return {
            type: "input",
            name: "port",
            default() {
                return 8080;
            },
            message: "set port number",
        };
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    根目录下新建config.js,封装配置文件,代码如下:

    export function createConfig(answer) {
        function haveMiddleware(name) {
            return answer.middleware.indexOf(name) !== -1;
        }
        return {
            packageName: answer.packageName,
            port: answer.port,
            middleware: {
                static: haveMiddleware("koaStatic"),
                router: haveMiddleware("koaRouter"),
            },
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    修改根目录index.js, 修改内容如下:

    import question from "./question/index.js";
    import { createConfig } from "./config.js";
    const answer = await question();
    const inputConfig = createConfig(answer);
    console.log(inputConfig);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    重复测试,输出结果一致,说明这里的代码抽离封装是没问题的, 可以放开下面代码的注释

    依赖安装

    使用execa插件进行package依赖的安装,execa是对node官方推荐库child-process子进程的二次封装,使用更加方便

    execa文档:

    https://www.npmjs.com/package/execa

    npm install execa 安装execa

    修改index.js,修改内容如下:

    import { execa } from "execa";
    // 4. Todo
    execa("npm install", {
        cwd: getRootPath(),
        stdio: [2, 2, 2],
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    全局使用cli

    在支持全局使用之前,需要修改一下模板文件中的文件引用路径,不然执行时会报错

    根目录下新建bin文件夹,之前的文件(index, createIndexTemplate, createPackageTemplate, config, template, question)移到bin下, 修改package.json, 代码如下:

    {
      "name": "zyf-cli",
      "version": "1.0.0",
      "description": "基于nodejs的cli工具",
      "main": "index.js",
      "bin": "./bin/index.js",
      "type": "module",
      "scripts": {
        "test": "rm -rf ./demo && node index.js",
        "test-g": "rm -rf ./demo && zyf-cli"
      },
      "repository": {
        "type": "git",
        "url": "git@gitee.com:wendZzoo/zyf-cli.git"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "dependencies": {
        "ejs": "^3.1.8",
        "execa": "^6.1.0",
        "inquirer": "^9.0.2"
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    上面可以看到新增了一条指令test-g, 需要先配置一下全局使用

    修改一下createIndexTemplate.js 中文件路径的引用方式,修改代码如下:

    import ejs from "ejs";
    import fs from "fs";
    import path from "path";
    import { fileURLToPath } from "url";
    export default (config) => {
        const __dirname = fileURLToPath(import.meta.url);
        const indexTemplate = fs.readFileSync(
            path.resolve(__dirname, "../template/index.ejs")
        );
        const code = ejs.render(indexTemplate.toString(), {
            middleware: config.middleware,
        });
        return code;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    因为package.json中配置了"type": "module",原本node可以使用的__dirname只能借助fileURLToPath实现
    相同的,createPackageTemplate一样的改造,代码重复就不贴了

    配置全局使用, 终端执行npm link, npm root -g 打开输出的文件路径,全局下已经出现了zyf-cli

    执行yarn test-g, 正常出现demo

    代码格式化

    使用prettier格式化demo下代码

    prettier官方文档:

    https://www.prettier.cn/docs/index.html

    npm install prettier 安装prettier

    只需要在创建模板最终输出的时候,使用prettier进行代码格式化

    改造createIndexTemplate的return, 代码如下:

    import prettier from "prettier";
    
    return prettier.format(code, { parser: "babel" });
    
    • 1
    • 2
    • 3

    createPackageTemplate修改如下:

    import prettier from "prettier";
    
    return prettier.format(code, { parser: "json" });
    
    • 1
    • 2
    • 3

    最后

    至此,一个cli工具项目基本完成,基于此实现思路可以做很多平日里重复工作的优化

  • 相关阅读:
    护网HW面试常问——组件&中间件&框架漏洞(包含流量特征)
    innobackupex备份恢复,全备,增备,恢复单库单表
    “深入理解机器学习性能评估指标:TP、TN、FP、FN、精确率、召回率、准确率、F1-score和mAP”
    【看表情包学Linux】shell 命令及运行原理 | Linux 权限 | 文件权限的修改和转让 | 目录的权限 | Sticky bit 粘滞位
    润和软件DAYU 200的OpenHarmony赋能之旅
    【推荐系统】Embedding + MLP tensorflow特征处理 + 模型搭建实战 笔记
    L77.linux命令每日一练 -- 第11章 Linux系统管理命令 -- vmstat和mpstat
    SpringAMQP
    JavaScript【Array.isArray()、push()/pop()、shift()/unshift()、join()、concat()、reverse() 、slice()】(六)
    一文讲透支付宝沙箱的基本应用
  • 原文地址:https://blog.csdn.net/bertZuo/article/details/126111185