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

11,723 阅读19分钟

我们业务中可以通过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作为我们真正实现脚本命令逻辑的地方:

vta-cli项目目录文件夹
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中的命令就是局部可运行的,安装到全局中则变成了全局可以运行的命令。

说明一下,并不是一定要是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对象展示
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菊花图

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效果图
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,把业务通用的场景解决方案抽离处理,提升自己的开发效率吧!最后,我是你们的老朋友愣锤,欢迎👏👏点赞👍👍收藏💗💗哦~~~

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

❤️❤️❤️更新留白

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

👍👍👍作者其他文章推荐

本文使用 mdnice 排版