前言

这篇文章将带你从零实现一款命令行脚手架工具用于初始化项目以及如何发布到NPM。首先,我们所熟知的VueCLI就是采用命令行工具快速生成项目工程目录的,这样,我们每次开发项目前只需要在命令行中输入命令,然后就可以快速生成项目工程,非常方便。那么,这么方便的命令行工具是怎么实现的呢?下面我们就开始进入实战环节。

实战

我将会使用之前自己开发的一款脚手架工具​​strview-cli​​来介绍如何实现它的。这个脚手架工具的源码地址如下:

https://github.com/maomincoding/strview-cli

第一步

首先,我们姑且创建一个文件夹名字叫​​strviewCli​​。

mkdir strviewCli

然后,在文件夹中初始化项目

npm init

之后,自动生成一个​​package.json​​文件。

第二步

我们再创建一个文件夹,即​​bin​​。

mkdir bin

接着,在文件夹中我们创建一个​​index.js​​​文件、​​config​​​文件夹、​​utils​​文件夹。

touch index.js
mkdir config
mkdir utils

最后,在​​config​​​ 文件中创建一个​​index.js​​​文件,在​​utils​​​文件夹中创建一个​​checkDire.js​​。

touch index.js
touch checkDire.js

目前,我们文件目录结构为

- bin
-- config
--- index.js
-- utils
--- checkDire.js
-- index.js
- package.json

最后的最后,我们在根目录下,创建一个​​.gitignore​​​文件,以及​​README.md​​。

最后,命令行工具项目文件目录如下:

- bin
-- config
--- index.js
-- utils
--- checkDire.js
-- index.js
- package.json
- README.md
- .gitignore

第三步

上面两步,我们给我们的命令行工具奠定了基本结构。下面,我们将在一一为每个文件注入灵魂~

首先,​​.gitignore​​​文件和​​README.md​​,这里就不过多阐述了,可以根据自己的需要进行增添内容。

其次,详细介绍的就是​​package.json​​​文件。以下是我自己写的​​package.json​​,这里需要注意几个重要的字段。


  • name :项目名
  • version:版本号
  • description:项目描述
  • main:入口文件
  • author:作者
  • keywords:关键词
  • bin:脚本执行入口
  • repository:代码仓库
  • license:许可证
  • private:私有
  • dependencies:依赖
  • browserslist:指定了项目的目标浏览器的范围

{
"name": "strview-cli",
"version": "1.1.1",
"description": "Strview.js project scaffolding tools.",
"main": "index.js",
"author": {
"name": "maomincoding",
"email": "17864296568@163.com",
"url": "https://www.maomin.club"
},
"keywords": [
"strview",
"strview.js",
"strview-app",
"strview-cli"
],
"bin": {
"strview-cli": "./bin/index.js"
},
"repository": {
"type": "git",
"url": "https://github.com/maomincoding/strview-cli.git"
},
"license": "MIT",
"private": false,
"dependencies": {
"chalk": "^4.0.0",
"commander": "^5.0.0",
"fs-extra": "^9.0.0",
"inquirer": "^7.1.0"
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}

上面的​​package.json​​​中几个属性如:​​name​​​、​​version​​​、​​description​​​、​​main​​​、​​author​​​、​​keywords​​​、​​repository​​​、​​license​​可以根据自己的需求来定义。

你可能会看到​​dependencies​​​属性中有几个依赖,分别是​​chalk​​​、​​commander​​​、​​fs-extra​​​、​​inquirer​​​。这里先提一下,下面会详细介绍它们。不过,需要注意的是​​fs-extra​​模块是添加了本机fs模块中不包含的文件系统方法,并向fs方法添加了promise支持。它还使用优美的fs来防止EMFILE错误。它应该是fs的替代品。

第四步

接下来,我们将进入​​bin​​​文件夹中,然后,我们首先需要编辑​​config\index.js​​文件。

module.exports = {
npmUrl: 'https://registry.npmjs.org/strview-cli',
promptTypeList: [
{
type: 'list',
message: 'Please select the template type to pull:',
name: 'type',
choices: [
{
name: 'strview-app',
value: {
url: 'https://github.com/maomincoding/strview-app.git',
gitName: 'strview-app',
val: 'strview-app',
},
},
],
},
],
};

以上代码中导出一个对象,对象中有两个属性:​​npmUrl​​​和​​promptTypeList​​。

​npmUrl​​属性是命令行工具提交到NPM的地址。怎么得到这个地址呢?你需要按照下面步骤:

登录NPM

npm login

依次输入你的用户名、密码、邮箱。

发布到NPM

npm publish

发布成功后,会显示版本号。记住,每次发布都要更改版本号,否则会出错。

正常发布之后,你可以打开NPM网址,搜索你的命令行工具的名称。比如我的命令行工具​​strview-cli​​​。网址即:​​https://registry.npmjs.org/strview-cli​​。

​promptTypeList​​​属性中有一个​​choices​​属性,它是一个数组,你可以配置你远程项目工程仓库。数组每个元素是一个对象,比如这里的

{
name: 'strview-app',
value: {
url: 'https://github.com/maomincoding/strview-app.git',
gitName: 'strview-app',
val: 'strview-app',
},
}

​name​​​属性是你的项目工程名称,​​value​​​属性又是一个对象,里面有三个属性:​​url​​​、​​gitName​​​、​​val​​分别表示远程仓库地址仓库名值(一般与仓库名一致)。 你可以根据你的需要进行配置,这里是我配置的自己的​​strview-app​​。

以上就是​​config\index.js​​文件的配置。

第五步

下面,我们还在​​bin​​​文件夹中,我们接下来编辑​​utils\checkDire.js​​文件。

const fs = require("fs");
const chalk = require("chalk");

module.exports = function (dir, name) {
let isExists = fs.existsSync(dir);
if (isExists) {
console.log(
chalk.red(
`The ${name} project already exists in directory. Please try to use another projectName`
)
);
process.exit(1);
}
};

这个文件没有自定义的部分,你只需要直接用即可。这里我们看到引入两个模块,分别是​​fs​​​、​​chalk​​​。Node.js内置的​​fs​​​模块就是文件系统模块,负责读写文件。​​chalk​​模块是美化命令行输出样式,使输出命令不再单调。

我们看到这里导出一个函数,函数有两个参数:分别是​​dir​​​和​​name​​。我们这里先暂且不看这个函数,先只知道需要传两个参数就可以。

第六步

下面我们先分析​​bin\index.js​​文件,这个文件是命令行工具的入口文件,非常重要。同样,这里不需要自定义,直接用就可以。

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const commander = require('commander');
const inquirer = require('inquirer');
const checkDire = require('./utils/checkDire.js');
const { exec } = require('child_process');
const { version } = require('../package.json');
const { promptTypeList } = require('./config');

commander
.version(version, '-v, --version')
.command('init <projectName>')
.alias('i')
.description('Enter the project name and initialize the project template')
.action(async (projectName) => {
await checkDire(path.join(process.cwd(), projectName), projectName);
inquirer.prompt(promptTypeList).then((result) => {
const { url, gitName, val } = result.type;
console.log(
'The template type information you selected is as follows:' + val
);
console.log('Project initialization copy acquisition...');
if (!url) {
console.log(
chalk.red(`${val} This type is not supported at the moment...`)
);
process.exit(1);
}
exec('git clone ' + url, function (error, stdout, stderr) {
if (error !== null) {
console.log(chalk.red(`clone fail,${error}`));
return;
}
fs.rename(gitName, projectName, (err) => {
if (err) {
exec('rm -rf ' + gitName, function (err, out) { });
console.log(
chalk.red(`The ${projectName} project template already exist`)
);
} else {
if (fs.existsSync(`./${projectName}/.git/config`)) {
exec('git remote rm origin', { cwd: `./${projectName}` });
console.log(
chalk.green(
`✔ The ${projectName} project template successfully create`
)
);
}
}
});
});
});
});

commander.parse(process.argv);

我们从头开始看,我们会看到引入了​​fs​​​、​​path​​​、​​chalk​​​、​​commander​​​、​​inquirer​​​、​​child_process​​。

​path​​ 模块提供了用于处理文件和目录的路径的实用工具。

​commander​​是完整的 node.js 命令行解决方案,它有很多用法,具体你可以参照Commander中文文档:

https://github.com/tj/commander.js/blob/master/Readme_zh-CN.md

​inquirer​​​是通用交互式命令行用户界面的集合。开始通过​​npm init​​​创建​​package.json​​​文件的时候就有大量与用户的交互,而现在大多数工程都是通过脚手架来创建的,使用脚手架的时候最明显的就是与命令行的交互,如果想自己做一个脚手架或者在某些时候要与用户进行交互,这个时候就不得不提到​​inquirer.js​​了。

​child_process​​模块是nodejs的一个子进程模块,可以用来创建一个子进程,并执行一些任务。比如说就可以直接在js里面调用shell命令。

介绍完引入的模块,然后再介绍下面的代码。你会看到下面的代码大部分都用到了​​commander​​的方法。

首先,​​commander.version(version, '-v, --version')​​​,​​.version()​​​方法可以设置版本,之后就可以使用​​-v​​​或​​--version​​命令查看版本。

通过 ​​.command('init <projectName>')​​​ 可以配置命令。这里的意思是初始化你的项目名称,你可以根据自己的需求设置。也可以使用​​.alias('i')​​​简写初始化配置命令,原来​​npm init <projectName>​​​,现在也可以使用​​npm i <projectName>​​命令。

​.description('Enter the project name and initialize the project template')​​​这行代码中​​.description​​方法则是对上面初始化配置项目名的描述。

​.action((projectName, cmd) => {...})​​​这行代码中​​.action​​​方法是定义命令的回调函数,我们发现它使用了​​async/await​​​这组关键字用来处理异步。首先,​​await checkDire(path.join(process.cwd(), projectName), projectName);​​​传入两个参数分别为项目所在的目录、项目名称。我们这里先分析​​checkDire​​​方法,也就是之前​​utils\checkDire.js​​文件内的方法。

module.exports = function (dir, name) {
let isExists = fs.existsSync(dir);
if (isExists) {
console.log(
chalk.red(
`The ${name} project already exists in directory. Please try to use another projectName`
)
);
process.exit(1);
}
};

​fs.existsSync(dir)​​​以同步的方法检测目录是否存在。如果目录存在 返回 ​​true​​​ ,如果目录不存在 返回​​false​​。 如果存在了,就执行下面的提示并退出终止进程。

接着,我们又回到​​bin\index.js​​​文件。接着往下执行,到了​​inquirer.prompt()​​​这个方法,这个方法的作用主要是启动提示界面(查询会话),第一个参数是包含问题对象的问题(数组)(使用反应式接口,还可以传递一个Rx.Observable实例),这里我们传入的是​​config\index.js​​​文件中的​​promptTypeList​​属性,它是一个数组。

promptTypeList: [
{
type: 'list',
message: 'Please select the template type to pull:',
name: 'type',
choices: [
{
name: 'strview-app',
value: {
url: 'https://github.com/maomincoding/strview-app.git',
gitName: 'strview-app',
val: 'strview-app',
},
},
],
},
],

​inquirer.prompt()​​​这个方法返回一个Promise对象,所以这里可以​​then()​​​方法来获取返回的数据。然后我们通过解构来分别获取到​​url​​​、​​gitName​​​、​​val​​这三个属性值。

const { url, gitName, val } = result.type;

然后,下面就是输出命令以及执行命令的环节了。我分为两部分来分析剩余代码,第一部分如下:

console.log('The template type information you selected is as follows:' + val);
console.log('Project initialization copy acquisition...');

if (!url) {
console.log(chalk.red(`${val} This type is not supported at the moment...`));
process.exit(1);
}

我们这里有一个判断语句,就是判断远程仓库地址是否是假值,如果是假值的话,就执行提示并退出终止进程。

接着,我们分析第二部分:

exec('git clone ' + url, function (error, stdout, stderr) {
if (error !== null) {
console.log(chalk.red(`clone fail,${error}`));
return;
}
fs.rename(gitName, projectName, (err) => {
if (err) {
exec('rm -rf ' + gitName, function (err, out) { });
console.log(chalk.red(`The ${projectName} project template already exist`));
} else {
if (fs.existsSync(`./${projectName}/.git/config`)) {
exec('git remote rm origin', { cwd: `./${projectName}` });
console.log(
chalk.green(`✔ The ${projectName} project template successfully create`)
);
}
}
});
}

这部分主要是执行命令,也是最关键的部分。这部分首先使用了​​exec()​​方法,第一个参数是要执行的命令,第二个参数是回调函数。

首先,我们执行​​exec('git clone ' + url)​​​来下载远程项目,接着我们进入回调函数,如果有错误,就输出提示并退出。否则,将使用​​fs.rename()​​方法将文件重命名。因为我们下载的远程仓库名不一定跟我们初始化配置的名字一样。

如果错误,就删除远程项目工程目录。否则,就判断​​./${projectName}/.git/config​​​文件是否存在,如果存在就执行​​exec('git remote rm origin', { cwd:​​​./${projectName}​​});​​命令删除远程仓库地址。这是因为需要自定义配置仓库地址,而不是直接使用下载的仓库地址。最后,提示创建成功。

最后一行。​​commander.parse(process.argv);​​​这行代码中​​.parse()​​​的第一个参数是要解析的字符串数组,也可以省略参数而使用​​process.argv​​。指明,按 node 约定。

第七步

到了这一步,所有配置文件都配置完成了。如果你想开源的话,你可以参照线上自己生成一个​​LICENSE​​文件。这个文件是软件许可证,可以去github去自动生成这个文件。

最后,我们就要发布我们这个命令行工具了。注意,在发布之前,需要改一下你的版本号。 如之前是​​1.0.0​​​,现在可以改成​​2.0.0​​。具体这三个数字怎么定义,也有说法。第一部分为主版本号,变化了表示有了一个不兼容上个版本的大更改。第二部分为次版本号,变化了表示增加了新功能,并且可以向后兼容。第三部分为修订版本号,变化了表示有bug修复,并且可以向后兼容。

npm publish

发布成功。

第八步

这里以​​strview-cli​​为例。

  1. 你可以全局安装你的脚手架。
npm i strview-cli -g
  1. 安装完成之后,你可以查看版本。
strview-cli -v
  1. 最后,就是初始化项目了,​​<projectName>​​是自定义项目名称。
strview-cli init <projectName>

or

strview-cli i <projectName>

结语

谢谢阅读,希望没有耽误你的时间。

你可以自己封装一个常用的项目工程,可以通过这种方式来初始化你的项目。这样,才会显得有逼格!!!哈哈哈~

关于作者

作者:Vam的金豆之路