一、Situation(情景)
在前端的日常开发中,我们需要执行一些重复性操作和快捷命令,例如:
- 实现
在本地启动项目后,监听到配置文件(如.env)的变化,自动重新启动项目
; - 快速切换
公司/个人2个层面的git提交信息(如:企业邮箱<-->个人邮箱、企业中的昵称<-->个人的昵称等)
- 快速生成
.gitignore
文件; - 快速生成符合个人习惯的
.vscode
配置; - 在控制台输出当前项目的贡献者名单;
- 等等。
面对这些任务,我们:
- 可能会采用CV大法;
- 可能会寻找一些开源辅助工具;
- 可能自行开发一些定制性的程序/脚本;
- 等等。
这些行为可能是一次性的,使用的模板/配置文件/自行开发的脚本可能也是散落在电脑的各个角落。
那么,有没有一种方式既能收藏好的模板/配置文件/命令片段,也能灵活高效的使用它们,还能持续完善并高效的迁移呢?
或许,开发一个私人定制的脚手架工具是一种不错的方式!
问:为什么说开发一个私人定制的脚手架工具是一种不错的方式?
答:因为可以在全局命令行中作为命令使用 。一个脚手架工具就是一个项目,我们可以在这个项目中存放各种文件,不限于收藏的模板文件、命令片段等,并将其配置为一条命令以便于随时灵活的使用。我们可以使用git进行版本控制,不停迭代。我们可以将项目放在github上(当然出于隐私保护,你可以将仓库设为Private),以便于在任何其他电脑上使用。
问:出于隐私保护的目的,并不准备发包到npm上,那么要如何作为命令使用该工具呢?
答:做好相应配置,然后在脚手架项目根目录下执行npm link
即可。
问:如何实现收藏功能?
答:其实就是将模板/配置文件分好类,然后放在项目的src/templates
目录下。以模板的形式或者你认为的比较方便使用的方式。
二、Task(任务)
本文主要分享、记录如何使用脚手架模式,打造私人定制的提效命令行界面(CLI)。
主要内容包含:
- 搭建基础环境(
这里node >= 16.0.0
),安装核心依赖库(zx
、nodemon
、inquirer
),其他工具函数都是zx
自带; - 记录
zx
的基本使用; - 针对上述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 -p
、tools --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 其他文件
- 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);
- 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种场景均提供了个人的实现方案、较详细的备注、命令行的使用方式说明。
代码可能并不优雅,这里主要是想分享这样一种思路!