用zx封装个人提效cli

一、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等,怎么简单怎么来!

bash 复制代码
# 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

javascript 复制代码
import nodemon from "nodemon";
import { getPackageManager } from "../utils/index.mjs";

/**
 * 执行文件/目录的监听,当其变化时重新启动项目:
 *
 * 使用方式:tools -w .env
 * @param {object} argv - {@link https://google.github.io/zx/api#argv 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

javascript 复制代码
/**
 * 查看或切换个人/公司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 && $.p_name && $.p_name !== name) {
      name = $.p_name;
      // 设置个人昵称
      await $`git config --global user.name ${$.p_name}`;
    }
    if (p && $.p_email && $.p_email !== email) {
      email = $.p_email;
      // 设置个人邮箱
      await $`git config --global user.email ${$.p_email}`;
    }
    // 个人与公司不能同时设置
    if (!p && c && $.c_name && $.c_name !== name) {
      name = $.c_name;
      // 设置企业中的昵称
      await $`git config --global user.name ${$.c_name}`;
    }
    if (!p && c && $.c_email && $.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]

javascript 复制代码
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 https://google.github.io/zx/api#argv 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

javascript 复制代码
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import { copyTemplate } from "../utils/index.mjs";

/**
 * 复制.vscode配置
 *
 * 使用方式:tools --vscode
 * @param {object} argv - {@link https://google.github.io/zx/api#argv 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

javascript 复制代码
/**
 * 在控制台输出当前项目的贡献者名单
 *
 * 使用方式: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 入口文件
javascript 复制代码
#!/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
javascript 复制代码
// 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]) => {
          $[key] = value;
        });
      }
      res(true);
    } catch (error) {
      rej(error);
    }
  });
};

四、Result(结果)

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

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

本文demo的github地址

相关推荐
喵叔哟19 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django