xinxinqian_cli

1.0.0 • Public • Published

搭建脚手架 - 一键生成项目模板

  • npm init -y初始化 package.json
  • 创建 bin 文件夹,以及 bin/cli.js 文件
  • 创建 README.md 以记录

项目目录

xinxinqian
|- bin
|- |- cli.js
|- package.json
|- README.md

配置 package.json 文件

{
  "bin": {
    "xin-cli": "./bin/cli.js" // 配置启动文件路径,xin-cli为别名,用于下面终端执行`xin-cli`
  }
}

编辑 cli.js 文件

  • #! /usr/bin/env node的理解: 参考
    • #!只是标识作用,表示的是该文件作为执行文件来运行。即可当做脚本来运行。
    • /usr/bin/env node: 表示用 node 来执行此文件。
      • node 怎么来呢?就去用户 usr 的安装根目录 bin 下的 env 环境去找。
      • 在 windows 上,就去安装 node 的 bin 目录去找 node 执行器。,一般都放在环境变量中,所以能正确找到 node 来执行。
#! /usr/bin/env node
console.log("~ working ~");

执行 npm link (后面不加参数)

  • 终端中输入npm link
  • 终端中输入xin-cli
  • 可以看到 终端中打印结果:~ working ~

创建脚手架启动命令

  • commander - 实现终端命令行的输出
    • 参照vue-cli中的命令有 create、config 等
  • 安装 commander 依赖包npm install commander --save
  • 安装完成后,编辑 cli.js 的内容, 创建create的命令:
#! /usr/bin/env node
const program = require("commander");
// 定义命令和参数:创建了 create 的命令,作用是创建一个新的项目目录
program
  .command("create [name]")
  .description("create a new project")
  // -f or --force 为强制创建,如果创建的目录存在,则直接覆盖
  .option("-f, --force", "overwrite target directory if it exits")
  .action((name, options) => {
    // 打印结果,输出用户手动输入的项目名字
    console.log("name", name);
  });
program
  // 配置版本号信息
  .version(`v${require("../package.json").version}`)
  .usage("<command> [option]");

// 解析用户执行命令传入参数
program.parse(process.argv);
  • 执行xin-cli create去验证,xin-cli create my-project,在终端看效果。
  • 创建文件夹 lib,该文件下的内容 - 主要逻辑实现。
  • 创建 lib/create.js 文件, 并编辑内容
module.exports = async function (name, options) {
  // 验证是否正确取到值
  console.log("create success", name);
};
  • 修改 bin/cli.js 文件内容:
.action((name, options) => {
    // 打印结果,输出用户手动输入的项目名字
    // console.log("name", name)
    require("../lib/create")(name, options);
})
  • 执行指令xin-cli create my-project,在终端看效果。

创建项目文件目录

  • 要考虑一个问题:目录是否已经存在,怎么处理已经存在的目录?
    • 如果不存在,则直接创建一个新的项目文件目录
    • 如果存在,是否要直接删除或者用一个新的项目文件目录替换掉
    • 可以给用户提供命令选择
  • 涉及 nodejs 对文件的处理,引入依赖包 fs-extra
  • 安装npm install fs-extra --save

继续创建命令,config 命令

// 配置config命令
program
  .command("config [value]")
  .description("inspect and modify the config")
  .option("-g --get <path>", "get value from option")
  .option("-s, --set <path><value>")
  .option("-d, --delete <path>", "delete option from config")
  .action((value, options) => {
    console.log("自定义config命令", value);
  });

可以使用依赖 chalk、figlet,做出一些好看的样式 - 自定义修改默认的 help 展示信息

  • chalk 文档地址
  • figlet - npm 文档
  • 安装依赖 chalk npm i chalk@4.1.2 --save
  • 安装依赖 figlet npm i figlet --save
    • const chalk = require("chalk"); // chalk 版本 4 以下才支持,版本 5 以上不支持 es Module,要使用 import chalk from 'chalk';
// 自定义help输出信息,如果加上program.helpInformation,那么之前默认输出的help信息就会别覆盖。
// program.helpInformation = function () {
//     return '';
// };
// 可以使用chalk、figlet做出一些好看的样式
program.on("--help", () => {
  // 使用figlet 绘制logo
  console.log(
    "\r\n" +
      figlet.textSync("xinxinqian", {
        font: "Ghost",
        horizontalLayout: "default",
        verticalLayout: "default",
        width: 80,
        whitespaceBreak: true,
      })
  );
});
// 说明信息
console.log(`\r\nRun ${chalk.cyan("roc <command> --help")} show details\r\n`);

实现询问哟用户的操作

  • inquirer - npm 文档
  • 安装 inquirer 依赖 npm i inquirer@8.2.4 --save
    • inquirer 版本 8 以下才支持 require 引入,版本 9 以上不支持 es Module,要使用 import inquirer from 'inquirer';
  • 修改 lib/create.js 文件内容
const path = require("path");
const fs = require("fs-extra");

// 引入inquirer - 用于控制台询问,交互式命令行
const inquirer = require("inquirer");

module.exports = async function (name, options) {
  // 选择目录
  const cwd = process.cwd();
  // 需要创建的目录地址
  const targetAir = path.join(cwd, name);
  // 判断目录是否已经存在
  if (fs.existsSync(targetAir)) {
    // 是否为强制创建?
    if (options.force) {
      await fs.remove(targetAir);
    } else {
      // TODO: 询问用户是否确定要覆盖
      // 在终端输出询问用户是否覆盖:
      const inquirerParams = [
        {
          name: "operation",
          type: "list",
          message: "目标文件目录已存在,请选择如下操作:",
          choices: [
            {
              name: "替换当前目录",
              value: "replace",
            },
            {
              name: "移除已有目录",
              value: "remove",
            },
            {
              name: "取消当前操作",
              value: "cancel",
            },
          ],
        },
      ];
      let { operation } = await inquirer.prompt(inquirerParams);
      if (!operation || operation === "cancel") {
        return;
      } else {
        // 移除已存在的目录
        console.log("\r\nRemoving...");
        await fs.remove(targetAir);
      }
    }
  }
  // 验证是否正确取到值
  fs.mkdir(`./${name}`, async (error) => {
    if (error) {
      console.log("目录不存在");
      console.error("create fail", error);
      return;
    }
    console.log("create success");
  });
};

2023.6.19重新学习 参考网址

inquirer 字段描述

  • type:问题类型,可以是input(输入框)、list(列表选择框)、confirm(二选一选择框)等。
  • name:问题名称,用于标识答案对象中对应的属性名
  • message:问题描述,将会作为问题提示信息展示给用户
  • choices:选项列表,只有当问题类型为 list 时才需要提供

项目模板创建

  • bin文件夹下创建templates.js, 内容如下:
/** 暴露模版代码 */
module.exports = [
  {
    name: "xinxin-plus",
    value: "https://github.com:xinxin2qian/xinxin-plus",
  },
  {
    name: "jslib-xinxin",
    value: "https://github.com:xinxin2qian/jslib-xinxin",
  },
  {
    name: "test-demo",
    value: "https://github.com:xinxin2qian/test-demo",
  },
];

注意模版地址部分,域名 github.com 和模版地址之间是用冒号:隔开的,不是斜杠/,这个是下一节下载 git 仓库代码模版所用到的库 download-git-repo 的规则。 实际项目中要根据自己的需求配置不同的模版,比如 gitlab,gitee 等,文章后面也会换成接口动态请求。

  • 在 lib/create.js,加入代码,用于加载 templates 模板列表
const { template } = await inquirer.prompt({
  type: "list",
  name: "template",
  message: "请选择模版:",
  choices: templates, // 模版列表
});
console.log("模版地址:", template);

实现下载模板

  • 拿到用户选择的模板地址后,就要根据用户输入的项目名称,把指定的项目模板下载到对应项目文件夹中,实现下载 git 项目模板的功能需要下载download-git-repo依赖,通过指令npm i download-git-repo -S进行安装。
  • download-git-repo的语法
const downloadGitRepo = require("download-git-repo");

downloadGitRepo("项目git地址", "目标文件夹", function (err) {
  if (err) {
    console.log("下载失败", err);
  } else {
    console.log("下载成功");
  }
});
// 项目git地址:在选择完模板时可以获取到。
// 目标文件夹:应该是用户执行命令行所在位置下的项目名称文件夹。
const path = require("path");
// 目标文件夹 = 用户命令行所在目录 + 项目名称
const dest = path.join(process.cwd(), name);

默认会拉取 master 分支的代码,如果想从其他分支拉取代码,可以在 git 地址后面添加#branch 选择分支。如,指定 feature 分支:https://github.com:xinxin2qian/xinxin-plus#feature

优化 cli 脚手架

  • 由于从github下载的模板,有时候网络不好,下载时间会久一些,使用loading动画来提升用户体验,使用ora来实现,这是一个命令行的loading动画库
  • 使用指令npm i ora@5.4.1 -S进行安装,因为安装新的版本需要用 import 引入,由于 node 环境,也没有引入 babel 这些,就直接使用 5.x 的版本,可以直接 require 引入。
// 引入ora,loading动画库
const ora = require("ora");
// 定义loading
const loading = ora("正在下载模板...");
// ...
// 开始loading
loading.start();
// 开始下载模板
downloadGitRepo(templateUrl, targetAir, (error) => {
  if (error) {
    loading.fail("create template error" + error.message);
  } else {
    loading.succeed("create template success");
  }
});
// ...

支持从命令行传参数

  • 可以通过命令行参数形式直接传入项目名称和模板名称
// ...
// 如果通过命令行传入模板名称
let templateItem = templates.find(
  (template) => template.name === options.template
);
let templateUrl = templateItem ? templateItem.value : "";
if (!templateUrl) {
  const { template } = await inquirer.prompt({
    type: "list",
    name: "template",
    message: "请选择模版:",
    choices: templates, // 模版列表
  });
  console.log("模版地址:", template);
  templateUrl = template;
}
// ...

覆盖文件夹操作(补充)

  • 当前用户输入的目录所在位置已经有同名的文件夹时,应该提示用户是否覆盖,如果选了覆盖,则把原文件删除,如果选择不覆盖,就停止所有操作,退出命令行。
  • 通过existsSync方法判断目标文件夹是否存在,需要下载fs-extra依赖包
  • 如果存在,使用inquirer模块的confirm类型交互来让用户选择是否覆盖。

添加模板后,引导操作

  • 类似vitevue-cli等脚手架在从创建完项目后,都会有一个引导提示,比如,cd xxx进入文件夹,npm i安装依赖包等等

用接口获取动态模板

  • 如果新增或者删除模板,就需要修改cli脚手架代码,那么项目中的templates.js文件也要跟着修改,所以,换成接口请求的方式,增加灵活度,像githubgitlab等代码仓库网站,都有提供获取仓库信息等api,比如,githubapi.github.com返回很多接口。 => 倒数第二个是一个获取用户仓库列表信息的接口,其中{user}是用户名称参数,可以通过这个接口查询到对应github用户下所有公开的git仓库信息。如:自己的仓库信息
  • 在文件夹lib下,新建api.js文件,并在终端通过指令npm i axios -S安装依赖
  • api.js写入内容:
const axios = require("axios");
const getGitReposList = (username) => {
  return new Promise((resolve, reject) => {
    axios
      .get(`https://api.github.com/users/${username}/repos`)
      .then(function (response) {
        if (response.status === 200) {
          const list = response.data.map((item) => ({
            name: item.name,
            value: `https://github.com:${username}/${item.name}`,
          }));
          resolve(list);
        } else {
          reject(response.status);
        }
      })
      .catch(function (error) {
        reject(error);
      });
  });
};
module.exports = {
  getGitReposList,
};
  • 修改create.js文件的代码:
// ...
console.log("create success");
const getRepoLoading = ora("get templates...");
getRepoLoading.start();
const templates = await getGitReposList("xinxin2qian");
getRepoLoading.succeed("get templates success!");
// 如果通过命令行传入模板名称
let templateItem = templates.find(
  (template) => template.name === options.template
);
// ...

cli脚手架使用

安装

$ npm install -g xin-cli
# or yarn
$ yarn global add xin-cli

使用

# 创建模版
$ xin-cli create <name> [-t|--template]
# 示例
$ xin-cli create my-test -template xinxin-plus

Readme

Keywords

Package Sidebar

Install

npm i xinxinqian_cli

Weekly Downloads

0

Version

1.0.0

License

ISC

Unpacked Size

21.8 kB

Total Files

6

Last publish

Collaborators

  • xinxinqian