美文网首页前端开发那些事儿
Node —— 写一个实用cli工具

Node —— 写一个实用cli工具

作者: 有一种感动叫做丶只有你懂 | 来源:发表于2021-04-07 17:44 被阅读0次

    学习目标
    用node写实用的cli工具,是我们工程化的一个必经之路,本文也能激起大家学习node的兴趣,

    本文实现一个vue脚手架,这个脚手架的主要实现的功能就是:

    • 自动克隆github项目
    • 自动安装依赖
    • 自动npm run serve
    • 自动打开浏览器
    • 我们在view文件夹下面添加xxx.vue文件的时候,router.js自动生成

    一、创建工程

    创建文件


    image.png

    安装依赖

    npm i commander download-git-repo ora handlebars figlet clear chalk open -s
    

    编写kkb.js文件

    #!/usr/bin/env node
    //指定解释器类型
    console.log ('Hello My-Cli');
    

    编写package.json文件,新增bin属性,kkb就是我们注册的命令

    {
      "name": "vue-auto-router-cli",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "bin": {
        "kkb": "./bin/kkb.js"
      },
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "dependencies": {
        "chalk": "^4.1.0",
        "clear": "^0.1.0",
        "commander": "^7.2.0",
        "download-git-repo": "^3.0.2",
        "figlet": "^1.5.0",
        "handlebars": "^4.7.7",
        "open": "^8.0.5",
        "ora": "^5.4.0"
      }
    }
    

    将我们编写的cli工具安装到全局,(就跟npm install xxx -g一样),在项目根目录下面运行以下命令

    npm link
    

    验证
    window+r打开一个新的终端,执行kkb命令,是否配置成功,成功则输出Hello My-Cli

    image.png

    二、编写程序

    1.使用commander定制命令行
    • command相当于注册了一个init命令name就是后面跟的参数,命令具体的操作在action里面写,commander会将命令后面的参数传到这个action接收的这个函数参数里面
    #!/usr/bin/env node
    //指定解释器类型
    const program = require ('commander');
    program.version (require ('../package.json').version); //指定版本号
    program.command ('init <name>').description ('初始化项目中...').action (payload => {
      console.log (payload);
    }); //相当于注册一个命令
    program.parse (process.argv); //process描述的是主进程  process.argv是命令后面的参数,整个program是通过解析后面的参数来完成的
    

    执行命令kkb init project

    输出:project
    
    2.打印一个欢迎界面

    文件目录


    image.png

    编辑kkb.js

    #!/usr/bin/env node
    //指定解释器类型
    const program = require ('commander');
    program.version (require ('../package.json').version); //指定版本号
    program
      .command ('init <name>')
      .description ('初始化项目...')
      .action (require ('../lib/init.js')); //相当于注册一个命令
    program.parse (process.argv); //process描述的是主进程  process.argv是命令后面的参数,整个program是通过解析后面的参数来完成的
    
    

    新建init.js文件

    const {promisify} = require ('util'); //promisify 将异步函数转换为Promise类型的;
    
    const figlet = promisify (require ('figlet')); //艺术字;
    const chalk = require ('chalk'); //粉笔;
    const clear = require ('clear'); //清屏;
    const log = content => console.log (chalk.red (content)); //封装一个log方法,用chalk染色;
    module.exports = async name => {
      clear ();首先清屏
      const data = await figlet ('Welcome My Cli');
      log (data);
    };
    

    运行kkb init name

    image.png
    3.实现克隆github项目的功能
    • 使用download-git-repo这个包
    • ora:进度条

    新建download.js文件

    const {promisify} = require ('util');
    const ora = require ('ora'); //进度条
    const download = promisify (require ('download-git-repo'));
    module.exports = async (repo, name) => {
      const process = ora ('下载中...' + name);
      process.start ();
      await download (repo, name);
      process.succeed ();
    };
    

    编辑init.js文件

    const {promisify} = require ('util'); //promisify 将异步函数转换为Promise类型的;
    
    const figlet = promisify (require ('figlet')); //艺术字;
    const chalk = require ('chalk'); //粉笔;
    const clear = require ('clear'); //清屏;
    const log = content => console.log (chalk.red (content)); //封装一个log方法,用chalk染色;
    const download = require ('./download');
    module.exports = async name => {
      clear ();
      const data = await figlet ('Welcome My Cli');
      log (data);
      log ('开始克隆项目');
      await download ('github:su37josephxia/vue-template', name);
    };
    

    运行kkb init vue-template命令,成功克隆项目

    image.png
    image.png
    4.安装依赖

    项目成功克隆之后,接下来常规操作安装依赖,运行npm install命令,然后npm run serve启动,那么在nodejs里面我们如何写脚本让他自动执行呢?

    • 使用Promise封装spawn方法,创建一个子进程让他去执行npm install这个命令。
    • 因为子进程执行,我们是看不见的,所以通过pipe(管道)对接到主进程,让他执行过程能在我们终端显示出来,你也可以把proc.stdout.pipe (process.stdout); proc.stderr.pipe (process.stderr);这俩句注释掉,结果就是控制台不会打印任何信息,但项目依然能启动。如此,显而易见。
    • 为什么要用npm.cmd,可以参考这篇文章
    • 关于child_process这个模块你可以自己下去仔细学习下,这个包很重要,这篇文章不做赘述。
    const {promisify} = require ('util'); //promisify 将异步函数转换为Promise类型的;
    
    const figlet = promisify (require ('figlet')); //艺术字;
    const chalk = require ('chalk'); //粉笔;
    const clear = require ('clear'); //清屏;
    const log = content => console.log (chalk.red (content)); //封装一个log方法,用chalk染色;
    const open = require ('open');
    // const download = require ('./download');
    
    // 封装spawn方法
    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 ();
        });
      });
    };
    module.exports = async name => {
      clear ();
      const data = await figlet ('Welcome My Cli');
      log (data);
      // 克隆项目
      // log ('开始克隆项目');
      // await download ('github:su37josephxia/vue-template', name);//克隆github项目
    
      // 安装依赖
      log ('开始安装依赖');
      await spawn ('npm.cmd', ['install'], {cwd: `./${name}`});
    };
    
    
    image.png
    image.png
    5.启动项目并且打开浏览器
    • open:使用系统浏览器打开一个网址;
    const {promisify} = require ('util'); //promisify 将异步函数转换为Promise类型的;
    
    const figlet = promisify (require ('figlet')); //艺术字;
    const chalk = require ('chalk'); //粉笔;
    const clear = require ('clear'); //清屏;
    const log = content => console.log (chalk.red (content)); //封装一个log方法,用chalk染色;
    const open = require ('open');
    // const download = require ('./download');
    
    // 封装spawn方法
    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 ();
        });
      });
    };
    module.exports = async name => {
      clear ();
      const data = await figlet ('Welcome My Cli');
      log (data);
      // 克隆项目
      // log ('开始克隆项目');
      // await download ('github:su37josephxia/vue-template', name);//克隆github项目
    
      // 安装依赖
      // log ('开始安装依赖');
      // await spawn ('npm.cmd', ['install'], {cwd: `./${name}`});
    
      // 打开浏览器安装运行
      open ('http://localhost:8080');
      await spawn ('npm.cmd', ['run', 'serve'], {
        cwd: `./${name}`,
      });
    };
    
    image.png
    6.自动生成router.jsApp.vue中的router-link

    我们日常开发项目的时候,每次新加一个页面都要编辑router.js和App.vue里面加一个链接,这样的重复操作给我们带来了很大的心智负担,所以我们接下来要实现的就是运行命令,自动生成。

    看一下此时的目录结构


    image.png

    在我们克隆的项目vue-template中新建一个tempalte文件夹,以及文件App.vue.hbsrouter.js.hbs,这俩个文件将来要给handelbars这个包使用。

    //App.vue.hbs
    <template>
      <div id="app">
        <div id="nav">
          <router-link to="/">Home</router-link> 
          {{#each list}}
          | <router-link to="/{{name}}">{{name}}</router-link>
          {{/each}}
        </div>
        <router-view/>
      </div>
    </template>
    
    <style>
    #app {
      font-family: 'Avenir', Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
      margin-top: 60px;
    }
    </style>
    
    
    //router.js.hbs文件
    import Vue from 'vue'
    import Router from 'vue-router'
    import Home from './views/Home.vue'
    
    Vue.use(Router)
    
    export default new Router({
      mode: 'history',
      base: process.env.BASE_URL,
      routes: [
        {
          path: '/',
          name: 'home',
          component: Home
        },
        {{#each list}}
        {
          path: '/{{name}}',
          name: '{{name}}',
          component: () => import('./views/{{file}}')
        },
        {{/each}}
      ]
    })
    
    

    lib文件夹下面创建refresh.js

    const fs = require ('fs');
    const handlebar = require ('handlebars'); //
    module.exports = async () => {
      const list = fs.readdirSync ('./vue-template/src/views').map (v => ({
        name: v.replace ('.vue', '').toLowerCase (),
        file: v,
      })); //文件集合
      compile (
        {list},
        './vue-template/src/router.js',
        './vue-template/template/router.js.hbs'
      ); //生成router.js
      compile (
        {list},
        './vue-template/src/App.vue',
        './vue-template/template/App.vue.hbs'
      );//生成App.vue
      function compile (meta, filePath, templatePath) {
        if (fs.existsSync (templatePath)) {
          const content = fs.readFileSync (templatePath).toString ();
          const data = handlebar.compile (content) (meta);
          fs.writeFileSync (filePath, data);
          console.log (`${filePath}创建成功`);
        }
      }
    };
    
    

    编辑kkb.js,新增一个命令kkb refresh

    #!/usr/bin/env node
    //指定解释器类型
    const program = require ('commander');
    program.version (require ('../package.json').version); //指定版本号
    
    // 初始化项目
    program
      .command ('init <name>')
      .description ('初始化项目...')
      .action (require ('../lib/init.js')); //相当于注册一个命令
    
    // 刷新路由文件
    program
      .command ('refresh')
      .description ('自动生成路由...')
      .action (require ('../lib/refresh'));
    
    program.parse (process.argv); //process描述的是主进程  process.argv是命令后面的参数,整个program是通过解析后面的参数来完成的
    

    views下面新增一个文件,执行kkb refresh命令,我们会看到router.jsApp.vue自动生成

    image.png image.png

    相关文章

      网友评论

        本文标题:Node —— 写一个实用cli工具

        本文链接:https://www.haomeiwen.com/subject/mnmskltx.html