手把手教你實現腳手架工具Koa-generator

咱們平常中常常使用各類cli來加速咱們的工做,大家也必定和我同樣想知道這些cli內部都幹了什麼?接下來咱們就以實現一個koa-generator來打開腳手架工具的大門,來跟着我一步一步作吧:node

爲了加快咱們的學習進度,更快的理解cli,咱們這裏會省略一些內容,旨在幫助你們更快創建基本的概念和入門方法linux

需求分析

首先咱們先對咱們要實現的工具作一個簡單的需求分析:git

  1. 自動化生成koa初始項目結構
  2. 能夠自定義一些內容
  3. 發佈

是否是很簡單?沒錯,真的很簡單!github

逐步實現

1

想要自動化生成koa初始項目結構的前提,就是要知道咱們構建出來的結構是什麼樣的:npm

上圖就是咱們想要生成的項目結構json

明確了咱們的目的接下來就開始着手吧!api

2

2.1

建立文件夾bash

mkdir koa-simple-generator
複製代碼

2.2

進入項目目錄app

cd koa-simple-generator
複製代碼

2.3

初始化npm(等不及實踐就一路enter,後面也能夠再作修改)koa

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();
}
複製代碼

歡迎關注個人公衆號啊,學習資源,就業指導,心得交流盡在這裏 我相信大家會關注的是否是

相關文章
相關標籤/搜索