前言本文将带你从零开始实现一个命令行脚手架工具初始化一个项目以及如何发布到NPM。首先,大家熟知的VueCLI使用命令行工具快速生成项目工程目录。这样我们只需要在开发项目之前在命令行输入命令,就可以快速生成项目工程,非常方便。那么,这么方便的命令行工具是如何实现的呢?让我们从实战开始。实战中,我会使用之前开发的脚手架工具strview-cli来介绍如何实现。这个脚手架工具的源码地址如下:https://github.com/maomincoding/strview-cliStep1首先我们新建一个文件夹strviewCli。mkdirstrviewCli然后在文件夹中初始化项目npminit后,会自动生成一个package.json文件。第二步,我们再创建一个文件夹,即bin。mkdirbin接下来我们在文件夹中创建一个index.js文件、config文件夹和utils文件夹。touchindex.jsmkdirconfigmkdirutils最后,在配置文件中创建一个index.js文件,在utils文件夹中创建一个checkDire.js文件。touchindex.jstouchcheckDire.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上面的第3步在两个步骤中,我们布置了命令行工具的基本结构。接下来,我们将对每个文件逐一注入灵魂~首先是.gitignore文件和README.md,这里就不细说了,大家可以根据自己的需要添加内容。其次,详细介绍的是package.json文件。下面是自己写的package.json。这里有几个重要的字段需要注意。name:项目名称version:版本号description:项目描述main:入口文件author:作者keywords:关键字bin:脚本执行入口repository:代码仓库license:许可证private:私有dependencies:依赖browserslist:指定项目的目标浏览器Scope工具{"name":"strview-cli","version":"1.1.1","description":"Strview.jsprojectscaffoldingtools.","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%","last2versions"]}上面package.json中的几个属性如:name,version,description,main,author,keywords,repository,license可以根据自己的需要定义。你可能会看到dependencies属性中有几个依赖,分别是chalk、commander、fs-extra和inquirer。这里先提一下,下面再详细介绍一下。不过需要注意的是,fs-extra模块增加了原生fs模块没有的文件系统方法,并为fs方法增加了promise支持。它还使用优雅的fs来防止EMFILE错误。它应该是fs的替代品。Step4接下来,我们将进入bin文件夹,然后,我们首先需要编辑config\index.js文件。module.exports={npmUrl:'https://registry.npmjs.org/strview-cli',promptTypeList:[{type:'list',message:'请选择模板类型topull:',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的地址。你怎么得到这个地址?您需要按照以下步骤操作:登录NPMnpmlogin,依次输入您的用户名、密码和邮箱。发布到npmnpmpublish发布成功后,会显示版本号。记住每次发布都要更改版本号,否则会出错。正常发布后,您可以打开npmURL并搜索您的命令行工具的名称。例如我的命令行工具strview-cli。URL是: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代表远程仓库地址,仓库名,value(一般和仓库一样姓名)。大家可以根据自己的需要进行配置,这里是我自己的strview-app配置。以上是config\index.js文件的配置。第五步下面,我们还是在bin文件夹下,我们接下来编辑utils\checkDire.js文件。constfs=require("fs");constchalk=require("chalk");module.exports=function(dir,name){letisExists=fs.existsSync(dir);if(isExists){console.log(chalk.red(`The${name}projectalreadyexistsindirectory.PleasetrytouseanotherprojectName`));process.exit(1);}};该文件中没有自定义部分,直接使用即可。这里我们看到引入了两个模块,分别是fs和chalk。Node.js内置的fs模块就是文件系统模块,负责文件的读写。chalk模块是为了美化命令行的输出风格,让输出的命令不再单调。我们看到这里导出了一个函数,函数有两个参数:dir和name。这里暂且不看这个函数。我们只需要传递两个参数。Step6接下来我们先分析bin\index.js文件。该文件是命令行工具的入口文件,非常重要。同样,这里也不需要自定义,直接使用即可。#!/usr/bin/envnodeconstfs=require('fs');constpath=require('path');constchalk=require('chalk');constcommander=require('commander');constinquirer=require('inquirer');constcheckDire=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').alias('i').description('Entertheprojectnameandinitializetheprojecttemplate').动作(异步(项目名称)=>{awaitcheckDire(path.join(process.cwd(),projectName),projectName);inquirer.prompt(promptTypeList).then((结果)=>{const{url,gitName,val}=result.type;console.log('你选择的模板类型信息如下:'+val);console.log('项目初始化复制获取...');if(!url){console.log(chalk.red(`${val}目前不支持该类型。..`));process.exit(1);}exec('gitclone'+url,function(error,stdout,stderr){if(error!==null){console.log(chalk.red(`clonefail,${error}`));return;}fs.rename(gitName,projectName,(err)=>{if(err){exec('rm-rf'+gitName,function(err,out){});console.log(chalk.red(`The${projectName}projecttemplatealreadyexist`));}else{if(fs.existsSync(`./${projectName}/.git/配置`)){exec('gitremotermorigin',{cwd:`./${projectName}`});console.log(chalk.green(`?The${projectName}projecttemplatesuccessfullycreate`));}}});});});});commander.parse(process.argv);我们从头开始,我们会看到fs、path、chalk、commander、inquirer、child_processpath模块的引入,提供了处理文件和目录的实用程序路径。Commander是一个完整的node.js命令行解决方案。它有很多用途。具体可以参考Commander中文文档:https://github.com/tj/commander.js/blob/master/Readme_zh-CN.mdinquirer是通用的交互式命令行用户界面的集合。package.json文件在通过npminit创建的时候,跟用户有很多交互,但是现在大部分项目都是通过脚手架创建的。使用脚手架时,最明显的是与命令行进行交互。如果你想在某个时候自己做一个脚手架或者和用户交互,这个时候就不得不提inquirer.js了。child_process模块??是nodejs的子进程模块,可以用来创建子进程,执行一些任务。比如在js中可以直接调用shell命令。引入导入的模块后,再引入下面的代码。您会看到下面的大部分代码都使用了commander方法。首先commander.version(version,'-v,--version'),.version()方法可以设置版本,然后可以使用-v或--version命令查看版本。可以通过.command('init')配置命令。这里的意思是初始化你的工程名,你可以根据自己的需要来设置。您也可以使用.alias('i')缩写初始化配置命令。原来的npminit现在也可以使用npmi命令。代码行.description('Entertheprojectnameandinitializetheprojecttemplate')中的.description方法是对上面初始化配置项目名的描述。.action((projectName,cmd)=>{...})在这行代码中,.action方法是定义命令的回调函数。我们发现它使用async/await关键字来处理异步。首先,等待checkDire(path.join(process.cwd(),projectName),projectName);传入的两个参数分别是项目所在目录和项目名称。这里先分析checkDire方法,也就是前面utils\checkDire.js文件中的方法。module.exports=function(dir,name){letisExists=fs.existsSync(dir);if(isExists){console.log(chalk.red(`The${name}projectalreadyexistsindirectory.PleasetrytouseanotherprojectName`));process.exit(1);}};fs.existsSync(dir)同步检测目录是否存在。如果目录存在,则返回true,如果目录不存在,则返回false。如果存在,则执行如下提示,退出终止进程。接下来,我们回到bin\index.js文件。然后再往下走,进入inquirer.prompt()这个方法。该方法的作用主要是启动提示界面(查询会话)。第一个参数是包含问题对象的question(数组)(使用reactive接口,也可以传入一个Rx.Observable实例),这里我们传入config\index.js文件中的promptTypeList属性,它是一个大批。promptTypeList:[{type:'list',message:'请选择模板typetopull:',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;那么下面就是输出命令和执行命令的环节了。我分成两部分来分析剩下的代码,第一部分如下:.log(chalk.red(`${val}Thistypeisnotsupportedatthemoment...`));process.exit(1);}我们这里有一个判断语句,就是判断远程仓库地址是否为假值。如果是假值,执行提示,Exit终止进程。接下来分析第二部分:exec('gitclone'+url,function(error,stdout,stderr){if(error!==null){console.log(chalk.red(`clonefail,${error}`));return;}fs.rename(gitName,projectName,(err)=>{if(err){exec('rm-rf'+gitName,function(err,out){});console.log(chalk.red(`${projectName}projecttemplate已经存在`));}else{if(fs.existsSync(`./${projectName}/.git/config`)){exec('gitremotermorigin',{cwd:`./${projectName}`});console.log(chalk.green(`?${projectName}projecttemplatesuccessfullycreate`));}}});}这部分主要是执行命令,同时也是最关键的部分,这部分首先使用exec()方法,第一个参数是要执行的命令,第二个参数是回调函数,首先我们执行exec('gitclone'+url)来下载远程工程,然后我们进入回调函数,如果有错误,输出提示并退出。否则,会使用fs.rename()方法重命名文件。因为rem的名字我们下载的ote仓库不一定和我们初始配置的名字一样。如果有错误,请删除远程项目的项目目录。否则判断./${projectName}/.git/config文件是否存在,执行exec('gitremotermorigin',{cwd:./${projectName}});命令删除远程仓库如果存在地址。这是因为需要自定义配置仓库地址,不能直接使用下载的仓库地址。最后提示创建成功。最后一行。commander.parse(process.argv);这行代码中.parse()的第一行第一个参数是要解析的字符串数组,也可以省略该参数,使用process.argv。指示,根据节点约定。步骤7至此,所有配置文件配置完毕。如果想开源,可以参考网上自己生成LICENSE文件。这个文件是一个软件license,你可以去github自动生成这个文件。最后,我们即将发布我们的命令行工具。请注意,您需要在发布前更改版本号。如果之前是1.0.0,现在可以改成2.0.0。也有关于如何定义这三个数字的意见。第一部分是主版本号,有变化表示发生了与之前版本不兼容的重大变化。第二部分是次要版本号,表示增加了新的特性,向后兼容。第三部分是修订号。如果有变化,就意味着有错误修复,并且是向后兼容的。npmpublish发布成功。这里的第八步以strview-cli为例。您可以在全球范围内安装您的脚手架。npmistrview-cli-g安装完成后,可以查看版本。strview-cli-v最后初始化项目,就是自定义项目的名字。strview-cliinit或strview-clii结论感谢您的阅读,希望我没有浪费您的时间。你可以自己封装一个普通的项目工程,你可以通过这种方式来初始化你的工程。这样,才会显得有说服力!!!哈哈哈~本文转载自微信公众号《前端之路》,可以通过以下二维码关注。转载本文请联系前端公众号。