手把手教你实现脚手架工具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();
}
复制代码

欢迎关注个人公众号啊,学习资源,就业指导,心得交流尽在这里 我相信大家会关注的是否是

相关文章
相关标签/搜索