1.0.0 • Published 7 months ago

kz-staging-cli v1.0.0

Weekly downloads
-
License
ISC
Repository
-
Last release
7 months ago

从零搭建一个前端脚手架命令行工具

一)初始化项目

在对应的目录下创建项目名称 kz-staging-cli. 执行命令如下:

mkdir kz-staging-cli && cd kz-staging-cli

然后生成 package.json 文件,执行命令如下:

npm init -y

二)创建入口

在项目的根目录文件夹下,建一个 bin 目录,然后再新建一个 main 文件(注意:是纯 main,没有.js 后缀)。main 代码如下:

#! /usr/bin/env node

console.log('我是入口文件...');

然后,我们在命令行运行 node ./bin/main 后就会执行这个 main 文件,并且打印出来了结果,如下:

tugenhua@192 kz-staging-cli % node ./bin/main
我是入口文件...

#! /usr/bin/env node 是 base 脚本需要在第一行执行脚本的解释语言。我们使用的语言是 node。

如果我们想执行 kz-staging-cli 命令就能执行代码的话,我们只需要在 package.json 中增加如下代码:

{
  "name": "kz-staging-cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {},
  "bin": {
    "kz-staging-cli": "bin/main"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

为了暂时测试下,因此我们执行 npm link 链接到全局命令。现在这个时候 我们就可以在命令中运行 kz-staging-cli 也一样可以运行代码了,如下所示:

tugenhua@192 kz-staging-cli % kz-staging-cli
我是入口文件...

三)配置脚手架的选项(options)

3.1)增加版本

我们需要用到一个插件 commander, commander 是用来实现脚手架命令配置的插件。更多的使用方式 我们自己可以看下 commander 中文文档

先安装下该插件:

npm install commander@9

因此我们为 main 中增加如下代码:

#! /usr/bin/env node

const program = require("commander");

// 获取当前的版本号
const version = require('../package.json').version;

program
  // 配置脚手架名称
  .name('kz-staging-cli')
  // 配置命令格式
  .usage(`<command> [option]`)
  // 配置版本号
  .version(version);

program.parse(process.argv);

然后我们进行运行 kz-staging-cli --help 看一下

可以看到已经有 --version 的提示了。我们执行一下命令: kz-staging-cli --version 就会发现可以输出版本号了。

3.2)增加提示

增加提示其实就是为了美化效果,我们需要引入插件 chalk. chalk 是用来美化字体的插件,也就是改变字体,背景颜色等等。想了解更多,我们可以去 chalk 地址 看看.

安装命令:

npm install chalk@4

现在,我们继续为 main 增加如下代码, 所有的代码变成如下:

#! /usr/bin/env node

const program = require("commander");
const chalk = require("chalk");

// 获取当前的版本号
const version = require('../package.json').version;

program
  // 配置脚手架名称
  .name('kz-staging-cli')
  // 配置命令格式
  .usage(`<command> [option]`)
  // 配置版本号
  .version(version);

// 给提示增加
program.on('--help', () => {
  console.log();
  console.log(`
    Run ${chalk.cyan('kz-staging-cli <command> --help')} fro detailed usage of giver command.
  `)
});

program.parse(process.argv);

继续运行 kz-staging-cli --help 看一下效果如下:

如上,我们就配置了脚手架的选项(options)了。

3.3)配置脚手架命令(command)

脚手架的核心是命令,比如 vue create yyy. 因此,我们也需要实现自己的脚手架命令。

我们的目标是使用这个脚手架,可以拉取 vue 或 react 的模版代码。

添加命令模块

现在,我们需要有一个 create 模块来完成创建指令。因此我们新建一个文件夹 lib,并增加一个文件 create.js, 代码如下:

module.exports = function (projectName, options) {
  console.log('---projectName---', projectName);
  console.log('---options------', options);
}

如上 lib/create.js 代码,导出一个函数,该函数接收两个参数,然后将接收到的参数打印出来。

现在我们继续改造 main 文件,在 main 文件增加以下代码, 所有代码如下:

#! /usr/bin/env node

const program = require("commander");
const chalk = require("chalk");
// 获取create模块
const createModel = require('../lib/create');

// 获取当前的版本号
const version = require('../package.json').version;

program
  // 配置脚手架名称
  .name('kz-staging-cli')
  // 配置命令格式
  .usage(`<command> [option]`)
  // 配置版本号
  .version(version);

// 给提示增加
program.on('--help', () => {
  console.log();
  console.log(`
    Run ${chalk.cyan('kz-staging-cli <command> --help')} fro detailed usage of giver command.
  `)
});

program
  .command('create <project-name>')
  .description('create a new project')
  .option('-f, --force', 'overwrite target directory if it exists')
  .action((projectName, options) => {
    // 引入create模块, 并传入参数
    createModel(projectName, options);
  });

program.parse(process.argv);

如上命令解释:

program.command 是定义一个命令,命令的格式是 kz-staging-cli create yyy
description 是这个命令的描述。
option 是命令后面可以带的参数以及参数的相关描述。
action 后面是一个回调函数,回调函数的第一个参数是上面的 yyy(项目名称),第二个参数就是 -- 后面的

现在我们运行命令: kz-staging-cli create test --force, 打印如下:

四)编写 create 模块

创建 Create 类

create 模块将来可能会包含很多功能,比如校验目录是否存在,或 拉取远程代码模块等功能。因此我们需要创建一个类。代码如下:

class Create {
  constructor(projectName, options) {
    this.projectName = projectName;
    this.options = options;
  }
  // 创建
  async create() {
    //....
  }
}

module.exports = function (projectName, options) {
  const creator = new Create(projectName, options);
  await creator.create();
}

如上就是我们的 create 类的模版代码。现在我们需要 校验目录是否存在,因此我们需要在 上面的 async create 函数添加代码逻辑了。 我们的目标是想通过 kz-staging-cli create yyy 来创建一个 yyy 项目名,但是我们所在的目录本身可能已经存在 yyy 目录。因此我们需要做一个目录是否存在的校验。校验规则如下:

1)如果使用了 --force 参数,那么直接删除原先的项目名,然后直接创建新的项目名。
2)如果没有使用 --force 参数,那么询问用户,是否覆盖,选择覆盖则执行1的逻辑,不覆盖则终止创建。

在第二步的时候,询问用户需要和用户进行交互,因此我们需要使用 inquirer 插件,我们还需要使用 fs-extra 模块来判断目录是否存在。

安装 inquirer 插件 命令如下:

npm install inquirer@8 fs-extra

现在我们可以在 create.js 添加逻辑代码如下:

const path = require('path');
const fs = require('fs-extra');
const chalk = require('chalk');
const Inquirer = require('inquirer');
const cwd = process.cwd();

class Create {
  constructor(projectName, options) {
    this.projectName = projectName;
    this.options = options;
  }
  // 创建
  async create() {
    const isOverwrite = await this.handleDirectory();
    console.log('---isOverwrite---', isOverwrite);
    if (!isOverwrite) return;
  }
  // 是否有相同的项目名
  async handleDirectory() {
    // 当前目标路径
    const targetDirectory = path.join(cwd, this.projectName);
    console.log('---targetDirectory---', targetDirectory);
    // 如果目录中存在需要创建的目录,如果有参数 force 的话,直接删除当前的目录,然后再创建
    if (fs.existsSync(targetDirectory)) {
      console.log('---this.options---', this);
      if (this.options.force) {
        await fs.remove(targetDirectory);
      } else {
        let { isOverwrite } = await new Inquirer.prompt([
          {
            name: 'isOverwrite',
            type: 'list',
            message: '是否强制覆盖已存在的相同目录?',
            choices: [
              {
                name: '覆盖',
                value: true
              },
              {
                name: '不覆盖',
                value: false
              }
            ]
          }
        ]);
        if (isOverwrite) {
          await fs.remove(targetDirectory);
        } else {
          console.log(chalk.red.bold('不能覆盖文件夹,创建终止'));
          return false;
        }
      }
    }
    return true;
  }
}

module.exports = function (projectName, options) {
  const creator = new Create(projectName, options);
  creator.create();
}

上面代码编写完成后,我们在项目目录外面新建一个空文件夹 test,然后我们再运行 kz-staging-cli create test 后,执行结果如下:

可以看到命令行进行交互了。

五)增加调取模版 API

接下来,我们需要从远程去获取需要拉取的模版列表,然后选择一个需要拉取的模版,拉取到本地。 需要拉取的模版列表的 API:

里面的 topics 包含了 template 就是可以拉取的模版。

因此我们需要 axios。下载 axios 命令如下:

npm install axios

在项目的根目录下新建 api 文件夹,然后在该文件夹下 新建 request.js.

api/request.js 代码如下:

// api/request.js
const axios = require('axios');

class HttpRequest {
  constructor(baseUrl, options = {}) {
    this.baseUrl = baseUrl;
    this.commonOptions = options;
  }
  getInsideConfig() {
    const configs = {
      baseUrl: this.baseUrl,
      ...this.commonOptions
    };
    return configs;
  }
  request(options) {
    const instance = axios.create({});
    options = Object.assign(this.getInsideConfig(), options);
    return instance(options);
  }
}

module.exports = HttpRequest;

api/index.js 代码如下:

const HttpRequest = require('./request');
module.exports = new HttpRequest('');

api/interface/index.js 代码如下:

const axios = require('../api/index');

const getRepoList = params => {
  return axios.request({
    url: 'https://api.github.com/users/kongzhi0707/repos',
    params,
    method: 'get'
  })
}

module.exports = {
  getRepoList,
}

六)获取模版列表

上面我们只是封装了 api,现在我们需要调用上面的 api 的 getRepoList 获取模版列表,让用户选择,需要获取那个模版。由于我们拉取远程数据需要时间。 因此,为了优化体验感,我们需要增加一个 loading 的效果,因此需要用到 ora 库。

ora 是命令行 loading 效果的库,了解更多 请查看 ora 文档查看.

安装命令如下:

npm install ora@5

下面就是我们在 lib/create.js 增加的逻辑代码如下:

const path = require('path');
const fs = require('fs-extra');
const chalk = require('chalk');
const Inquirer = require('inquirer');
const cwd = process.cwd();

const ora = require('ora');
const api = require('../api/interface/index');

class Create {
  constructor(projectName, options) {
    this.projectName = projectName;
    this.options = options;
    this.getCollectRepo();
  }
  // 创建
  async create() {
    const isOverwrite = await this.handleDirectory();
    console.log('---isOverwrite---', isOverwrite);
    if (!isOverwrite) return;
  }
  // 是否有相同的项目名
  async handleDirectory() {
    // 当前目标路径
    const targetDirectory = path.join(cwd, this.projectName);
    console.log('---targetDirectory---', targetDirectory);
    // 如果目录中存在需要创建的目录,如果有参数 force 的话,直接删除当前的目录,然后再创建
    if (fs.existsSync(targetDirectory)) {
      console.log('---this.options---', this);
      if (this.options.force) {
        await fs.remove(targetDirectory);
      } else {
        let { isOverwrite } = await new Inquirer.prompt([
          {
            name: 'isOverwrite',
            type: 'list',
            message: '是否强制覆盖已存在的相同目录?',
            choices: [
              {
                name: '覆盖',
                value: true
              },
              {
                name: '不覆盖',
                value: false
              }
            ]
          }
        ]);
        if (isOverwrite) {
          await fs.remove(targetDirectory);
        } else {
          console.log(chalk.red.bold('不能覆盖文件夹,创建终止'));
          return false;
        }
      }
    }
    return true;
  }
  // 获取可拉取的仓库列表
  async getCollectRepo() {
    const loading = ora('正在获取模版信息...');
    loading.start();
    const { data: list } = await api.getRepoList({ per_page: 100 });
    loading.succeed();
    const templateNameList = list.filter(item => item.topics.includes('template')).map(item => item.name);
    let { choiceTemplateName } = await new Inquirer.prompt([
      {
        name: 'choiceTemplateName',
        type: 'list',
        message: '请选择模版',
        choices: templateNameList
      }
    ]);
    console.log('选择了模版: ' + choiceTemplateName);
  }
}

module.exports = function (projectName, options) {
  const creator = new Create(projectName, options);
  creator.create();
}

现在,我们运行命令: kz-staging-cli create test

七)下载对应模版

接下来,我们就需要根据用户的选择模版来定向拉取对应的模版到本地来了,因此我们需要使用到 download-git-repo 这个插件来把 git 上面的模版拉取到本地来。

安装命令如下:

npm install download-git-repo

在 lib/create.js 中的代码变成如下:

const path = require('path');
const fs = require('fs-extra');
const chalk = require('chalk');
const Inquirer = require('inquirer');
const cwd = process.cwd();

const ora = require('ora');
const api = require('../api/interface/index');

const util = require('util');
const downloadGitRepo = require('download-git-repo');

class Create {
  constructor(projectName, options) {
    this.projectName = projectName;
    this.options = options;
    this.getCollectRepo();
  }
  // 创建
  async create() {
    const isOverwrite = await this.handleDirectory();
    console.log('---isOverwrite---', isOverwrite);
    if (!isOverwrite) return;
  }
  // 是否有相同的项目名
  async handleDirectory() {
    // 当前目标路径
    const targetDirectory = path.join(cwd, this.projectName);
    console.log('---targetDirectory---', targetDirectory);
    // 如果目录中存在需要创建的目录,如果有参数 force 的话,直接删除当前的目录,然后再创建
    if (fs.existsSync(targetDirectory)) {
      console.log('---this.options---', this);
      if (this.options.force) {
        await fs.remove(targetDirectory);
      } else {
        let { isOverwrite } = await new Inquirer.prompt([
          {
            name: 'isOverwrite',
            type: 'list',
            message: '是否强制覆盖已存在的相同目录?',
            choices: [
              {
                name: '覆盖',
                value: true
              },
              {
                name: '不覆盖',
                value: false
              }
            ]
          }
        ]);
        if (isOverwrite) {
          await fs.remove(targetDirectory);
        } else {
          console.log(chalk.red.bold('不能覆盖文件夹,创建终止'));
          return false;
        }
      }
    }
    return true;
  }
  // 获取可拉取的仓库列表
  async getCollectRepo() {
    const loading = ora('正在获取模版信息...');
    loading.start();
    const { data: list } = await api.getRepoList({ per_page: 100 });
    loading.succeed();
    const templateNameList = list.filter(item => item.topics.includes('template')).map(item => item.name);
    let { choiceTemplateName } = await new Inquirer.prompt([
      {
        name: 'choiceTemplateName',
        type: 'list',
        message: '请选择模版',
        choices: templateNameList
      }
    ]);
    console.log('选择了模版: ' + choiceTemplateName);
    // 下载模版
    this.downloadTemplate(choiceTemplateName);
  }
  // 下载github仓库中的模版
  async downloadTemplate(choiceTemplateName) {
    this.downloadGitRepo = util.promisify(downloadGitRepo);
    const templateUrl = `kongzhi0707/${choiceTemplateName}`;
    const loading = ora('正在拉取模版...');
    loading.start();
    console.log('---templateUrl---', templateUrl);
    console.log('---projectName----',  path.join(cwd, this.projectName))
    await this.downloadGitRepo(templateUrl, path.join(cwd, this.projectName));
    loading.succeed();
  }
}

module.exports = function (projectName, options) {
  const creator = new Create(projectName, options);
  creator.create();
}

现在,我们运行命令: kz-staging-cli create test 就可以把我们的代码拉取下来了。

八)模版提示

我们还需要增加一个优化功能,在拉取成功后,需要告诉用户怎么操作,并且增加一些艺术字体效果。增加艺术字体使用 figlet 插件。查看 figlet 文档

安装命令如下:

npm install figlet

然后我们的 create.js 增加如下代码:

const path = require('path');
const fs = require('fs-extra');
const chalk = require('chalk');
const Inquirer = require('inquirer');
const cwd = process.cwd();

const ora = require('ora');
const api = require('../api/interface/index');

const util = require('util');
const downloadGitRepo = require('download-git-repo');

const figlet = require('figlet');

class Create {
  constructor(projectName, options) {
    this.projectName = projectName;
    this.options = options;
    this.getCollectRepo();
  }
  // 创建
  async create() {
    const isOverwrite = await this.handleDirectory();
    console.log('---isOverwrite---', isOverwrite);
    if (!isOverwrite) return;
  }
  // 是否有相同的项目名
  async handleDirectory() {
    // 当前目标路径
    const targetDirectory = path.join(cwd, this.projectName);
    console.log('---targetDirectory---', targetDirectory);
    // 如果目录中存在需要创建的目录,如果有参数 force 的话,直接删除当前的目录,然后再创建
    if (fs.existsSync(targetDirectory)) {
      console.log('---this.options---', this);
      if (this.options.force) {
        await fs.remove(targetDirectory);
      } else {
        let { isOverwrite } = await new Inquirer.prompt([
          {
            name: 'isOverwrite',
            type: 'list',
            message: '是否强制覆盖已存在的相同目录?',
            choices: [
              {
                name: '覆盖',
                value: true
              },
              {
                name: '不覆盖',
                value: false
              }
            ]
          }
        ]);
        if (isOverwrite) {
          await fs.remove(targetDirectory);
        } else {
          console.log(chalk.red.bold('不能覆盖文件夹,创建终止'));
          return false;
        }
      }
    }
    return true;
  }
  // 获取可拉取的仓库列表
  async getCollectRepo() {
    const loading = ora('正在获取模版信息...');
    loading.start();
    const { data: list } = await api.getRepoList({ per_page: 100 });
    loading.succeed();
    const templateNameList = list.filter(item => item.topics.includes('template')).map(item => item.name);
    let { choiceTemplateName } = await new Inquirer.prompt([
      {
        name: 'choiceTemplateName',
        type: 'list',
        message: '请选择模版',
        choices: templateNameList
      }
    ]);
    console.log('选择了模版: ' + choiceTemplateName);
    // 下载模版
    this.downloadTemplate(choiceTemplateName);
  }
  // 下载github仓库中的模版
  async downloadTemplate(choiceTemplateName) {
    this.downloadGitRepo = util.promisify(downloadGitRepo);
    const templateUrl = `kongzhi0707/${choiceTemplateName}`;
    const loading = ora('正在拉取模版...');
    loading.start();
    console.log('---templateUrl---', templateUrl);
    console.log('---projectName----',  path.join(cwd, this.projectName))
    await this.downloadGitRepo(templateUrl, path.join(cwd, this.projectName));
    loading.succeed();

    // 增加艺术字体
    this.showTemplateHelp();
  }
  showTemplateHelp() {
    console.log(`\r\nSuccessfully created project ${chalk.cyan(this.projectName)}`);
    console.log(`\r\n  cd ${chalk.cyan(this.projectName)}\r\n`);
    console.log("  npm install");
    console.log("  npm run dev\r\n");
    console.log(`
      \r\n
      ${chalk.green.bold(
        figlet.textSync("SUCCESS", {
          font: "isometric4",
          horizontalLayout: "default",
          verticalLayout: "default",
          width: 80,
          whitespaceBreak: true,
        })
      )}
   `)
  }
}

module.exports = function (projectName, options) {
  const creator = new Create(projectName, options);
  creator.create();
}

我们现在再执行命令: ts-staging-cli create test 效果如下:

九)发布脚手架

package.json 文件如下:

{
  "name": "kz-staging-cli",
  "version": "1.0.0",
  "description": "前端脚手架命令行工具",
  "bin": {
    "kz-staging-cli": "bin/main"
  },
  "keywords": ["前端脚手架命令行工具", "脚手架", "kz-staging-cli"],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^1.5.1",
    "chalk": "^4.1.2",
    "commander": "^9.5.0",
    "download-git-repo": "^3.0.2",
    "figlet": "^1.6.0",
    "fs-extra": "^11.1.1",
    "inquirer": "^8.2.6",
    "ora": "^5.4.1"
  }
}
1. 我们登录 npm , 执行命令 npm login.
2. 在本地增加一个.npmignore 文件,写上需要忽略的文件,比如.vscode 等.
3. 执行 npm publish 即可.