(满满干货)从零实现命令行脚手架工具——自动初始化项目工程以及发布到NPM

2021-12-01 11:41:40 浏览数 (1)

前言

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

实战

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

代码语言:javascript复制
https://github.com/maomincoding/strview-cli

第一步

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

代码语言:javascript复制
mkdir strviewCli

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

代码语言:javascript复制
npm init

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

第二步

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

代码语言:javascript复制
mkdir bin

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

代码语言:javascript复制
touch index.js
代码语言:javascript复制
mkdir config
代码语言:javascript复制
mkdir utils

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

代码语言:javascript复制
touch index.js
代码语言:javascript复制
touch checkDire.js

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

代码语言:javascript复制
- bin
 -- config
  --- index.js
 -- utils
  --- checkDire.js
 -- index.js
- package.json

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

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

代码语言:javascript复制
- 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:指定了项目的目标浏览器的范围
代码语言:javascript复制
{
  "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中几个属性如:nameversiondescriptionmainauthorkeywordsrepositorylicense可以根据自己的需求来定义。

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

第四步

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

代码语言:javascript复制
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',
     },
    },
   ],
  },
 ],
};

以上代码中导出一个对象,对象中有两个属性:npmUrlpromptTypeList

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

登录NPM

代码语言:javascript复制
npm login

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

发布到NPM

代码语言:javascript复制
npm publish

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

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

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

代码语言:javascript复制
{
 name: 'strview-app',
 value: {
   url: 'https://github.com/maomincoding/strview-app.git',
   gitName: 'strview-app',
   val: 'strview-app',
  },
}

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

以上就是configindex.js文件的配置。

第五步

下面,我们还在bin文件夹中,我们接下来编辑utilscheckDire.js文件。

代码语言:javascript复制
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);
  }
};

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

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

第六步

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

代码语言:javascript复制

#!/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);

我们从头开始看,我们会看到引入了fspathchalkcommanderinquirerchild_process

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

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

代码语言:javascript复制
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方法,也就是之前utilscheckDire.js文件内的方法。

代码语言:javascript复制
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。如果存在了,就执行下面的提示并退出终止进程。

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

代码语言:javascript复制
 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()方法来获取返回的数据。然后我们通过解构来分别获取到urlgitNameval这三个属性值。

代码语言:javascript复制
const { url, gitName, val } = result.type;

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

代码语言:javascript复制

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);
}

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

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

代码语言:javascript复制
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修复,并且可以向后兼容。

代码语言:javascript复制
npm publish

发布成功。

第八步

这里以strview-cli为例。

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

or

代码语言:javascript复制
strview-cli i <projectName>

结语

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

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

0 人点赞