Node —— 写一个实用cli工具
2021-04-07 本文已影响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
二、编写程序
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
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
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.js
和App.vue
中的router-link
我们日常开发项目的时候,每次新加一个页面都要编辑router.js和App.vue里面加一个链接,这样的重复操作给我们带来了很大的心智负担,所以我们接下来要实现的就是运行命令,自动生成。
看一下此时的目录结构
image.png
在我们克隆的项目vue-template
中新建一个tempalte文件夹,以及文件App.vue.hbs
和router.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.js
和App.vue
自动生成