手把手教你實現腳手架工具Koa-generator
我們日常中經常使用各種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<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(); } 複製程式碼