脚手架必备技能

前言

一说起脚手架,你肯定会想到vue-cli。有些人看到vue-cli那个酷炫的功能和交互就会打退堂鼓。如果我告诉你,我们只需要具备nodejs基础就能写出vue-cli那样的功能来,你是不是稍微会兴奋点呢?接下来跟着我的步伐一点一点揭开脚手架的面纱吧。

准备工作

首先我们的电脑需要有nodejs开发环境,下载安装请点击这里(已有环境请省略):nodejs
cmd输入node -v,出现下图版本号即安装成功。

image

开发步骤

初始化项目

npm init初始化一个项目;初始化成功之后会根据输入的配置生成一个package.json文件:

{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

添加脚手架命令和命令入口文件(test)

<strong>脚手架命令</strong>:在package.json中添加bin配置来指定自己定义的命令对应的可执行文件的位置,代码如下:

{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "bin": {
    "test": "bin/test.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

<strong>命令入口文件</strong>:根据package.json中的bin配置,我们在项目根目录下新增bin文件夹,并且在该文件夹下新建·test.js,代码如下:

#!/usr/bin/env node
console.log('test')

第一行是必须添加的,是指定这里用node解析这个脚本。默认找/usr/bin目录下,如果找不到去系统环境变量查找。

这时我们cmd输入test命令,你会发现报错了:

image

为什么找不到test命令呢?让我们先来理解下为啥我们安装了vue-cli之后,输入vue就不报错呢?带着疑问打开我们npm全局安装的文件目录:
image

请留意上图红色的地方,我们会发现我们熟悉的老朋友也在里头:cnpm,vue。这里我们先留下悬念,先回到刚才的脚手架项目。

<strong>link到全局</strong>:项目根目录下执行如下命令npm link,执行成功之后我们再查看我们全局npm的目录,这个时候我们会发现,多了几个项,细心地你肯定会发现,多出来的文件命名就是我们在package.json中配置的bin键:

image

这个时候我们再次打开cmd输入test试下:

image

对比上面失败的效果,你会发现这次系统识别了test命令,而且控制台输出了test。到这里上面的疑问应该都解开了:执行npm link,将当前的代码在npm全局目录下留个快捷方式,npm检测到package.json里面存在一个bin字段,它就同时在全局npm包目录下生成了一个可执行文件:

  1. npm link,链接到全局
  2. bin配合npm link,生成命令文件
  3. "test": "bin/test.js"cmd执行test,根据配置会去执行bin/test.js这个文件

添加命令

别人脚手架有的功能,我们也得有,例如:-V(--version) -H(--help)等。

commander

首先我们先安装下需要的commander库,然后为我们的test命令添加最基础的版本号查看命令:

#!/usr/bin/env node
const program = require('commander')
program
    .version(`version is ${require('../package.json').version}`)
    .parse(process.argv)

这里需要注意的是末尾的.parse(process.argv)的代码不能丢,如果没有则解析不了我们输入的参数,效果如下:

image

此外还可以添加.description .usage .option .action .command等;下面我们来重点说下.command和.action方法:其中,command允许我们注册一个命令(类似于vue create),代码如下:

program
    .version(`Version is ${require('../package.json').version}`)
    .description('A simple CLI for building initialize project include Wechat applet, Vue, Egg (nodejs)')
    .usage('<command> [options]')
    .command('create')
    .option("-l --list", "project list", listOption)
    .action((name, cmd) => {
        console.log(111)
        require('../lib/create')
    })
    .parse(process.argv)

终端输入test create查看效果:

image

我们注册的create命令,配合action方法打印出来111,其中我们可以在action中执行别的js文件,模块化开发我们的create命令,如下代码输入text create将会去执行lib/create.js

.action((name, cmd) => {
        require('../lib/create')
    })
image

参数解析:

const program = require('commander')
const inquirer = require('inquirer')
program.usage('<project-name>').parse(process.argv)
console.log(program.args)

控制台输入test create saa dfasas:

image

更多使用参考:commander

chalk

chalk包的作用是修改控制台中字符串的样式:例如字体样式,字体颜色,背景颜色等。


image

更多使用参考:chalk

log-symbols

为各种日志级别提供着色的符号(类似下载成功的√)


image

更多使用参考:log-symbols

ora

显示下载中,可以设置样式等;有start,fail,succeed方法等。


image

inquirer命令交互

这个库是我们可以和用户交互的工具;我们定义了两个问题:项目名称和版本号,create.js中写入以下代码:

const inquirer = require('inquirer')
getInquirer().then((res) => {
    console.log(res)
})
function getInquirer() {
    return inquirer.prompt([
        {
            name: 'projectName',
            message: 'project name',
            default: 'project',
        },
        {
            name: 'projectVersion',
            message: '项目版本号',
            default: '1.0.0',
        },
    ])
}

终端执行test create,上面的代码打印了执行后的拿到的参数如下图所示:

image

download-git-repo

该模块用于下载git上的模板项目,类似于vue-cli初始化一样,也是在git上下载一份代码回来本地完成开发环境搭建。下面代码封装了下载的代码,作用是根据传入的git地址克隆目标地址的代码到本地:

const ora = require('ora');
const chalk = require("chalk");
const logSymbols = require('log-symbols');
const download = require('download-git-repo');
const {
    spawnSync
} = require("child_process");

module.exports = function (target, downLoadURL) {
    let {
        error
    } = spawnSync("git", ["--version"]);
    if (error) {
        let downurl = downLoadURL.replace("direct:", "");
        console.log(logSymbols.warning, chalk.yellow("未添加Git环境变量引起,添加Git与git管理库的环境变量即可;"))
        console.log(logSymbols.info, chalk.green('或直接到模板地址下载:', downurl));
        return Promise.reject(error);
    }

    return new Promise((resolve, reject) => {
        const spinner = ora(`正在下载模板`)
        spinner.start();
        console.log(downLoadURL)
        download(downLoadURL, target, {
            clone: true
        }, (err) => {
            if (err) {
                let errStr = err.toString()
                spinner.fail();
                reject(err);

                if (errStr.includes("status 128")) {
                    console.log('\n', logSymbols.warning, chalk.yellow("Git默认开启了SSL验证,执行下面命令关闭后再重试即可;"))
                    console.log(logSymbols.info, chalk.green("git config --global http.sslVerify false"))
                }
            } else {
                spinner.succeed();
                resolve(target);
            }
        })
    })
}

下载模板时校验输入的文件目录名:

function checkProjectName(projectName) {
    let next = null;
    if (list.length) {
        if (
            list.filter((name) => {
                const fileName = path.resolve(process.cwd(), path.join(".", name));
                const isDir = fs.statSync(fileName).isDirectory();
                return name.indexOf(projectName) !== -1 && isDir;
            }).length !== 0
        ) {
            console.error(logSymbols.error, chalk.red(`项目${projectName}已经存在`));
            return Promise.resolve(false);
        }
        next = Promise.resolve(projectName);
    } else if (rootName === projectName) {
        next = inquirer
            .prompt([{
                name: "buildInCurrent",
                message: "当前目录为空,且目录名称和项目名称相同,是否直接在当前目录下创建新项目?",
                type: "confirm",
                default: true,
            }, ])
            .then((answer) => {
                return Promise.resolve(answer.buildInCurrent ? "." : projectName);
            });
    } else {
        next = Promise.resolve(projectName);
    }

    return next;
}

child_process

这是nodejs自带的一个包,用于开启子线程,非常强大,详情可以参考这里;这里利用spawnSync方法校验本地是否有git环境:

const { spawnSync } = require("child_process");
let { error } = spawnSync("git", ["--version"]);

到这里你已经掌握开发一个脚手架的必备技能了,行动起来吧!
(如何本文对你有启发的话,请给个赞吧,撸字造图不易😄)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容