如何搭建自己的脚手架

概要

什么是脚手架?

  • 脚手架是一种工具,通常是一个全局命令行工具,用于帮助开发人员快速创建和初始化项目文件和目录结构。

脚手架的基本能力

  • 命令行执行能力: 脚手架具备能够在命令行中执行的能力,允许用户通过命令行命令来触发脚手架的各种功能。

  • 命令行交互能力: 脚手架可以与用户进行命令行交互,提示用户提供必要的信息或选项,以便根据用户的输入进行项目初始化。

  • 项目初始化代码下载能力: 脚手架能够从远程代码仓库下载项目的初始化代码,以便在本地创建项目。

如何实现一个自己的脚手架工具

要实现一个自己的脚手架工具,可以按照以下步骤进行实现

  1. 创建自定义全局命令: 使用Node.js或其他适合的编程语言创建一个全局命令行工具,这个命令可以在任何地方通过命令行调用。通常需要使用包管理器(如npm)来安装全局命令。

  2. 命令参数接受处理: 在您的脚手架工具中,编写代码以接受命令行参数,这些参数将指导脚手架执行不同的操作。您可以使用命令行解析库来帮助处理和解释这些参数。

  3. 下载远程项目代码: 实现代码下载功能,您可以使用Git或其他版本控制工具来从远程仓库中获取项目的初始化代码。确保您的脚手架可以处理不同的代码源(例如GitHub、GitLab等)。

  4. 项目初始化完成提示: 在项目初始化完成后,向用户提供适当的提示信息,指导他们下一步的操作。这可以包括运行项目、安装依赖项或其他相关任务。

  5. 测试和文档: 编写测试用例来确保脚手架的稳定性和可靠性。另外,提供清晰的文档,解释如何安装、使用和定制您的脚手架工具。

  6. 发布和分享: 将您的脚手架工具发布到适当的包管理器(如npm),以便其他开发人员可以轻松安装和使用它。分享您的脚手架的代码和文档,以促进社区的贡献和改进。

通过遵循这些步骤,您可以创建一个功能强大的脚手架工具,使项目初始化和开发过程更加高效和方便。

创建自定义全局命令

  1. 创建文件结构: 我们首先需要设置一个适当的文件结构来开始创建自定义全局命令。在这个过程中,我们会新建一个名为code的文件夹,然后在其中创建一个名为bin的子文件夹。在bin文件夹中,我们将建立一个名为cli.js的文件。这个cli.js文件将包含我们自定义命令的执行逻辑。

  2. 项目初始化: 接下来,我们要初始化一个npm项目,以便管理我们的全局命令。在命令行中,我们进入项目的根目录(在code文件夹下),并执行以下命令:

npm init -y

在这一步中,我们可以将项目命名为my-cli,并且对其他项目选项采用默认值,因为这些选项可以稍后在package.json中修改。

  1. 配置全局命令: 在我们的项目的package.json文件中,有一个特殊的字段叫做bin,它用于配置全局命令。我们将在这里定义我们的全局命令与cli.js文件之间的关联,这样npm就知道如何将我们的命令映射到全局环境中。
{
    "name": "my-cli",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "bin": {
        "my-cli": "bin/cli.js"
    },
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "ISC"
}
  1. 挂载全局命令: 最后,为了使我们的自定义全局命令在系统中可用,我们需要在命令行中执行以下命令,这将使我们的全局命令链接到全局npm包:
npm link

这一步骤完成后,我们就可以在任何地方使用my-cli命令来执行我们在cli.js文件中定义的逻辑了。

比如我们可以在cli.js中写点代码:

#! /usr/bin/env node

console.log('我的cli')

然后运行命令:

my-cli

此时我们就会在控制它上看到我们的打印内容:我的cli

#! /usr/bin/env node 是一个在Unix-like操作系统中用于指定脚本的解释器的特殊注释。这个注释的作用是告诉操作系统使用哪个解释器来执行脚本文件。在这种情况下,它指定了使用Node.js作为解释器来执行该脚本。

具体解释如下:

  • #!:这个字符序列称为"shebang",它告诉操作系统以下内容是用于解释执行脚本的命令。

  • /usr/bin/env:这是一个可执行文件,通常用于在系统的环境变量中查找指定的命令或程序。在这里,它被用于寻找可用的node命令。

  • node:这是Node.js的可执行文件。当操作系统执行这个脚本时,它会找到/usr/bin/env node,然后使用Node.js来解释和执行接下来的脚本内容。

因此,#! /usr/bin/env node 帮助我们确保无论用户的系统上Node.js的安装路径如何,都可以正确地执行我们的Node.js脚本。这对于全局命令行工具非常有用,因为它们需要在不同的系统上运行,而这些系统可能有不同的Node.js安装路径。这个shebang注释确保了脚本的可移植性和跨平台兼容性。

  1. 获取命令行参数:

在我们的自定义全局命令中,通常需要获取和解析命令行参数,以便根据用户的输入来执行不同的操作。我们可以在cli.js文件中编写如下代码来获取命令行参数:

console.log(process.argv);

当我们运行像my-cli --help这样的命令时,控制台将会打印出以下内容:

[ '/usr/local/bin/node', '/usr/local/bin/my-cli', '--help' ]

在这个输出中,process.argv数组包含了命令行中的所有参数。具体来说:

  • process.argv[0] 是 Node.js 的可执行文件路径(在这里是/usr/local/bin/node)。
  • process.argv[1] 是我们的全局命令的路径(在这里是/usr/local/bin/my-cli)。
  • 后续项包含传递给命令的参数,例如--help

我们可以根据这些参数来编写逻辑,以决定如何处理用户的输入。这是实现命令行交互能力的一部分,允许我们根据用户的需求来执行不同的功能操作。比如:

if(process.argv[2] === '--help'){
    console.log('获取到了参数')
}

但在实际情况中,我们可以使用一些第三方的包去管理我们的指令;比如:commander(命令参数处理工具)。

Commander命令参数处理工具

Commander 是一个强大的Node.js模块,用于处理命令行参数。以下是使用Commander的基本步骤:

安装Commander

首先,我们需要在npm上搜索并安装Commander模块,这可以通过以下命令完成:

npm install commander

引入和使用Commander

在我们的项目中,我们可以引入Commander模块并开始使用它来处理命令行参数。通常,我们会在脚本的开头加上如下注释来指定Node.js作为解释器:

#! /usr/bin/env node

接下来,在我们的cli.js文件中,引入Commander并调用program.parse(process.argv)来处理命令行参数:

const { program } = require('commander');

program.parse(process.argv);

添加命令和选项

现在,我们可以添加命令和选项来定制我们的命令行工具的行为。例如,我们可以使用program.option()来定义选项:

program.option('-f --framework <framework>', '设置框架')

这个例子中,我们定义了一个名为-f--framework的选项,还提供了一个描述。这将允许用户在命令行中设置一个框架参数。

program.option('-f --framework <framework>', '设置框架') 是使用Commander库定义命令行选项的语句。让我们解释这行代码的作用:

  • program.option():这是Commander库提供的一个方法,用于定义命令行选项。

  • -f--framework:这两个部分是选项的名称,它们以两种形式存在,一种是短格式(单字符,前面带一个短划线 -),一种是长格式(多字符,前面带两个短划线 --)。用户可以使用其中任何一种形式来指定选项。

  • <framework>:这是用尖括号括起来的参数占位符,表示该选项需要一个参数值。在这种情况下,用户需要在选项后面提供一个框架名称作为参数。

  • '设置框架':这是选项的描述文本,用于向用户解释选项的用途或目的。在帮助信息中,这个描述文本将帮助用户了解选项的含义。

综合起来,这行代码的作用是定义一个名为-f(或--framework)的命令行选项,该选项需要用户提供一个框架名称作为参数。用户可以通过命令行来设置这个框架选项,而描述文本 '设置框架' 会在帮助信息中显示,以帮助用户理解这个选项的目的。

示例用法:

my-cli --framework react

在这个示例中,--framework选项后面的react是用户提供的参数值,用来指定所选的框架。

显示帮助信息

使用Commander,我们还可以轻松地为我们的命令行工具生成帮助信息。只需在脚本中调用program.parse(process.argv)后,运行时,如果用户输入--help选项,将会显示帮助信息。例如:

my-cli --help

这将在终端中显示以下信息:

Usage: my-cli [options]

Options:
  -f --framework <framework>  设置框架
  -h, --help                 display help for command

处理命令行输入

最后,我们可以通过检查命令行输入来执行不同的操作。在我们的例子中,如果用户没有提供-f--framework参数,Commander会提示错误,要求用户输入框架参数:

my-cli -f

这将导致终端显示以下错误消息:

error: option '-f --framework <framework>' argument missing

用户可以通过在-f后面输入框架参数来解决此错误,如:

my-cli -f hello

这样,我们就可以使用Commander轻松处理命令行参数,增加命令和选项,以及生成帮助信息,从而更好地控制和定制我们的命令行工具的行为。

如何处理自定义命令参数

在命令行工具开发中,我们经常需要处理自定义的命令参数,以满足用户的需求。在这篇文章中,我们将探讨如何使用Node.js和Commander库来实现这一目标。我们的目标是创建一个命令行工具,允许用户通过输入my-cli create xxx来创建一个项目。同时,我们将通过--help命令自动生成帮助信息。

首先,让我们来看看在bin/cli.js中的代码:

#! /usr/bin/env node

const { program } = require('commander');

// 定义一个选项
program.option('-f --framework <framework>', '设置框架');

// 定义一个自定义命令
program
  .command('create <project> [other...]') //定义命令
  .alias('crt') //设置别名
  .description('创建项目') //备注
  .action((project, args) => { //回调函数
    console.log(project);
    console.log(args);
  });

// 解析命令行参数
program.parse(process.argv);
  • 我们首先引入了Commander库,并定义了一个选项,使用了-f(短格式)和--framework(长格式),允许用户设置一个框架名称作为参数。

  • 接着,我们使用program.command()定义了一个名为create(或别名crt)的自定义命令。这个命令接受一个project参数和可选的other参数。

  • 我们定义了一个.action()回调函数,用于处理命令的实际操作。在这里,我们简单地打印了project参数和args参数,以便演示如何访问这些自定义命令参数。

  • 最后,我们使用program.parse(process.argv)来解析命令行参数。

现在,让我们运行以下命令来测试:

my-cli create myproject otherOptions

结果将是:

myproject
[ 'otherOptions' ]

我们成功地捕获和处理了命令行参数。

另外,我们可以运行--help命令来查看自动生成的帮助信息:

my-cli --help

输出内容:

Options:
  -f --framwork <framwork>          设置框架
  -h, --help                        display help for command

Commands:
  create|crt <product> [orther...]  创建项目
  help [command]                    display help for command

你会发现,Commander自动将我们的自定义命令和选项集成到了帮助信息中,以帮助用户了解如何正确使用我们的命令行工具。

通过这个例子,我们学到了如何使用Node.js和Commander库来处理自定义命令参数,以及如何生成帮助信息,使我们的命令行工具更加友好和易用。

逻辑代码模块化拆分

在开发命令行工具时,将代码模块化拆分是一个良好的实践,有助于保持代码的可维护性和可扩展性。下面,我们将根据不同的功能进行拆分,并创建模块化的代码结构。

首先,我们新建一个lib/core文件夹,并在其中创建以下模块。

1. help.js 模块

lib/core文件夹下创建一个help.js文件,其内容如下:

const setupHelp = (program) => {
    program.option('-f --framework <framework>', '设置框架');
};

module.exports = setupHelp;

此模块用于配置命令行工具的帮助选项,并将其导出供其他模块使用。

2. myCommander.js 模块

接下来,在lib/core文件夹中创建一个myCommander.js文件:

const setupMyCommander = (program) => {
    program.command('create <project> [other...]')
        .alias('crt')
        .description('创建项目')
        .action((project, args) => {
            console.log(project);
            console.log(args);
        });
};

module.exports = setupMyCommander;

这个模块用于配置自定义命令,并将其导出供其他模块使用。

3. action.js 模块

最后,在lib/core文件夹下创建一个action.js文件,其中包含命令的实际操作逻辑:

const performAction = (project, args) => {
    console.log(project);
    console.log(args);
};

module.exports = performAction;

这个模块包含了实际操作的代码,并将其导出供其他模块使用。

接下来,让我们在 /bin/cli.js 中引入这些模块,并将它们组合在一起:

#! /usr/bin/env node
const { program } = require('commander');

// 导入模块
const setupHelp = require('../lib/core/help');
const setupMyCommander = require('../lib/core/myCommander');
const performAction = require('../lib/core/action');

// 配置帮助选项
setupHelp(program);

// 配置自定义命令
setupMyCommander(program);

// 设置命令的实际操作
program.action(performAction);

// 解析命令行参数
program.parse(process.argv);

通过这种方式,我们将代码逻辑模块化拆分,使其更易于理解和维护。同时,也提供了更好的可扩展性,以便将来添加更多功能或命令时能够更轻松地扩展我们的命令行工具。

命令行交互问答工具: Inquirer

通常,在命令行中需要与用户进行交互式的操作,比如询问用户一些问题,获取输入等。这时候,我们可以使用 Inquirer 库来方便地实现这些功能。

安装 Inquirer

首先,我们需要安装 Inquirer 库,可以通过以下命令进行安装:

npm install --save inquirer@^8.0.0

使用示例

安装完成后,我们可以在根目录下创建一个 /test/inquirer.js 文件来测试一下:

const inquirer = require('inquirer');

inquirer.prompt([
    {
        type: 'input',
        name: 'username',
        message: '你的名字',
    }
]).then(res => {
    console.log(res);
});

在当前文件夹内运行以下命令:

node inquirer.js

这将提示你输入名字,然后打印出你输入的内容:

{ username: '11' }

创建一个交互式命令

接下来,我们可以尝试改造之前的 create 命令。

打开 /lib/core/action.js 文件,引入 inquirer,同时在根目录下创建 /config.js 用于保存一些配置项:

/** action.js */
const inquirer = require('inquirer');
const config = require('../../config');

const myAction = (project, args) => {
    inquirer.prompt([
        {
            type: 'list',
            name: 'framework',
            message: '请选择你使用的框架',
            choices: config.framework,
        }
    ]).then(res => {
        console.log(res);
    });
}

module.exports = myAction;

config.js 文件内容如下:

module.exports = {
    framework: ['egg', 'koa', 'express']
}

然后在 /bin/cli.js 文件中引入并使用这个模块:

const { program } = require('commander');

const setupHelp = require('../lib/core/help');
const setupMyCommander = require('../lib/core/myCommander');
const performAction = require('../lib/core/action');

setupHelp(program);
setupMyCommander(program);

program.action(performAction);

program.parse(process.argv);

现在,在项目根目录下运行以下命令:

my-cli create nodetest

将会在控制台中出现一个选项供你选择,选择完成后回车,你将得到你选择的值:

? 请选择你使用的框架 koa
{ framework: 'koa' }

通过这样的方式,我们使用 Inquirer 实现了一个交互式命令,使我们的命令行工具更加灵活和友好。

当使用inquirer.prompt()时,我们可以传递一个配置对象,以定制交互式的提示。以下是一些常用的配置项及其说明:

  1. type(类型): 定义了用户交互的类型。常用的类型包括:

    • input: 接收用户的文本输入。
    • list: 提供一个列表供用户选择。
    • checkbox: 提供一个多选框列表供用户选择。
    • confirm: 提供一个是/否 的确认选择。
  2. name(名称): 定义了交互结果在返回的对象中的键名。

  3. message(消息): 显示给用户的问题或提示信息。

  4. default(默认值): 设定一个默认值,如果用户直接按下回车,则使用该值。

  5. choices(选项): 用于listcheckbox类型的交互,提供可供选择的选项。

  6. validate(验证): 可以是一个函数,用于验证用户的输入,返回 true 表示合法,返回错误消息字符串表示非法。

  7. filter(过滤): 可以是一个函数,用于对用户的输入进行处理,返回处理后的值。

  8. when(条件): 可以是一个函数,根据前一个问题的答案来确定是否需要显示当前问题。

  9. pageSize(分页大小): 用于list类型的交互,设置显示的选项数量。

  10. prefix(前缀): 在问题显示前,可添加一个前缀。

  11. suffix(后缀): 在问题显示后,可添加一个后缀。

  12. transformer(转换器): 用于对用户输入和交互结果的进一步处理。

  13. loop(循环): 当typelist时,选择到最后一个选项会回到第一个选项。

  14. validate(验证): 可以是一个函数,用于对用户的输入进行验证。

  15. default(默认值): 设置一个默认值,如果用户直接按下回车,则使用该值。

这些参数可以根据需求组合使用,以实现丰富的交互体验。

示例:

inquirer.prompt([
    {
        type: 'input',
        name: 'username',
        message: '请输入你的名字',
        default: 'John Doe', // 默认值
        validate: function (value) {
            if (value.length) {
                return true; // 输入合法
            }
            return '请输入你的名字'; // 输入非法时的错误消息
        }
    },
    {
        type: 'list',
        name: 'color',
        message: '选择一种颜色',
        choices: ['Red', 'Green', 'Blue'], // 提供的选项
        pageSize: 5 // 分页大小
    },
    // ...
]).then(answers => {
    console.log(answers);
});

如何下载远程项目代码

download-git-repo是一个用于从 Git 仓库下载代码的 Node.js 模块。它可以将远程仓库中的代码下载到本地的文件夹中。

安装插件:

npm install download-git-repo

使用方法

首先,在我们之前创建的 /config.js 文件中添加了远程项目的仓库地址:

module.exports = {
    framwork: ['egg', 'koa', 'express'],
    framworkUrl: {
        'egg': 'https://github.com/LonJinUp/lonjin-helper',
        'koa': 'https://github.com/LonJinUp/wxTabBar',
        'express': 'https://github.com/LonJinUp/vue-prettier-plugin'
    }
}

接着,我们创建了 /lib/core/download.js 文件,引入了 download-git-repo 并定义了一个函数:

const download = require('download-git-repo')
const downFramwork = (url, project) => {
    download(`direct:${url}`, project, { clone: true }, (error) => {
        console.log(error)
    })
}

module.exports = downFramwork

关于download-git-repodownload参数详细解释如下:

download(repository, destination, options, callback)
  • repository: 一个字符串,表示 Git 仓库的地址。可以是 HTTPS 或者 SSH 形式的 Git 仓库地址,也可以是 GitHub、GitLab 或者 Bitbucket 等托管服务的地址。

  • destination: 一个字符串,表示目标文件夹的路径,代码将会下载到该文件夹中。

  • options: 一个包含配置选项的对象,可以用来定制下载的行为。常用选项包括:

    • clone: 一个布尔值,表示是否使用 Git 的克隆功能,默认为 false。如果设置为 true,将会使用 Git 克隆整个仓库,否则将会直接下载 ZIP 归档文件(速度更快,但不支持私有仓库)。

    • checkout: 一个字符串,表示在下载后将会检出的分支或标签名。

    • depth: 一个整数,表示 Git 克隆时的深度(即历史记录的数量),默认为 undefined,表示克隆完整的历史记录。

  • callback: 一个回调函数,当下载完成时会调用该函数,可以获取到错误信息。

最后,我们回到了 /lib/core/action.js 中,导入了我们编写好的函数,并做了一些调整。我们也将之前的 inquirer 改成了 await 的写法以提高代码的可读性。

const inquirer = require('inquirer')
const config = require('../../config')
const download = require('./download')

const myAction = async (project, args) => {
    const answer = await inquirer.prompt([
        {
            type: 'list',
            name: 'framework',
            message: '请选择你使用的框架',
            choices: config.framework
        }
    ])
    // 下载模板
    download(config.frameworkUrl[answer.framework], project)
}

module.exports = myAction

最后,我们执行如下命令:

my-cli create text-download-1 

然后根据你的选择,会下载相应的远程代码,并且文件夹的名称将与 create 命令后面的参数一致。