Node —— 写一个实用cli工具

学习目标
用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

推荐阅读更多精彩内容