原文:https://juejin.cn/post/6847902225025466376
详解前端脚手架开发排坑全指南【前端提效必须上干货】
我们业务中可以通过Vue-cli脚手架快速生成vue项目,同样我们也可以开发一款cli脚手架用于快速生成我们日常提炼出来的业务基础模型/架构。本文将详细讲解脚手架如何开发,所涉及到的技术细节和坑以及各种第三方包的讲解,确保即使是小白同学也可以照着做出来自己的cli。
装逼大法!提升逼格!!升职加薪!!!赢娶白富美!!!!你还在等什么?快快开始吧~~~
言归正传,首先思考一下我们的脚手架要帮助我们做什么事情?比如,这里我们就实现一个vta-cli
,通过在终端运行一个vta create my-app
就可以初始化我们日出提炼出来的一套Vue+Ts+ElementUi
的RBAC系统的基础项目架构。有了这个目标,我们就可以拆解要实现的步骤了:
- 支持终端命令
vta
vta create my-app
命令运行后,检查当前文件名的存在与否情况- 拉取们git上的模板项目到本地
- 拷贝当前下载的资源到我们目标地址
- 更新package.json等文件内容(比如name、author、version字段更改等)
- 在项目中初始化Git来管理项目
- 自动安装当前项目所需要的依赖
- 运行app
👇下面我们就一步一步讲解具体实现过程,跟上队别掉队哈~~~
✨✨初始化项目基础架构
- 首先创建文件夹,手动点击创建也行,终端运行命令也行:
# 终端创建项目根文件夹并进入到根文件夹
mkdir vta-cli && cd vta-cli
# 创建bin和src文件夹
mkdir bin src
# bin下创建init.js作为脚本的入口文件
cd bin && touch init.js
# 并在init.js中键入如下内容:
#!/usr/bin/env node
console.log('Hello,my bin!')
# 初始化npm的包管理文件, 根目录下执行
# 该命令会询问你很多配置参数,如果不想询问直接在后面加-y参数即可
npm init
项目基本的目录文件夹出来了,说下具体的目录作用,bin文件夹用于存放我们的命令入口文件,init.js作为入口文件(命名随你),src作为我们真正实现脚本命令逻辑的地方:
紧接着我们需要配置package.json
文件,在里面添加我们的脚本命令。打开我们的package.json
文件,在里面添加bin
字段:
{
"bin": {
"vta": "bin/init.js"
},
}
这就是我们定义了vta这个可以在终端运行的脚本命令,即运行vta
这个命令的时候,程序会去运行我们配置的bin/init.js
这个脚本文件。其实,根据npm的机制,当install
一个包的时候,会自动去查询其定义的bin命令,并把他添加到node_modules/.bin
文件中去,作为shell的命令可以去执行。因此当你的包安装到局部的项目中,那么其bin中的命令就是局部可运行的,安装到全局中则变成了全局可以运行的命令。
说明一下,并不是一定要是js文件,其实在linux系统中一切皆文件,是没有后缀名的规定的,至少为了让“人”好识别而已。
重点强调一下:init.js文件的第一行,一定是第一行,我们添加了#!/usr/bin/env node
代码,是指定了我们脚本的运行环境,和自定在我们运行vta命令的时候添加了node命令作为前缀,即实际运行的是node vta
。
- 接下来,为了方便我们测试,我们需要将这个包发不到本地的全局环境。我们可以通过如下命令:
# 终端运行命令(需在当前项目根目录下)
npm link
注意,npm link是当我们当前包link到本地的全局中,就好比如我们安装依赖时使用了-g
参数把一些包装到了全局环境一样,是用来方便我们本地开发时测试的,他可以让我们开发的时候自动热更新。如果不清楚npm link的
小伙伴,可以去npm官网查查npm link的
用法再继续往下学习。
但是,我想说的时候,很多小伙伴在这块可能会踩坑:
- 首先,最好把你的npm的镜像源改为npm本身的镜像源(如果你指定了为淘宝等其他的话);特别是你需要发布npm仓库的时候会失败。
- 其次,一定要在
package.json
的配置中把node_modules
等无关的文件夹去掉(或者指定我们需要的),也可以通过.gitignore
等配置文件忽略掉也可以,或者.npmrc
等。在哪里设置都可以,因为npm配置取值是有一套先后顺序的规则,有兴趣的话可以移步npm文档查阅。这里演示一下如何在package.json
文件的配置:
{
"files": [
"./bin",
"./src"
],
}
我们通过在package.json文件中指定files文件夹目录,即告诉npm我们实际应该包含的真正文件有哪些,比如我们只需要bin和src
文件夹,一些默认的文件像package.json
啊,其他的一些基础配置文件啊,即使你不添加,也会被默认包含进来的。这也是当我们把这个包发布到npm
所需要配置的,也就是需要哪些文件发布到npm仓库上。
注意,也可以通过排除的字段,exclude
。但是,很多时候指定我们需要哪些文件,可能更为方便哦!再强调一遍,node_modules一定要排除掉 ,不然npm link会巨慢而且会失败的概率大,小心踩坑~~
测试命令
# 终端运行
vta
# 那么脚本执行后,便会看到终端的输出
# 说明脚本执行成功了
再次强调,init.js首行一定要添加沙棒,如下:
#!/usr/bin/env node
console.log('运行测试')
❤️❤️命令行界面的解决方案
commander.js
是nodejs命令行界面的一个完整解决方案。可以帮助我们定义各种命令行命令/参数等等。比如我们想定义create命令啊,或者-v作为版本号查询的参数等等。那就先看下怎么使用吧:
- 安装
cnpm install commander -S
- 引入使用
// 在init.js中引入
const { Command } = require('commander');
// 导入当前根目录下的package.json文件,
// 为了获取对应的字段值,比如版本version
const package = require('../package');
// 初始化
const program = new Command();
- 定义版本命令和help命令的说明信息
//
// 如此,
program
.version(package.version, '-v, --version', 'display version for vta-cli')
.usage('<command> [options]');
//
通过调用version方法,定义命令行命令版本的功能,我们便可以在命令行输入vta -v
得到当前的版本信息。
调用usage方法,是定义的我们的辅助命令(help)的提示的文案标题,类似于定义table的表头的感觉,如下图,当我们输入vta -h时,就是定义的蓝色框框内展示的部分:
版本信息演示代码注意,这里version方法
的第三个参数,是我们定义的说明内容,如上图的红色部分。help
默认也是这个值
- 定义命令行参数
/**
* 定义vta的参数
*/
program
.option('-y, --yes', 'run default action')
.option('-f, --force', 'force all the question');
/**
* 可以通过判断,当用户输入了对应的这些参数时,
* 我们可以做一些操作:
*/
if (program.force) {
// do something..
}
通过option方法,定义我们的命令行参数,比如vta -f
,等同于vta --force
。注意,第一个参数是定义命令行参数,包含一个短的名称(1个字符)和一个长的名称,不能多了。第二个参数,就是定义的说明内容。注意,判断部分的代码,只能使用长的名称,不能判断短的,例如program.f
。
- 创建一个子命令
创建子命令是重要的一部分,比如我们使用vue create my-app
创建项目时, create
就是vue命令的子命令,my-app
是命令参数。这里我们也定义一个子命令:
/**
* 调用command方法,创建一个create命令,
* 同时create命令后面必须跟一个命令参数
* 如果你在终端运行vta create不加名称,则会报错提示用户
*/
program.command('create <name>')
// 定义该命令的描述
.description('create a vta template project')
// 为该命令指定一些参数
// 最后我们都可以解析到这些参数,然后根据参数实现对应逻辑
.option('-f, --force', '忽略文件夹检查,如果已存在则直接覆盖')
/**
* 最后定义我们的实现逻辑
* source表示当前定义的name参数
* destination则是终端的cmd对象,可以从中解析到我们需要的内容
*/
.action((source, destination) => {
/**
* 比如我们这里把实现逻辑放在了另一个文件中去实现,
* 方便代码解耦,
* 因为destination参数比较杂乱,其实还是在此处先解析该参数对应再传入使用吧
* 可以定义一个解析的工具函数
*/
new CreateCommand(source, destination)
});
如图,看下destination
对象到底是什么?还是满多的内容。我们需要关注的就是红色框框的这部分,这里就是我们定义的该命令的所有参数的列表,我们变量该列表,取图中蓝色的部分的值,解决--
后面的部分,然后作为key到整个cmd对象中取匹配,其值就是用户输入的参数的值。
cmd对象展示比如,可能会定义一个解析的工具函数:
/**
* parseCmdParams
* @description 解析用户输入的参数
* @param {} cmd Cammander.action解析出的cmd对象
* @returns { Object } 返回一个用户参数的键值对象
*/
exports.parseCmdParams = (cmd) => {
if (!cmd) return {}
const resOps = {}
cmd.options.forEach(option => {
const key = option.long.replace(/^--/, '');
if (cmd[key] && !isFunction(cmd[key])) {
resOps[key] = cmd[key]
}
})
return resOps
}
上述的解析方法实现方式和我们vue-cli的差不多。
- 完成解析
/**
* 切记parse方法的调用,一定要program.parse()方式,
* 而不是直接在上面的链式调用之后直接xxx.parse()调用,
* 不然就会作为当前command的parse去处理了,从而help命令等都与你的预期不符合了
*/
try {
program.parse(process.argv);
} catch (error) {
console.log('err: ', error)
}
最后一定要解析,不解析是拿不到对应参数program.parse(process.argv)
,也就是不会执行对应的命令等行为的。切记!切记!切记!!!更详细的命令请查询commander文档。
🌞对目标路径进行检查
从上面的步骤我们可以看出,我们已经定义好了vta create <name>
的命令了,即当我们运行vta create my-app
命令的时候,就会初始化我们定义的CreateCommand
类了。下面我们看看入如何实现这个逻辑:我们首先创建src/command/CreateCommand.js
这个文件来实现我们的逻辑:
/**
* class 项目创建命令
*
* @description
* @param {} source 用户提供的文件夹名称
* @param {} destination 用户输入的create命令的参数
*/
class Creator {
constructor(source, destination, ops = {}) {
this.source = source
this.cmdParams = parseCmdParams(destination)
this.RepoMaps = Object.assign({
repo: RepoPath, // 配置文件中放置的远程地址常量
temp: path.join(__dirname, '../../__temp__'),
target: this.genTargetPath(this.source)
}, ops);
this.gitUser = {};
this.spinner = ora();
this.init();
}
// 其他实例方法
// ...
}
// 最终导出这个class
module.exports = Creator;
我们看下这个构造函数我们用来做了什么事情,首先就是把实例化时传进来的参数赋值给this对象,供后面其他实例方法中去使用。然后定义了RepoMaps
属性设置我们的一些基础参数,像项目模板的地址repo
、我们本地cli项目内部临时存放的项目模板的地址temp
、和最终我们需要把项目安装到的目标地址taregt
。因为项目最终会安装到终端运行的地址下的位置,而你的脚手架包是被安装在其他地址的。
然后定义了gitUser用于存放用户的git信息,后面会通过自动执行命令获取相关的信息,然后最后我们会把信息塞到package.json文件中。
this.spinner = ora();
就是实例化一个菊花图,当我们在执行命令的时候可以调用this.spinner方法进行菊花转呀转!
下面我们来实现这个init初始化的方法吧:
// 初始化函数
async init() {
try {
// 检查目标路径文件是否正确
await this.checkFolderExist();
// 拉取git上的vue+ts+ele的项目模板
// 存放在临时文件夹中
await this.downloadRepo();
// 把下载下来的资源文件,拷贝到目标文件夹
await this.copyRepoFiles();
// 根据用户git信息等,修改项目模板中package.json的一些信息
await this.updatePkgFile();
// 对我们的项目进行git初始化
await this.initGit();
// 最后安装依赖、启动项目等!
await this.runApp();
} catch (error) {
console.log('')
log.error(error);
exit(1)
} finally {
this.spinner.stop();
}
}
从上面代码注释可以看到,我们的init方法,就是把一系列操作一次调用执行即可。最后先看一下配置文件吧:
exports.InquirerConfig = {
// 文件夹已存在的名称的询问参数
folderExist: [{
type: 'list',
name: 'recover',
message: '当前文件夹已存在,请选择操作:',
choices: [
{ name: '创建一个新的文件夹', value: 'newFolder' },
{ name: '覆盖', value: 'cover' },
{ name: '退出', value: 'exit' },
]
}],
// 重命名的询问参数
rename: [{
name: 'inputNewName',
type: 'input',
message: '请输入新的项目名称: '
}]
}
// 远程Repo地址
// 大家开发阶段,如果没有自己的项目,可以先调用我的这个地址练习
// 也可以随便一个地址练习都可以
exports.RepoPath = 'github:chinaBerg/vue-typescript-admin'
后面我们将看看这一系列方法该如何实现?
🌞终端的菊花图工具
首先介绍一下我们的小菊花吧!我们在执行各种操作的时候,比如拉模板数据等等,都是会有一定等待实际的,那么这个等待过程,我们可以在终端有个小菊花转转转,这样 会给用户更好的体验,让用户知道当前脚本在执行加载,如图(最左侧有个小菊花在转转转~~~):
ora就是这一款终端使用的菊花图工具,下面看看如何使用吧!
- 安装
cnpm install ora -S
- 使用
const ora = require('ora');
// ora参数创建spinner文字内容
// 也可以传递一个对象,设置spinner的周期、颜色等
// 调用start方法启动,最终返回一个实例
const spinner = ora('Loading start')
// 开启菊花转转
spinner.start();
// 停止
spinner.stop()
// 设置文案,后者菊花的color
spinner.text = '正在安装项目依赖文件,请稍后...';
spinner.color = 'green';
// 显示转成功的状态
spinner.succeed('package.json更新完成');
注意,文案的颜色,还是得靠chalk辅助。后面会介绍chalk。上个图片演示一下实际的运用:
更多详细的用户请查阅ora文档。
🌛五彩斑斓的控制台
chalk是一款可以让我们的控制台打印出各种颜色/背景的内容的工具,由此我们可以鲜明的区分各种提示内容,如下图(就问你骚不骚???):
安装
# 终端运行
cnpm i chalk -S
- 使用
const chalk = require('chalk');
// 比如,这里定义一个log对象
exports.log = {
warning(msg = '') {
console.warning(chalk.yellow(`${msg}`));
},
error(msg = '') {
console.error(chalk.red(`${msg}`));
},
success(msg = '') {
console.log(chalk.green(`${msg}`));
}
}
比如,上面我们封装了最简单的log方法,用于打印各种类型的信息时展示带颜色的内容。还有一点,我们说一下上面提到了的如何配合ora使用吧:
const chalk = require('chalk');
const ora = require('ora');
const spinner = ora('Loading start')
// 开启菊花转转
spinner.start(chalk.yellow('打印一个yellow色的文字'));
用法比较简单,不多说了,更多用法还是查阅文档吧!
✨fs-extra文件操作
在详细说明各个步骤实现的方式之前,我们先说一下在cli中使用的文件操作的库。node本身有fs操作,那么我们为什么还要引入fs-extra库呢?是因为他完全可以用来取代fs的库,省去了mkdirp``rimraf``ncp
等库等安装引入。用于拷贝、读取、删除等文件操作,而且提供了更多的功能等等。
- 安装
cnpm install fs-extra
具体的api的方法,请查阅文档fs-extra,后面讲解各个步骤具体实现的时候也会提及到。
✨检查文件夹是否合法
到了我们运行vta create my-app
的时候了,这时候我们就要考虑了,如果当前位置已经存在了同名的文件夹,那么我们肯定是不能直接覆盖的,而是要给用户选择,比如覆盖、重新创建一个新的文件夹、退出,如下图:
然后根据用户的不同选择作出对于的操作。下面我们看这个文件夹检查的具体实现:
checkFolderExist() {
return new Promise(async (resolve, reject) => {
const { target } = this.RepoMaps
// 如果create附加了--force或-f参数,则直接执行覆盖操作
if (this.cmdParams.force) {
await fs.removeSync(target)
return resolve()
}
try {
// 否则进行文件夹检查
const isTarget = await fs.pathExistsSync(target)
if (!isTarget) return resolve()
const { recover } = await inquirer.prompt(InquirerConfig.folderExist);
if (recover === 'cover') {
await fs.removeSync(target);
return resolve();
} else if (recover === 'newFolder') {
const { inputNewName } = await inquirer.prompt(InquirerConfig.rename);
this.source = inputNewName;
this.RepoMaps.target = this.genTargetPath(`./${inputNewName}`);
return resolve();
} else {
exit(1);
}
} catch (error) {
log.error(`[vta]Error:${error}`)
exit(1);
}
})
}
具体讲解:
- 我们定义了这个方法,返回的是一个Promise对象。
- 我们判断用户在输入
vta create my-app
的时候有没有在后面加-f
的参数,如果添加了参数则是告诉我们忽略检查直接往后走,就是默认覆盖的操作。通过调用fs.removeSync(target);
方法进行移除需要覆盖的文件; - 否则的话,我们则需要进行文件夹检查的实现逻辑了。通过
await fs.pathExistsSync(target)
逻辑进行判断当前文件夹名称是否已经存在,如果不存在则resolve告诉程序执行文件夹检查成功之后的程序。 - 如果同名的则给用户提示,让用户选择操作。下面将讲解如何在命令行进行交互。
❤️命令行交互
说到命令行交互,就要提到一个比较程序的库inquirer,这是一个用于node环境下进行命令行交互的库,支持单选、多选、用户输入、confirm询问等等操作。
- 安装
cnpm i inquirer -S
- 使用
const inquirer = require('inquirer');
// 定义询问的参数
// type表示询问的类型,是单选、多选、确认等等
// name可以理解为当前交互的标识符,其值为交互的结果
const InquirerConfig = {
// 文件夹已存在的名称的询问参数
folderExist: [{
type: 'list',
name: 'recover',
message: '当前文件夹已存在,请选择操作:',
choices: [
{ name: '覆盖', value: 'cover' },
{ name: '创建一个新的文件夹', value: 'newFolder' },
{ name: '退出', value: 'exit' },
]
}],
// 重命名的询问参数
rename: [{
name: 'inputNewName',
type: 'input',
message: '请输入新的项目名称: '
}]
}
// 使用
// 通过当前标识符获取交互的结果
// 比如,如下是一个单选的演示
const { recover } = await inquirer.prompt(InquirerConfig.folderExist);
// 如果用户选中的是“覆盖”选项
if (recover === 'cover') {
await fs.removeSync(target);
return resolve();
// 如果用户选中的是“创建新文件夹”选中
} else if (recover === 'newFolder') {
// 再次创建一个用户输入的交互操作
// 让用户输入新的文件夹名称
const { inputNewName } = await inquirer.prompt(InquirerConfig.rename);
this.RepoMaps.target = this.genTargetPath(`./${inputNewName}`);
return resolve();
// 如果用户选的是“退出”选项
} else {
exit(1);
}
- 如果用户选择了覆盖,我们就移除文件夹然后reolve
- 如果用户选择了创建新的文件夹,那么我们就再给出一个用于输入的终端,让用户输入新的文件夹名称。在用户输入完成后,我们把target的目标地址更新掉。
- 如果用户选择退出,我们则调用process.exit方法进行退出当前node程序即可。
❤️拉取git等远程仓库代码
在进行了文件夹监测完成之后,就应该是要下载我们在git上的项目资源了。下载资源我们是通过download-git-repo这个库来实现的。
- 安装
cnpm install download-git-repo -S
- 使用
const path = require('path');
const downloadRepo = require('download-git-repo');
// 下载repo资源
downloadRepo() {
// 菊花转起来~
this.spinner.start('正在拉取项目模板...');
const { repo, temp } = this.RepoMaps
return new Promise(async (resolve, reject) => {
// 如果本地临时文件夹存在,则先删除临时文件夹
await fs.removeSync(temp);
/**
* 第一个参数为远程仓库地址,注意是类型:作者/库
* 第二个参数为下载到的本地地址,
* 后面还可以继续加一个配置参数对象,最后一个是回调函数,
*/
download(repo, temp, async err => {
if (err) return reject(err);
// 菊花变成对勾
this.spinner.succeed('模版下载成功');
return resolve()
})
})
}
主要逻辑就是把资源下载到我们当前的临时文件夹位置,如果临时文件夹已经存在了那么就先删除临时文件夹。
👍把资源拷贝到目标地址,并移除无关文件
上面通过git上资源的下载,我们是下载到了cli目录内的临时文件内,那么我们还需要把资源移动到我们指定的位置,并且删除不必要的资源。所以我们这边会在utlis里面封装一个公共函数,用于资源的拷贝:
- 拷贝函数的封装
/**
* copyFiles 拷贝下载的repo资源
* @param { string } tempPath 待拷贝的资源路径(绝对路径)
* @param { string } targetPath 资源放置路径(绝对路径)
* @param { Array<string> } excludes 需要排除的资源名称(会自动移除其所有子文件)
*/
exports.copyFiles = async (tempPath, targetPath, excludes = []) => {
const removeFiles = ['./git', './changelogs']
// 资源拷贝
await fs.copySync(tempPath, targetPath)
// 删除额外的资源文件
if (excludes && excludes.length) {
await Promise.all(excludes.map(file => async () =>
await fs.removeSync(path.resolve(targetPath, file))
));
}
}
- 调用
// 拷贝repo资源
async copyRepoFiles() {
const { temp, target } = this.RepoMaps
await copyFiles(temp, target, ['./git', './changelogs']);
}
这里,我们移除了项目中本身含有的./git
、./changelogs
等文件,因为这些是该git项目需要的内容,而我们实际是不需要的。
👍自动更新package.json文件
通过上面的操作,我们已经把资源拷贝到我们的目标地址了。那么我们还想自动把package.json中的name、version、author等字段更新成我们需要的,应该怎么做呢?
/**
* updatePkgFile
* @description 更新package.json文件
*/
async updatePkgFile() {
// 菊花转起来!
this.spinner.start('正在更新package.json...');
// 获取当前的项目内的package.json文件的据对路径
const pkgPath = path.resolve(this.RepoMaps.target, 'package.json');
// 定义需要移除的字段
// 这些字段本身只是git项目配置的内容,而我们业务项目是不需要的
const unnecessaryKey = ['keywords', 'license', 'files']
// 调用方法获取用户的git信息
const { name = '', email = '' } = await getGitUser();
// 读取package.json文件内容
const jsonData = fs.readJsonSync(pkgPath);
// 移除不需要的字段
unnecessaryKey.forEach(key => delete jsonData[key]);
// 合并我们需要的信息
Object.assign(jsonData, {
// 以初始化的项目名称作为name
name: this.source,
// author字段更新成我们git上的name
author: name && email ? `${name} ${email}` : '',
// 设置非私有
provide: true,
// 默认设置版本号1.0.0
version: "1.0.0"
});
// 将更新后的package.json数据写入到package.json文件中去
await fs.writeJsonSync(pkgPath, jsonData, { spaces: '\t' });
// 停止菊花
this.spinner.succeed('package.json更新完成!');
}
这一块,上面代码注释已经写的非常清晰了,看一遍应该就晓得过程逻辑了吧!!!至于其中获取用户git信息的逻辑,后面马上会讲解到!!!
🌟获取Git信息
现在我们看下如何获取git信息的,我们定义了一个公共的方法getGitUser:
/**
* getGitUser
* @description 获取git用户信息
*/
exports.getGitUser = () => {
return new Promise(async (resolve) => {
const user = {}
try {
const [name] = await runCmd('git config user.name')
const [email] = await runCmd('git config user.email')
// 移除结尾的换行符
if (name) user.name = name.replace(/\n/g, '');
if (email) user.email = `<${email || ''}>`.replace(/\n/g, '')
} catch (error) {
log.error('获取用户Git信息失败')
reject(error)
} finally {
resolve(user)
}
});
}
我们都知道,在终端想查看用户的git信息,那么只需要键入git config user.name
即可,git config user.email
可以获取用户的邮箱。那么我们同样的在脚本中也执行这样的命令不就可以获取到了吗?
那么剩下的就是如何在终端执行shell命令呢?
✨✨node脚本中,执行指定的shell命令
node是通过开启一个子进程来执行脚本命令的,child_process说明是node提供的一个开启子进程的方法。于是我们可以封装一个方法用于执行子进程:
// node的child_process可以开启一个进程执行任务
const childProcess = require('child_process');
/**
* runCmd
* @description 运行cmd命令
* @param { string } 待运行的cmd命令
*/
const runCmd = (cmd) => {
return new Promise((resolve, reject) => {
childProcess.exec(cmd, (err, ...arg) => {
if (err) return reject(err)
return resolve(...arg)
})
})
}
所以上述获取git详情的操作其实就是调用的这个方法,让node开启一个子进程去运行我们的git命令,然后将结果返回出来。
❤️初始化git文件
// 初始化git文件
async initGit() {
// 菊花转起来
this.spinner.start('正在初始化Git管理项目...');
// 调用子进程,运行cd xxx的命令进入到我们目标文件目录
await runCmd(`cd ${this.RepoMaps.target}`);
// 调用process.chdir方法,把node进程的执行位置变更到目标目录
// 这步很重要,不然会执行失败(因为执行位置不对)
process.chdir(this.RepoMaps.target);
// 调用子进程执行git init命令,辅助我们进行git初始化
await runCmd(`git init`);
// 菊花停下来
this.spinner.succeed('Git初始化完成!');
}
这一块也是调用的我们封装的方法执行git命令而已。但是一定要注意、process.chdir(this.RepoMaps.target);
变更进程的执行位置,如果变更目录失败会抛出异常(例如,如果指定的 directory 不存在)。这步操作非常重要,切记!!切记!!!详细可以查阅process.chdir说明
🌟安装依赖
最后我们就需要自动暗转项目依赖了。本质也是调用子进程执行npm命令就可以了。这里我们直接指定了使用淘宝的镜像源,小伙伴们也可以扩展,根据用户的选择指定npm、yarn和其他镜像源等等,尽情发挥吧!!!
// 安装依赖
async runApp() {
try {
this.spinner.start('正在安装项目依赖文件,请稍后...');
await runCmd(`npm install --registry=https://registry.npm.taobao.org`);
await runCmd(`git add . && git commit -m"init: 初始化项目基本框架"`);
this.spinner.succeed('依赖安装完成!');
console.log('请运行如下命令启动项目吧:\n');
log.success(` cd ${this.source}`);
log.success(` npm run serve`);
} catch (error) {
console.log('项目安装失败,请运行如下命令手动安装:\n');
log.success(` cd ${this.source}`);
log.success(` npm run install`);
}
}
最后👍👍👍
vta-cli脚手架git源码地址,有兴趣的小伙伴可以查阅代码实现。也可以使用vta-cli快速初始化Vue+Ts+ElementUi
的RBAC后台管理系统的基础架构。安装vta-cli
的方法:
# 安装cli
npm i vta-cli -g
# 初始化项目
vta create my-app
vue-typescript-admin项目模板将会很快完善起来!!!也欢迎小伙伴们一起贡献代码哦~~
关于cli开发的讲解,到这就基本结束了!!!上面涵盖了常见的技术实现方案和注意细节,项目可以无痛上手的~~~有兴趣的小伙伴们可以照着封装自己的cli,把业务通用的场景解决方案抽离处理,提升自己的开发效率吧!
评论区