我们日常中经常使用各种cli来加速我们的工作,你们也一定和我一样想知道这些cli内部都干了什么?接下来我们就以实现一个koa-generator来打开脚手架工具的大门,来跟着我一步一步做吧:
为了加快我们的学习进度,更快的理解cli,我们这里会省略一些内容,旨在帮助大家更快建立基本的概念和入门方法
需求分析
首先我们先对我们要实现的工具做一个简单的需求分析:
- 自动化生成koa初始项目结构
- 可以自定义一些内容
- 发布
是不是很简单?没错,真的很简单!
逐步实现
1
想要自动化生成koa初始项目结构的前提,就是要知道我们构建出来的结构是什么样的:
上图就是我们想要生成的项目结构
明确了我们的目的接下来就开始着手吧!
2
2.1
创建文件夹
mkdir koa-simple-generator
2.2
进入项目目录
cd koa-simple-generator
2.3
初始化npm(等不及实践就一路enter,后面也可以再做修改)
npm init
2.4
打开我们的package.json,如下
将下面的代码复制到package.json里
{
"name": "koa-simple-generator",
"version": "1.0.0",
"description": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"main": "bin/wowKoa",
"bin": {
"koa2": "./bin/wowKoa"
},
"dependencies": {
"commander": "2.7.1",
"mkdirp": "0.5.1",
"sorted-object": "1.0.0"
},
"devDependencies": {
"mocha": "2.2.5",
"rimraf": "~2.2.8",
"supertest": "1.0.1"
},
"engines": {
"node": ">= 7.0"
}
}
1. dependencies和devDependencies简单来说就是应用的依赖包,devDependencies只会在开发环境安装
2. 这句话的意思是我们的这个工具需要node7.0及以上的版本才能支持
"engines": {
"node": ">= 7.0"
}
重点是这两句
"main": "bin/wowKoa",
"bin": {
"wowKoa": "./bin/wowKoa"
},
意思是默认执行的是bin目录下的wowKoa,
执行wowKoa的命令,执行的也是bin目录下的wowKoa,
2.5
接下来安装我们的依赖吧
npm i
2.6
安装完,我们新建一个目录template
mkdir template
然后我们可以把我们想要生成的目录结构拷贝进去,这里我就只是把koa2的目录拷贝进去,现在我们的目录长这样:
2.7
新建bin目录,在bin下新建文件wowKoa
2.8
接下来就是关键了,我们的所有工作都是在bin下的wowKoa文件里完成的
直接复制粘贴下面的,然后进入项目目录运行node bin/wowKoa
就能看到结果了
代码我已经大部分都注释啦
#!/usr/bin/env node
// 告诉Unix和Linux系统这个文件中的代码用node可执行程序去运行
var program = require('commander');
var mkdirp = require('mkdirp');
var os = require('os');
var fs = require('fs');
var fsm = require('fs-extra')
var path = require('path');
var readline = require('readline');
var pkg = require('../package.json');
// 退出node进程
var _exit = process.exit;
// s.EOL属性是一个常量,返回当前操作系统的换行符(Windows系统是\r\n,其他系统是\n)
var eol = os.EOL;
var version = pkg.version;
// Re-assign process.exit because of commander
// TODO: Switch to a different command framework
process.exit = exit
program
/**
* .version('0.0.1', '-v, --version')
* 1版本号<必须>,
* 2自定义标志<可省略>:默认为 -V 和 --version
*
* .option('-n, --name', 'name description', 'default name')
* 1 自定义标志<必须>:分为长短标识,中间用逗号、竖线或者空格分割;标志后面可跟必须参数或可选参数,前者用 <> 包含,后者用 [] 包含
* 2 选项描述<省略不报错>:在使用 --help 命令时显示标志描述
* 3 默认值<可省略>
*
* .usage('[options] [dir]')
* 作用:只是打印用法说明
*
* .parse(process.argv)
* 作用:用于解析process.argv,设置options以及触发commands
* process.argv获取命令行参数
*
*
* Commander提供了api来取消未定义的option自动报错机制, .allowUnknownOption()
*/
.version(version, '-v, --version')
.allowUnknownOption()
.usage('[options] [dir]')
.option('-f, --force', 'force on non-empty directory')
.parse(process.argv);
// 没有退出时执行主函数
if (!exit.exited) {
main();
}
/**
* 主函数
*/
function main() {
// 获取当前命令执行路径
var destinationPath = program.args.shift() || '.';
// 根据文件夹名称定义appname
// 用于package.json里的name
var appName = path.basename(path.resolve(destinationPath));
// 判断当前文件目录是否为空
emptyDirectory(destinationPath, function (empty) {
// 如果为空或者强制执行时,就直接生成项目
if (empty || program.force) {
createApplication(appName, destinationPath);
} else {
// 否则询问
confirm('当前文件夹不为空,是否继续?[y/N] ', function (ok) {
if (ok) {
// 控制台不再输入时销毁
process.stdin.destroy();
createApplication(appName, destinationPath);
} else {
console.error('aborting');
exit(1);
}
});
}
})
}
/**
* Check if the given directory `path` is empty.
* 判断文件夹是否为空
* @param {String} path
* @param {Function} fn
*/
function emptyDirectory(path, fn) {
fs.readdir(path, function (err, files) {
if (err && 'ENOENT' != err.code) throw err;
fn(!files || !files.length);
});
}
/**
* 在给定路径中创建应用
* @param {String} path
*/
function createApplication(app_name, path) {
// wait的值等于complete函数执行的次数
// 用于选择在哪一次complete函数执行后执行控制台打印引导使用的文案
var wait = 1;
console.log();
function complete() {
if (--wait) return;
var prompt = launchedFromCmd() ? '>' : '$';
console.log();
console.log(' install dependencies:');
console.log(' %s cd %s && npm install', prompt, path);
console.log();
console.log(' run the app:');
// 根据控制台的环境不同打印不同文案(linux或者win)
if (launchedFromCmd()) {
console.log(' %s SET DEBUG=koa* & npm start', prompt, app_name);
} else {
console.log(' %s DEBUG=%s:* npm start', prompt, app_name);
}
}
copytmp(complete, path,app_name)
}
// 拷贝模拟里的文件到本地
function copytmp(fn, destinationPath,app_name) {
// 获取模板文件的文件目录
tmpPath = path.join(__dirname, '..', 'template')
// 创建目录
fsm.ensureDir(destinationPath + '/'+app_name)
.then(() => {
// 拷贝模板
fsm.copy(tmpPath, destinationPath + '/'+app_name, err => {
if (err) return console.log(err)
fn()
})
})
}
/**
* Determine if launched from cmd.exe
* 判断控制台环境(liux或者win获取其他)
*/
function launchedFromCmd() {
return process.platform === 'win32' &&
process.env._ === undefined;
}
/**
* node是使用process.stdin和process.stdout来实现标准输入和输出的
* readline 模块提供了一个接口,用于一次一行地读取可读流(例如 process.stdin)中的数据。 它可以使用以下方式访问:
*/
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// 控制台问答
function confirm(msg, callback) {
rl.question(msg, function (input) {
callback(/^y|yes|ok|true$/i.test(input));
});
}
// 控制台问答
function wrieQuestion(msg, callback) {
rl.question(msg, function (input) {
// rl.close()后就不再监听控制台输入了
rl.close();
callback(input)
});
}
/**
* 通过fs读取模板文件内容
*/
function loadTemplate(name) {
return fs.readFileSync(path.join(__dirname, '..', 'template', name), 'utf-8');
}
/**
* echo str > path.
* 写入文件
* @param {String} path
* @param {String} str
*/
function write(path, str, mode) {
fs.writeFileSync(path, str, { mode: mode || 0666 });
console.log(' \x1b[36mcreate\x1b[0m : ' + path);
}
/**
* 这里是主要解决在winodws上的一些bug,不用卡在这里,核心目的就是为了能让进程优雅退出
* Graceful exit for async STDIO
*/
function exit(code) {
// flush output for Node.js Windows pipe bug
// https://github.com/joyent/node/issues/6247 is just one bug example
// https://github.com/visionmedia/mocha/issues/333 has a good discussion
function done() {
if (!(draining--)) _exit(code);
}
var draining = 0;
var streams = [process.stdout, process.stderr];
exit.exited = true;
streams.forEach(function (stream) {
// submit empty write request and wait for completion
draining += 1;
stream.write('', done);
});
done();
}
欢迎关注我的公众号啊,学习资源,就业指导,心得交流尽在这里
我相信你们会关注的是不是