咱們平常中常常使用各類cli來加速咱們的工做,大家也必定和我同樣想知道這些cli內部都幹了什麼?接下來咱們就以實現一個koa-generator來打開腳手架工具的大門,來跟着我一步一步作吧:node
爲了加快咱們的學習進度,更快的理解cli,咱們這裏會省略一些內容,旨在幫助你們更快創建基本的概念和入門方法
linux
首先咱們先對咱們要實現的工具作一個簡單的需求分析:git
是否是很簡單?沒錯,真的很簡單!github
想要自動化生成koa初始項目結構的前提,就是要知道咱們構建出來的結構是什麼樣的:npm
上圖就是咱們想要生成的項目結構json
明確了咱們的目的接下來就開始着手吧!api
建立文件夾bash
mkdir koa-simple-generator
複製代碼
進入項目目錄app
cd koa-simple-generator
複製代碼
初始化npm(等不及實踐就一路enter,後面也能夠再作修改)koa
npm init
複製代碼
打開咱們的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
複製代碼
安裝完,咱們新建一個目錄template
mkdir template
複製代碼
而後咱們能夠把咱們想要生成的目錄結構拷貝進去,這裏我就只是把koa2的目錄拷貝進去,如今咱們的目錄長這樣:
新建bin目錄,在bin下新建文件wowKoa
接下來就是關鍵了,咱們的全部工做都是在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<path>', '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();
}
複製代碼
歡迎關注個人公衆號啊,學習資源,就業指導,心得交流盡在這裏 我相信大家會關注的是否是