用zx封装个人提效cli


theme: smartblue

一、Situation(情景)

在前端的日常开发中,我们需要执行一些重复性操作和快捷命令,例如:

  1. 实现在本地启动项目后,监听到配置文件(如.env)的变化,自动重新启动项目
  2. 快速切换公司/个人2个层面的git提交信息(如:企业邮箱<-->个人邮箱、企业中的昵称<-->个人的昵称等)
  3. 快速生成.gitignore文件;
  4. 快速生成符合个人习惯的.vscode配置;
  5. 在控制台输出当前项目的贡献者名单;
  6. 等等。

面对这些任务,我们:

  1. 可能会采用CV大法;
  2. 可能会寻找一些开源辅助工具;
  3. 可能自行开发一些定制性的程序/脚本;
  4. 等等。

这些行为可能是一次性的,使用的模板/配置文件/自行开发的脚本可能也是散落在电脑的各个角落。

那么,有没有一种方式既能收藏好的模板/配置文件/命令片段,也能灵活高效的使用它们,还能持续完善并高效的迁移呢?

或许,开发一个私人定制的脚手架工具是一种不错的方式!


问:为什么说开发一个私人定制的脚手架工具是一种不错的方式?

答:因为可以在全局命令行中作为命令使用。一个脚手架工具就是一个项目,我们可以在这个项目中存放各种文件,不限于收藏的模板文件、命令片段等,并将其配置为一条命令以便于随时灵活的使用。我们可以使用git进行版本控制,不停迭代。我们可以将项目放在github上(当然出于隐私保护,你可以将仓库设为Private),以便于在任何其他电脑上使用。


问:出于隐私保护的目的,并不准备发包到npm上,那么要如何作为命令使用该工具呢?

答:做好相应配置,然后在脚手架项目根目录下执行npm link即可。


问:如何实现收藏功能?

答:其实就是将模板/配置文件分好类,然后放在项目的src/templates目录下。以模板的形式或者你认为的比较方便使用的方式。


二、Task(任务)

本文主要分享、记录如何使用脚手架模式,打造私人定制的提效命令行界面(CLI)。

主要内容包含:

  1. 搭建基础环境(这里node >= 16.0.0),安装核心依赖库(zxnodemoninquirer),其他工具函数都是zx自带;
  2. 记录zx的基本使用;
  3. 针对上述1~5这几项使用场景,提供具体的解决方案并将其配置为命令。

请注意,本文中介绍的代码和场景基于个人使用经验,可能不适用于所有人。读者应根据各自实际情况进行参考。

三、Action(行动)

首先搭建一下环境,按如下步骤操作即可:

注:为了不增加额外的心智负担,暂时不使用ESLint、typescript等,怎么简单怎么来!

# 1. 创建1个文件夹,这里为tools
mkdir tools

2. 创建bin/index.mjs文件

mkdir tools/bin
echo “#!/usr/bin/env node\n\nconsole.log(‘hello zx’)” > tools/bin/index.mjs

3. 切换到tools目录下,然后执行npm init -y

cd tools
npm init -y

4. git初始化,添加.gitignore

git init
echo “node_modules” > .gitignore

5. 安装2个核心库(nodemon、zx)

npm i -D nodemon zx

6. 到此环境准备就绪,执行一下npm link。

npm link

7. 测试命令, 应该可以看到输出:hello zx

tools

下面针对每种场景,分别介绍:

3.1 实现"在本地启动项目后,监听到配置文件(如.env)的变化,自动重新启动项目"

这个场景来自于本人实际经历:需要经常切换dev、qa环境进行开发、测试。

因为项目是通过本地.env文件来配置不同环境的代理,但当前项目的配置并不支持监听到.env变化就自动重启。所以每次修改.env后,需要手动停掉项目,然后再重新启动,比较繁琐。故而只能临时想出如下法子简化一下操作

关键词:nodemon

使用方式示例:tools -w .env

import nodemon from "nodemon";
import { getPackageManager } from "../utils/index.mjs";

/**

  • 执行文件/目录的监听,当其变化时重新启动项目:

  • 使用方式:tools -w .env

  • @param {object} argv - {@link API Reference | google/zx argv}

  • @param {true|string|string} argv.w
    */
    const execWatch = (argv) => {
    const { w } = argv;
    if (w) {
    const watchFiles = { boolean: [“.env”], string: [w], object: w }[typeof w];

    // 类似于执行:nodemon -w .env -x ‘npm start’
    nodemon(-w ${watchFiles.join(" -w ")} -x '${getPackageManager()} start');
    }
    };

export default execWatch;

3.2 快速切换"git提交信息"

这个场景主要用于区分个人环境与公司环境:因为在公司可能需要git提交真实姓名和企业邮箱,但个人项目可能只提交昵称和个人邮箱。

关键词:git、email、name

使用方式示例:tools --git -ptools --git -c

/**
 * 查看或切换个人/公司git信息
 *
 * 使用方式 - 1:tools --git -p  切换为个人
 *
 * 使用方式 - 2:tools --git -c  切换为公司
 * @param {object} argv - {@link https://google.github.io/zx/api#argv argv}
 * @param {true} argv.git
 * @param {true|undefined} argv.p
 * @param {true|undefined} argv.c
 */
const execToggleGitInfo = async (argv) => {
  const { git, p, c } = argv;
  if (git) {
    let { stdout: name } = await $`git config --global user.name`.quiet();
    let { stdout: email } = await $`git config --global user.email`.quiet();
name = name.replace(/\s/, "");
email = email.replace(/\s/, "");

// 下面p_name、p_email、c_name、c_email是在入口处通过配置文件注入
if (p &amp;&amp; $.p_name &amp;&amp; $.p_name !== name) {
  name = $.p_name;
  // 设置个人昵称
  await $`git config --global user.name ${$.p_name}`;
}
if (p &amp;&amp; $.p_email &amp;&amp; $.p_email !== email) {
  email = $.p_email;
  // 设置个人邮箱
  await $`git config --global user.email ${$.p_email}`;
}
// 个人与公司不能同时设置
if (!p &amp;&amp; c &amp;&amp; $.c_name &amp;&amp; $.c_name !== name) {
  name = $.c_name;
  // 设置企业中的昵称
  await $`git config --global user.name ${$.c_name}`;
}
if (!p &amp;&amp; c &amp;&amp; $.c_email &amp;&amp; $.c_email !== email) {
  email = $.c_email;
  // 设置企业中的邮箱
  await $`git config --global user.email ${$.c_email}`;
}

// 回显昵称/邮箱
console.log(chalk.green(`当前git信息:\n${name}\n${email}`));

}
};

export default execToggleGitInfo;

3.3 快速生成.gitignore文件;

这个场景主要用于在新项目中创建.gitignore

关键词:写文件、fetch、inquirer

使用方式示例:tools -g [template_name]

import { existsSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import inquirer from "inquirer";

const rootPath = process.cwd();
const api = “https://api.github.com/gitignore/templates”;
const fileName = “.gitignore”;

/**

  • 获取.gitignore可选模板类型
    */
    const getList = async () => {
    const response = await fetch(api);
    const data = await response.json();
    return data;
    };

/**

  • 获取模板文件并生成.gitignore
    */
    const getTemplate = async (name) => {
    let msg = “”;
    await spinner(${name}${fileName} 文件拉取中..., async () => {
    const response = await fetch(${api}/${name});
    const data = await response.json();
    if (data && data.source) {
    await writeFile(resolve(rootPath, fileName), data.source);
    } else {
    msg = data?.message || “not found”;
    }
    });
    if (msg) {
    console.log(chalk.red(msg));
    } else {
    console.log(chalk.green(${fileName} 文件创建成功!));
    }
    };

/** 执行交互式创建.gitignore */
const createGitignore = async (name) => {
// 文件存在,则直接退出
if (existsSync(resolve(rootPath, fileName))) {
console.log(chalk.red(${fileName}文件已存在!));
return;
}
if (name) {
return getTemplate(${name[0].toUpperCase()}${name.slice(1)});
}
const choices = await getList();
const answers = await inquirer.prompt([
{
type: “list”,
name: “name”,
message: "Select template name: ",
choices,
},
]);
getTemplate(answers.name);
};

/**

  • 交互式创建.gitignore文件
  • 使用方式:tools -g [template_name]
  • @param {object} argv - {@link API Reference | google/zx argv}
  • @param {true} argv.g
  • @param {string|undefined} argv.name - 模板名称
    */
    const execGitignore = (argv) => {
    const { g, name } = argv;
    if (g) {
    createGitignore(name);
    }
    };

export default execGitignore;

3.4 快速生成符合个人习惯的.vscode配置;

这个场景主要用于在新项目中copy个人常用的.vscode配置。

关键词:文件拷贝

使用方式示例:tools --vscode

import { existsSync } from "node:fs";
import { resolve } from "node:path";
import { copyTemplate } from "../utils/index.mjs";

/**

  • 复制.vscode配置
  • 使用方式:tools --vscode
  • @param {object} argv - {@link API Reference | google/zx argv}
  • @param {true} argv.vscode
    */
    const execVsCode = async (argv) => {
    const { vscode } = argv;
    if (vscode) {
    const extPath = resolve(process.cwd(), “.vscode/extensions.json”);
    const setPath = resolve(process.cwd(), “.vscode/settings.json”);
    if (!existsSync(extPath)) {
    // 复制模板中的.vscode:详见utils/
    copyTemplate(“.vscode/extensions.json”, extPath);
    } else {
    console.log(chalk.red(“.vscode/extensions.json已存在”));
    }
    if (!existsSync(setPath)) {
    copyTemplate(“.vscode/settings.json”, setPath);
    } else {
    console.log(chalk.red(“.vscode/settings.json已存在”));
    }
    }
    };

export default execVsCode;

3.5 在控制台输出当前项目的贡献者名单

这个场景主要用于查看项目的所有提交人。

使用方式示例:tools -a

/**
 * 在控制台输出当前项目的贡献者名单
 *
 * 使用方式:tools -a
 * @param {object} argv - {@link https://google.github.io/zx/api#argv argv}
 * @param {true} argv.a
 */
const execContributors = async (argv) => {
  const { a } = argv;
  if (a) {
    const authors = new Set();
    // git获取提交记录中author
    const { stdout: logs } = await $`git log --pretty=format:"%an"`.quiet();
    // author去重
    (logs || "")
      .split("\n")
      .forEach((item) => item && authors.add(item.trim()));
console.log([...authors]);

}
};

export default execContributors;

3.6 其他文件

  1. bin/index.mjs 入口文件
#!/usr/bin/env node

// bin/index.mjs 入口文件

import “zx/globals”;
import { fileURLToPath } from “node:url”;
import { dirname, resolve } from “node:path”;
import init from “…/src/index.mjs”;
import { getConfigYaml } from “…/src/utils/index.mjs”;

// 获取tools项目根目录
.toolsRootPath = resolve(dirname(fileURLToPath(import.meta.url)), ".."); // 获取tools项目模板文件地址 .toolsTemplatePath = resolve($.toolsRootPath, “src”, “templates”);

// 注入全局配置文件
await getConfigYaml();

init(argv);

  1. src/utils/index.mjs
// src/utils/index.mjs

import { resolve } from “node:path”;
import { existsSync, readFileSync } from “node:fs”;

/** 获取当前环境包管理工具 */
export const getPackageManager = () => {
const cwd = process.cwd();
if (existsSync(resolve(cwd, “pnpm-lock.yaml”))) {
return “pnpm”;
}
if (existsSync(resolve(cwd, “yarn.lock”))) {
return “yarn”;
}

return “npm”;
};

/** 复制模板文件 */
export const copyTemplate = async (name, targetPath) => {
try {
await fs.copy(resolve($.toolsTemplatePath, name), targetPath);
console.log(chalk.green(name + " success!"));
} catch (err) {
console.error(err);
}
};

/** 注入yaml配置文件 */
export const getConfigYaml = () => {
return new Promise((res) => {
try {
const configPath = resolve(.toolsRootPath, "config.yaml"); if (existsSync(configPath)) { const file = readFileSync(configPath, "utf8"); const info = YAML.parse(file); Object.entries(info).forEach(([key, value]) =&gt; { [key] = value;
});
}
res(true);
} catch (error) {
rej(error);
}
});
};

四、Result(结果)

第1~5种场景均提供了个人的实现方案、较详细的备注、命令行的使用方式说明。

代码可能并不优雅,这里主要是想分享这样一种思路!

本文demo的github地址


这是一个从 https://juejin.cn/post/7369097402500661288 下的原始话题分离的讨论话题