详解前端脚手架开发排坑全指南【前端提效必须上干货】

咱们业务中能够经过Vue-cli脚手架快速生成vue项目,一样咱们也能够开发一款cli脚手架用于快速生成咱们平常提炼出来的业务基础模型/架构。本文将详细讲解脚手架如何开发,所涉及到的技术细节和坑以及各类第三方包的讲解,确保即便是小白同窗也能够照着作出来本身的cli。javascript

装逼大法!提高逼格!!升职加薪!!!赢娶白富美!!!!你还在等什么?快快开始吧~~~html

感受像作梦同样

言归正传,首先思考一下咱们的脚手架要帮助咱们作什么事情?好比,这里咱们就实现一个vta-cli,经过在终端运行一个vta create my-app就能够初始化咱们日出提炼出来的一套Vue+Ts+ElementUi的RBAC系统的基础项目架构。有了这个目标,咱们就能够拆解要实现的步骤了:前端

  • 支持终端命令vta
  • vta create my-app命令运行后,检查当前文件名的存在与否状况
  • 拉取们git上的模板项目到本地
  • 拷贝当前下载的资源到咱们目标地址
  • 更新package.json等文件内容(好比name、author、version字段更改等)
  • 在项目中初始化Git来管理项目
  • 自动安装当前项目所须要的依赖
  • 运行app

👇下面咱们就一步一步讲解具体实现过程,跟上队别掉队哈~~~vue

✨✨初始化项目基础架构

  • 首先建立文件夹,手动点击建立也行,终端运行命令也行:
# 终端建立项目根文件夹并进入到根文件夹
mkdir vta-cli && cd vta-cli

# 建立bin和src文件夹
mkdir bin src

# bin下建立init.js做为脚本的入口文件
cd bin && mkdir init.js

# 并在init.js中键入以下内容:
#!/usr/bin/env node
console.log('Hello,my bin!')

# 初始化npm的包管理文件, 根目录下执行
# 该命令会询问你不少配置参数,若是不想询问直接在后面加-y参数便可
npm init
复制代码

项目基本的目录文件夹出来了,说下具体的目录做用,bin文件夹用于存放咱们的命令入口文件,init.js做为入口文件(命名随你),src做为咱们真正实现脚本命令逻辑的地方:java

vta-cli项目目录文件夹

  • 紧接着咱们须要配置package.json文件,在里面添加咱们的脚本命令。打开咱们的package.json文件,在里面添加bin字段:
{
    "bin": {
        "vta": "bin/init.js"
    },
}
复制代码

这就是咱们定义了vta这个能够在终端运行的脚本命令,即运行vta这个命令的时候,程序会去运行咱们配置的bin/init.js这个脚本文件。其实,根据npm的机制,当install一个包的时候,会自动去查询其定义的bin命令,并把他添加到node_modules/.bin文件中去,做为shell的命令能够去执行。所以当你的包安装到局部的项目中,那么其bin中的命令就是局部可运行的,安装到全局中则变成了全局能够运行的命令。node

说明一下,并非必定要是js文件,其实在linux系统中一切皆文件,是没有后缀名的规定的,至少为了让“人”好识别而已。linux

重点强调一下:init.js文件的第一行,必定是第一行,咱们添加了#!/usr/bin/env node代码,是指定了咱们脚本的运行环境,和自定在咱们运行vta命令的时候添加了node命令做为前缀,即实际运行的是node vtaios

  • 接下来,为了方便咱们测试,咱们须要将这个包发不到本地的全局环境。咱们能够经过以下命令:
# 终端运行命令(需在当前项目根目录下)
npm link
复制代码

注意,npm link是当咱们当前包link到本地的全局中,就比如如咱们安装依赖时使用了-g参数把一些包装到了全局环境同样,是用来方便咱们本地开发时测试的,他可让咱们开发的时候自动热更新。若是不清楚npm link的小伙伴,能够去npm官网查查npm link的用法再继续往下学习。git

可是,我想说的时候,不少小伙伴在这块可能会踩坑:github

  • 首先,最好把你的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菊花图

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是一款可让咱们的控制台打印出各类颜色/背景的内容的工具,由此咱们能够鲜明的区分各类提示内容,以下图(就问你骚不骚???):

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);
      }
    })
  }
复制代码

具体讲解:

  1. 咱们定义了这个方法,返回的是一个Promise对象。
  2. 咱们判断用户在输入vta create my-app的时候有没有在后面加-f的参数,若是添加了参数则是告诉咱们忽略检查直接日后走,就是默认覆盖的操做。经过调用fs.removeSync(target);方法进行移除须要覆盖的文件;
  3. 不然的话,咱们则须要进行文件夹检查的实现逻辑了。经过await fs.pathExistsSync(target)逻辑进行判断当前文件夹名称是否已经存在,若是不存在则resolve告诉程序执行文件夹检查成功以后的程序。
  4. 若是同名的则给用户提示,让用户选择操做。下面将讲解如何在命令行进行交互。

❤️命令行交互

说到命令行交互,就要提到一个比较程序的库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);
}
复制代码
  1. 若是用户选择了覆盖,咱们就移除文件夹而后reolve
  2. 若是用户选择了建立新的文件夹,那么咱们就再给出一个用于输入的终端,让用户输入新的文件夹名称。在用户输入完成后,咱们把target的目标地址更新掉。
  3. 若是用户选择退出,咱们则调用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,把业务通用的场景解决方案抽离处理,提高本身的开发效率吧!最后,我是大家的老朋友愣锤,欢迎👏👏点赞👍👍收藏💗💗哦~~~

点赞👍、收藏👋、分享防走丢哦!!!须要的时候能够拿出来对着开发~~~

❤️❤️❤️更新留白

此处将留做后续更多和脚手架开发相关的优秀库的展现地址,后续会继续更新~~~

👍👍👍做者其余文章推荐

相关文章
相关标签/搜索