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

167 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

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

一、 脚手架用到的工具

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

二、初始化项目

  1. 初始化项目
npm init
  1. 新建bin文件夹,并在该文件夹下新建index.jsquestion.jscreate.js 在这里插入图片描述

  2. 配置初始化后生成的package.json文件 在这里插入图片描述

  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 

输出: 在这里插入图片描述 说明我们初步测试完成啦

三、获取版本

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

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

输出: 在这里插入图片描述

四、安装依赖

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

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

在这里插入图片描述

六、 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);

	});
}

在这里插入图片描述

七、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并在项目中使用