前端开发已经两年多了,大大小小的项目也做过不少,每次开新项目时不是直接使用vue-cli从头搭建架构,就是把之前的项目直接复制粘贴过来修修改改。当然稍微聪明一点的是弄个基础模板放在本地或者放在github,需要的时候直接clone过来。
前段时间刚好换新工作,相对于之前的公司来说事少了很多,就开始想为什么不做一个类似vue-cli的命令行工具呢(刚好现在这家公司也没有)。说干就干,经过一段时间的资料搜索,也是如愿以偿的做了出来。在这里给想要偷懒的小伙伴分享下。
项目的远程地址在 github ,有需要的同学可自行clone,有用的话别忘了点个start哦~
首先贴下目录结构
├─.gitignore
├─config.json // 拉取远程模板工程目录(我这里用的是gitee,因为github有时候clone不下来)
├─LICENSE
├─package.json
├─pnpm-lock.yaml
├─README.md
├─test // 测试用目录
| ├─chalk.js
| ├─child_process.js
| ├─commander.js
| ├─inquirer.js
| └ora.js
├─lib // 功能目录
| ├─utils // 方法封装
| | ├─config.js
| | ├─execFn.js
| | └removeDir.js
| ├─core // cli核心命令
| | ├─add.js // 新增模板(向config.json写入模板)
| | ├─create.js // 创建项目命令
| | ├─delete.js // 删除模板(移除config.json模板)
| | ├─list.js // 展示现有模板列表
| | └upgrade.js // 检查更新
├─bin // 执行文件目录
| └cli.js // 命令执行文件
一. 初始化package.json
这里采用的是EJS语法(因为有些库像commander的新版本使用的是ejs),如果要用cjs就把 “type”: “module” 这句删除就可以了(当然某些依赖包响应的就需要降到旧版本了),node目前默认是cjs环境(也就是 require 、module.exports 等语法)。不过使用ejs还是有一些不兼容的东西,比如__dirname在ejs环境就不能直接使用,后面提到的时候会说解决办法
{
"name": "sy-prt-cli",
"version": "1.0.5",
"description": "集成项目开发模板",
"main": "/bin/cli.js",
"type": "module",
"bin": {
"sy": "bin/cli.js" // 这里的 sy (名字可以随便取) 就是全局命令了,后面的命令都是通过 sy xxx来执行
},
"directories": {
"lib": "lib"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/zhiquanli-web/sy-prt-cli.git"
},
"keywords": [
"cli",
"vue3",
"node"
],
"author": "sy",
"license": "ISC",
"dependencies": {
"chalk": "^5.0.1",
"child_process": "^1.0.2",
"clear": "^0.1.0",
"commander": "^9.4.0",
"handlebars": "^4.7.7",
"inquirer": "^9.1.2",
"ora": "^6.1.2",
"update-notifier": "^6.0.2"
}
}
二. 依赖包简要说明
1. chalk: 用来给输出内容添加各种颜色
console.log(chalk.blue('hello world'))
console.log(chalk.rgb(255,0,255)('hello world'))
...
2. child_process: 可以帮助我们创建子进程来执行命令,本项目主要使用exec方法,具体用法执行查看npm
const cmd = "git clone -b master https://gitee.com/qy-tingyun/vue-admin-template.git ./test";
exec("cmd", (error) => {
if (error) {
console.log("发生了一个错误", chalk.red(JSON.stringify(error)));
process.exit();
}
resolve(true);
});
3. clear:清空前面所有输出内容
4. commander:负责将命令行输入的参数解析为选项和命令参数
5. handlebars::是一个页面模板化工具,本项目中用于覆写模板文件package.json中的{{name}},更多用法执行查看npm
6. inquirer:命令行交互工具(用于跟使用者进行交互如: 解析输入,校验问答等)
7. ora:主要用来实现node.js命令行环境的loading效果,和显示各种状态的图标等
8. update-notifier:检测本地与远程包是否有版本更新
三. 配置bin/cli.js
#! /usr/bin/env node 一定要置顶,表示用node来执行此文件
#! /usr/bin/env node
// Commander 负责将命令行输入的参数解析为选项和命令参数
// 具体信息及使用方法可访问:https://github.com/tj/commander.js/blob/master/Readme.md
import { program } from "commander";
import { readFile } from "fs/promises";
// 获取版本号
// 正如前文所说,在ejs环境无法使用require导入json文件,这里的是用下面的方法来做替换方案
// 如果用的是cjs就不需要这么麻烦了,直接使用require('../package.json')就可以引入
program.version(
JSON.parse(await readFile(new URL("../package.json", import.meta.url)))
.version,
"-V, --version"
);
// 这个一定要有,用于解析node环境下的一些参数等
program.parse(process.argv);
if (!program.args.length) {
program.help();
}
到这里之后 可以在命令行执行 npm link,将项目关联起来(类似于使用npm安装了个全局包)。之后你就可以在任何位置使用命令 sy -V 或 sy --version 来查看版本
四. 模板文件config.json配置信息如下
{
"templates": {
"vue-template": { // 模板名
"url": "https://gitee.com/qy-tingyun/vue-admin-template", // 远程模板地址
"branch": "master" // 代码拉取分支
},
"vue-emement-template": {
"url": "https://gitee.com/qy-tingyun/vue-admin-template",
"branch": "element-template"
},
"vue-antd-template": {
"url": "https://gitee.com/qy-tingyun/vue-admin-template",
"branch": "antd-template"
}
}
}
五. 创建项目(sy create [project])
#! /usr/bin/env node
import { program } from "commander";
import { readFile } from "fs/promises";
import myCreate from "../lib/core/create.js";
// 获取版本号
program.version(
JSON.parse(await readFile(new URL("../package.json", import.meta.url)))
.version,
"-V, --version"
);
myCreate(program);
program.parse(process.argv);
if (!program.args.length) {
program.help();
}
import clear from "clear";
import inquirer from "inquirer";
import ora from "ora";
import handlebars from "handlebars";
import fs from "fs";
import chalk from "chalk";
import execFn from "../utils/execFn.js";
import removeDir from "../utils/removeDir.js";
import myConfig from "../utils/config.js";
const spinner = ora();
function myInit(program) {
program
.command("create <project>")
.description("create a new project")
.action(async (projectName) => {
const config = await myConfig();
clear();
const answer = await inquirer.prompt([
{
name: "templateName",
type: "list",
message: "请选择你的目标模板",
choices: Object.keys(config.templates),
},
]);
spinner.start();
spinner.text = "正在下载...";
const { url, branch } = config.templates[answer.templateName];
const cloneCmd = `git clone -b ${branch} ${url} ${projectName}`;
await execFn(cloneCmd, spinner);
// 覆写模板项目中的{{name}}
const meta = {
name: projectName,
};
const content = fs.readFileSync(`${projectName}/package.json`).toString();
const result = handlebars.compile(content)(meta);
fs.writeFileSync(`${projectName}/package.json`, result);
// 删除模板中的 .git 文件
removeDir(`${projectName}/.git`);
// git init
let gitInitCmd = `git -C ./${projectName} init`;
await execFn(gitInitCmd);
// git add . && git commit
const gitCommitCmd = `git -C ./${projectName} add . && git -C ./${projectName} commit -m init`;
await execFn(gitCommitCmd);
spinner.succeed("下载完成");
console.log(chalk.cyan(`\n cd ${projectName} && pnpm i \n`));
process.exit();
});
}
export default myInit;
// execFn.js
import { exec } from "child_process";
import chalk from "chalk";
function execFn(cmd, spinner) {
return new Promise((resolve) => {
exec(cmd, (error) => {
if (error) {
console.log("发生了一个错误:", chalk.red(JSON.stringify(error)));
spinner.fail("下载失败");
process.exit();
}
resolve(true);
});
});
}
export default execFn;
// removeDir.js
import fs from "fs";
import path from "path";
function removeDir(dir) {
let files = fs.readdirSync(dir); //返回一个包含“指定目录下所有文件名称”的数组对象
for (var i = 0; i < files.length; i++) {
let newPath = path.join(dir, files[i]);
let stat = fs.statSync(newPath);
if (stat.isDirectory()) {
removeDir(newPath); //判断是否是文件夹,如果是文件夹就递归下去
} else {
fs.unlinkSync(newPath); //删除文件
}
}
fs.rmdirSync(dir); //如果文件夹是空的,就将自己删除掉
}
export default removeDir;
// config.js
import { readFile } from "fs/promises";
// 读取配置文件
async function myConfig() {
const config = JSON.parse(
await readFile(new URL("../../config.json", import.meta.url))
);
return config;
}
export default myConfig;
六. 查看模板列表(sy list 或 sy ls)
// bin/cli.js
import myList from "../lib/core/list.js";
myList(program);
// lib/core/list.js
import chalk from "chalk";
import myConfig from "../utils/config.js";
function myList(program) {
program
.command("list")
.alias("ls")
.description("show template list")
.action(async () => {
const config = await myConfig();
let str = "";
Object.keys(config.templates).forEach((item, index, array) => {
if (index === array.length - 1) {
str += item;
} else {
str += `${item} \n`;
}
});
console.log(chalk.cyan(str));
process.exit();
});
}
export default myList;
七. 查看远程是否有新版本(sy upgrade 或 sy u)
// bin/cli.js
import myUpgrade from "../lib/core/upgrade.js";
myUpgrade(program);
// lib/core/upgrade.js
import chalk from "chalk";
import { readFile } from "fs/promises";
import updateNotifier from "update-notifier";
const TIME = 1000 * 60 * 60 * 24;
function myUpgrade(program) {
program
.command("upgrade")
.alias("u")
.description("check the sy-prt-cli version.")
.action(async () => {
const pkg = JSON.parse(
await readFile(new URL("../../package.json", import.meta.url))
);
const notifier = updateNotifier({
pkg,
updateCheckInterval: TIME,
});
if (notifier.update) {
console.log(
`有新版本可用:${chalk.cyan(
notifier.update.latest
)},建议您在使用前进行更新`
);
notifier.notify();
} else {
console.log(chalk.cyan("已经是最新版本"));
}
});
}
export default myUpgrade;
八.发布
执行 npm publish,当然在这之前你需要登录npm login;如果没有npm账号的需要去注册个。这里还要注意npm中是否已存在相同的包名,
可以先在npm搜索下在发布。发布完成之后就可以使用了
补充
在ejs环境下 无法直接使用 __dirname,需要使用下面的替换方案
import path, { dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
path.resolve(__dirname, "../../config.json"),
剩下的 添加、删除模板就不贴上来了(当然有需要的自行clone远程完整项目),基本上弄懂前面的要实现都不难