从0开始搭建一套脚手架cli工具

时间:2022-11-07 13:55:44

前端开发者都会用脚手架搭建vue、react项目,那么如何搭建一套自己的脚手架cli工具呢?

一、 脚手架用到的工具

名称 用途
commander 用于命令行的自定义指令
download-git-repo 下载git仓库
fs-extra fs的一个扩展
handlebars 可以替换模板中的动态字符串
inquirer 交互式步骤提示问答
ora 动画效果
shelljs shell脚本
chalk 美化样式,高亮字体

二、初始化项目

  1. 初始化项目
npm init
  1. 新建bin文件夹,并在该文件夹下新建index.jsquestion.jscreate.js
    从0开始搭建一套脚手架cli工具

  2. 配置初始化后生成的package.json文件
    从0开始搭建一套脚手架cli工具

  3. npm link链接到全局
    主要是为了方便测试,把npm link在安装在本地目录。执行npm link之前,在package.json中指定bin 指定名字以及文件地址(上面我们已经配置过了), 然后执行npm link(mac系统加sudo)。

  4. 初步测试
    #!/usr/bin/env node需要固定在第一行,系统执行到这里后会沿着对应路径查找 node 并执行。

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

执行guilai-cli 命令

guilai-cli 

输出:
从0开始搭建一套脚手架cli工具
说明我们初步测试完成啦

三、获取版本

通过process.argv可以以数组形式获取命令行参数,通过用户传来的不同参数来判断执行不同操作

#! /usr/bin/env node
program.version(require('../package.json').version);
program.parse(process.argv);
guilai-cli -V

输出:
从0开始搭建一套脚手架cli工具

四、安装依赖

默认安装最新版本的命令,启动后可能会有一系列报错,博主的插件版本不会报错,报错可按上图的版本

yarn add chalk commander download-git-repo fs-extra handlebars inquirer ora shelljs 

五、 inquirer实现问答模式

  1. 在bin文件夹下新建question.js文件。
  • fs-extra继承了fs的所有方法,并在此基础上进行了扩展,fse.existsSync判断项目是否重名
```javascript
const fse = require("fs-extra")
const create = [
	{
		name: 'conf',
		type: 'confirm',
		message: '是否创建新的项目?',
	}, {
		name: 'name',
		message: '请输入项目名称:',
		validate: function (val) {
			if (!val) {
				return '亲,你忘了输入项目的名称哦~'
			}
			if (fse.existsSync(val)) {
				return '当前目录已存在同名的项目,请更换项目名'
			}
			return true
		},
		//如果上面为false,则该步骤就不执行
		when: res => Boolean(res.conf)
	}, {
		name: 'desc',
		message: '请输入项目的描述:',
		when: res => Boolean(res.conf)
	},
]
module.exports = {
	create
}
  1. index.js文件中
    如果在刚开始的选项是否新建项目选择false时,answers.conf的值就为false,将不会继续向下执行。
const program = require('commander');
const inquirer = require('inquirer');
const question = require("./question");
const initAction = () => {
	inquirer.prompt(question.create).then(answers => {
		if(answers.conf){
			console.log(answers)
			console.log('项目名称:', answers.name)//test
			console.log("正在拷贝项目,稍等-----")
		}
	})
}
program.version(require('../package.json').version);
program.command('init').description('创建项目').action(initAction);
program.parse(process.argv);

从0开始搭建一套脚手架cli工具

六、 shell实现拉取代码(或者用download-git-repo)

同样还是在index.js中,拉取代码到本地。

const initAction = () => {
	inquirer.prompt(
		question.create
	).then( async answers => {
		// shell脚本
		console.log('项目名为:', answers.name);
		console.log('正在拷贝项目,请稍等-------')
		const remote = "https://github.com/zbsguilai/catui.git"//克隆地址
		const currentName = "guilai-test"
		const targetName = answers.name;
		shell.exec(`
		  git clone ${remote} --depth=1
		  mv ${currentName} ${targetName}
		  rm -rf ./${targetName}/.git
		  cd ${targetName}
		  yarn
		`, (error, stdout, stderr) => {
			if (error) {
				console.error(`exec error:${error}`)
			}
			console.log(stdout)
			console.log(stderr)
			console.log("项目拷贝成功啦---------")
		})
	}).catch(error => {
		red(`❌ 程序出错 ❌`)
		process.exit(1);

	});
}

从0开始搭建一套脚手架cli工具

七、download-git-repo实现拉取代码(或者用shelljs)

在bin下新建create文件

  • process.exit(code)方法用于通过NodeJS中的退出代码结束同时运行的进程。
    参数:code:它可以是0或1。0表示没有任何类型的故障结束进程,而1表示由于某种故障而结束进程。
const download = require('download-git-repo')
const ora = require('ora')
const fse = require('fs-extra')
const handlebars = require('handlebars')
const myChalk = require('chalk')
const { red, yellow, green } = myChalk

function createProject(project) {
	//获取用户输入,选择的信息
	const { template, name, desc } = project;
	const spinner = ora("正在拉取框架...");
	spinner.start();
	download(template, name,{ clone: true }, async err => {
		if (err) {
			red(err);
			spinner.text = red(`拉取失败. ${err}`)
			spinner.fail()
			process.exit(1);
		} else {
			spinner.text = green(`拉取成功...`)
			spinner.succeed()
			spinner.text = yellow('请稍等,. 正在替换package.json中的项目名称、描述...')
			const multiMeta = {
				project_name: name,
				project_desc: desc
			}
			const multiFiles = [
				`${name}/package.json`
			]
			// 用条件循环把模板字符替换到文件去
			for (var i = 0; i < multiFiles.length; i++) {
				// 这里记得 try {} catch {} 哦,以便出错时可以终止掉 Spinner
				try {
					// 等待读取文件
					const multiFilesContent = await fse.readFile(multiFiles[i], 'utf8')
					// 等待替换文件,handlebars.compile(原文件内容)(模板字符)
					const multiFilesResult = await handlebars.compile(multiFilesContent)(multiMeta)
					// 等待输出文件
					await fse.outputFile(multiFiles[i], multiFilesResult)
				} catch (err) {
					// 如果出错,Spinner 就改变文字信息
					spinner.text = red(`项目创建失败. ${err}`)
					// 终止等待动画并显示 X 标志
					spinner.fail()
					// 退出进程
					process.exit(1)
				}
			}
			// 如果成功,Spinner 就改变文字信息
			spinner.text = yellow(`项目已创建成功!`)
			// 终止等待动画并显示 ✔ 标志
			spinner.succeed()
		}
	});
}

module.exports = createProject

index.js

const initAction = () => {
	inquirer.prompt(
		question.create
	).then( async answers => {
		if (answers.conf) {
			createProject(answers)
		} else {
			red(`???? 您已经终止此操作 ????`)
		}

	}).catch(error => {
		red(`❌ 程序出错 ❌`)
		process.exit(1);

	});
}

八、NPM发布

在此之前,博主有详细介绍将本地项目发布到npm,详细见本人底部

npm login//登录
npm publish//发布

九、优化脚手架

  • 使用ora实现动画效果(见上)
  • 使用chalk美化字体(见上)

十、常见错误

附:如何实现一个公共组件库上传到npm并在项目中使用