https://gitee.com/childe-jia/kfc-cli.git
gitee完整地址
介绍
为什么需要脚手架?
脚手架本质就是一个工具,作用是能够让使用者专注于写代码,它可以让我们只用一个命令就生成一个已经配置好的项目,而不用我们再花时间去配置和安装相关依赖,可以在很大程度上提升我们的开发效率。比如我们常用的create-vue
和create-react-app
就是脚手架,很多大厂也都有自己的脚手架。
一个脚手架应该具备哪些功能?
我们以vue官方的脚手架create-vue
为例来分析下一个脚手架应该具备哪些功能?
-
运行命令创建项目
➜ npm create vue@latest
-
用户根据自己需要选择一些配置项
- 根据选择的配置项会生成一个模版项目
通过分析create-vue
,我们可以知道,一个脚手架如果想要创建一个项目,最少要有以下两点功能:
- 可以通过命令行和用户交互
- 根据交互的结果去生成对应的模版项目
脚手架实现
初始化项目
- 执行如下初始化命令
➜ mkdir kfc-vme50
➜ cd kfc-vme50
➜ npm init -y
- 在根目录下创建
bin/index.js
文件作为入口文件,并添加如下代码
#!/usr/bin/env node
console.log('肯德基疯狂星期四v我50')
- 在package.json中添加bin字段
"bin": {
"kfc-vme50": "/bin/index.js"
}
- 在根目录下执行
npm link
将项目链接到本地环境,就可以实现kfc-vme50
命令全局调用 - 运行
kfc-vme50
并查看控制台输出
相关依赖
实现一个脚手架,通常会用到以下依赖包
- commander:命令行处理工具
#!/usr/bin/env node
// #! 是shebang的标识,告诉操作系统这是一个脚本文件。
// /usr/bin/env 是一个程序,用来查找环境变量中定义的程序路径。在这个例子中,它用来查找node的路径。
// node 是Node.js的可执行文件名,它是运行JavaScript代码的运行时环境。
// 它用于处理命令行参数;
const { program } = require("commander");
/**
* .name 命令名称出现在帮助中,也用于定位独立的可执行子命令。
* .usage 通过这个选项可以修改帮助信息的首行提示
*/
program.name("kfc-creat").usage("<command> [option]");
/**
* 「选项」 定义选项
* 使用.option()方法来定义选项,同时可以附加选项的简介。每个选项可以定义一个短选项名称(-后面接单个字符)
* 一个长选项名称(--后面接一个或多个单词),使用逗号、空格或|分隔。
* 有两种最常用的选项,一类是 boolean 型选项,选项无需配置参数,
* 另一类选项则可以设置参数(使用尖括号声明在该选项后,如--expect <value>)。
* 如果在命令行中不指定具体的选项及参数,则会被定义为undefined
*/
program
.option("-d, --debug", "output extra debugging")
.option("-s, --small", "small pizza size")
.option("-p, --pizza-type <type>", "flavour of pizza");
/**
* 「命令」 通过.command()或.addCommand()可以配置命令,
* .command()的第一个参数为命令名称。命令参数可以跟在名称后面,也可以用.argument()单独指定。
* 参数可为必选的(尖括号表示)、可选的(方括号表示)或变长参数(点号表示,如果使用,只能是最后一个参数)。
* ----------------------------------------------------------------
* description 出现在命令的帮助中。
* action 命令触发后的回调函数 [命令行的参数]
*/
program
.command("clone <source> [destination]")
.description("clone a repository into a newly created directory")
.action((source, destination) => {
console.log("clone command called");
console.log(source, destination);
});
/**
* 「parse」解析摩命令行参数
* program.parse 它的作用是解析 process.argv 数组,将命令行参数转换为可操作的对象。
* process.argv 是 Node.js 中的一个全局变量,它是一个数组,包含了命令行启动脚本时传递给 Node.js 进程的参数。
数组的第一个元素 process.argv[0] 总是 node, 表示 Node.js 可执行文件的路径 (安装路径)
接下来的元素是脚本文件的路径,即你正在运行的 JavaScript 文件的路径。
之后的元素是传递给脚本的命令行参数
*/
program.parse(process.argv);
/**
* 「opts」获取命令行参数
* 解析后的选项可以通过Command对象上的.opts()方法获取,同时会被传递给命令处理函数。
*/
const options = program.opts();
console.log(options);
console.log("肯德基疯狂星期四v我500");
- chalk:命令行输出美化工具
#!/usr/bin/env node
// 重要提示:Chalk 5 部分是 ESM。如果您想将 Chalk 与 TypeScript 或构建工具一起使用,您现在可能需要使用 Chalk 4
/**
ESM 是 "ECMAScript Module" 的缩写,它指的是一种 JavaScript 模块的规范,允许开发者将代码分割成可重用的模块。
ESM 是现代 JavaScript 的一个特性,它支持静态模块的导入和导出,这意味着模块的依赖关系可以在编译时就确定下来,从而提高代码的加载和执行效率。
然而,ESM 也有它的限制,比如它不支持 CommonJS 模块中的 require() 函数,而是使用 import() 来动态加载模块。
此外,ESM 需要在支持 ESM 的环境中使用,比如现代浏览器或者使用 Babel 等工具转换的 Node.js 环境。
*/
const chalk = require("chalk");
// 颜色
console.log(chalk.yellow("Welcome"));
// 加粗
console.log(chalk.red.bold("Welcome"));
// 背景色
console.log(chalk.yellow.bold.bgBlue("Welcome"));
- inquirer:命令行交互工具
#!/usr/bin/env node
/**
* !警告] Inquirer v9 及更高版本是本机 esm 模块,
* 这意味着您不能再使用 commonjs 语法 require('inquirer')
* 或者,如果您需要 commonjs 模块,则应该依赖旧版本,直到准备好升级环境
*/
const inquirer = require("inquirer");
/**
* inquirer.prompt(questions, answers) -> promise
* 启动提示界面(查询会话)
* questions (Array) 包含 Question 对象(使用反应式接口,还可以传递 Rx.Observable 实例)
* 答案(对象)包含已回答问题的值。询问者将避免询问此处已提供的答案。默认值 {}。
*
*「Question对象」 问题对象是包含问题相关值的哈希
type (字符串)提示的类型。【input, number, confirm, list, rawlist, expand, checkbox, password, editor】
name:(字符串)将答案存储在答案哈希中时使用的名称。如果名称包含句点,它将在答案哈希中定义路径。
message:(字符串|函数)要打印的问题。如果定义为函数,第一个参数将是当前询问者会话的答案。默认为 name 的值(后跟冒号)
default:(字符串|数字|布尔值|数组|函数)未输入任何内容时使用的默认值,或返回默认值的函数。如果定义为函数,第一个参数将是当前询问者会话的答案
*
*/
inquirer
.prompt([
// 将你的问题放在这
{
type: "input",
name: "food",
message: "你吃啥",
default: "披萨",
},
{
type: "confirm",
name: "hot",
message: "吃不吃辣",
default: false,
},
])
.then((answers) => {
//使用用户反馈。。。无论什么结果
console.log(answers);
})
.catch((error) => {
if (error.isTtyError) {
// 无法在当前环境中呈现提示
} else {
//其他问题
}
});
- ora:终端loading美化工具
#!/usr/bin/env node
const ora = require("ora");
// 启动旋转器。返回实例。如果提供了文本,则设置当前文本。
const spinner = ora("Loading unicorns").start();
setTimeout(() => {
spinner.color = "yellow";
spinner.text = "Loading rainbows";
}, 1000);
// 停止旋转器,将其更改为绿色 ✔ 并保留当前文本或文本(如果提供)。返回实例。请参阅下面的 GIF。
// setTimeout(() => {
// spinner.succeed("succeed");
// }, 2000);
// 停止旋转器,将其更改为红色 ✖ 并保留当前文本或文本(如果提供)。返回实例。请参阅下面的 GIF。
setTimeout(() => {
spinner.fail("fail");
}, 2000);
- git-clone:下·载项目模版工具
- figlet:终端生成艺术字
#!/usr/bin/env node
var figlet = require("figlet");
// 将 Figlet 对象作为函数调用是调用文本函数的简写。此方法允许您从文本创建 ASCII 艺术。
// 输入文本 - 要转换为 ASCII 艺术的文本字符串。
// 选项 - 指示字体名称的字符串或选项对象(如下所述)
// 回调 - 使用生成的 ASCII Art 执行的函数。
figlet("Hello World!!", function (err, data) {
if (err) {
console.log("Something went wrong...");
console.dir(err);
return;
}
console.log(data);
});
// 该方法是上述方法的同步版本
// 输入文本 - 要转换为 ASCII 艺术的文本字符串。
// 字体选项 - 指示字体名称的字符串或选项对象(如下所述)。
console.log(
figlet.textSync("Boo!", {
font: "Ghost", //类型:字符串 默认值:'标准' 指示要使用的 Figlet 字体的字符串值。
horizontalLayout: "default", //指示要使用的水平布局的字符串值
verticalLayout: "default", //指示要使用的垂直布局的字符串值
width: 80, //宽度
whitespaceBreak: true, //此选项与“宽度”结合使用。如果此选项设置为 true,则库在限制宽度时将尝试在空白处分解文本。
})
);
- fs-extra:用来操作本地目录
talk is cheap, show me the code
#!/usr/bin/env node
// 操作终端命令行
const { program } = require("commander");
// 艺术字
const figlet = require("figlet");
// 操作文件
const fs = require("fs-extra");
// 获取路径
const path = require("path");
// 命令行交互
const inquirer = require("inquirer");
// 彩色输出
const chalk = require("chalk");
//控制台loadding
const ora = require("ora");
// clone 项目
const gitClone = require("git-clone");
// 项目仓库
const projectList = {
vue: "https://gitee.com/y_project/RuoYi-Vue.git",
react: "https://gitee.com/whiteshader/ruoyi-react.git",
"react&ts": "https://gitee.com/whiteshader/ruoyi-react.git",
"vue&ts": "https://gitee.com/lyforvue/ruoyi_vue3_ts.git",
};
// 修改帮助信息的首行展示
program.usage("<command> [options]");
// 版本号
program.version(`v${require("../package.json").version}`);
// 艺术字展示 监听 help添加提示信息
program.on("--help", function () {
console.log(
figlet.textSync("kfc vme50", {
font: "Ghost",
horizontalLayout: "default",
verticalLayout: "default",
width: 100,
whitespaceBreak: true,
})
);
});
// 创建项目的命令
program
.command("create <app-name>") // 创建项目的命令 name必填
.description("创建新项目") //描述
//执行命令后的回调【命令后的值,】
.action(async function (name, option) {
//创建以一个名为name的文件夹,把代码放到文件夹下
const cwd = process.cwd(); //获取命令执行的文件目录
// 创建项目的位置
const targetPath = path.join(cwd, name);
// 如果文件夹存在
if (fs.existsSync(targetPath)) {
const res = await inquirer.prompt([
{
name: "action",
type: "list",
message: "是否覆盖已有文件夹?",
choices: [
{
name: "YES",
value: true,
},
{
name: "NO",
value: false,
},
],
},
]);
//不覆盖 取一个新的名字
if (!res.action) return;
fs.remove(targetPath);
console.log(chalk.red("已删除之前的文件夹"));
}
//新建项目
const res = await inquirer.prompt([
{
name: "type",
type: "list",
message: "请选择使用的框架",
choices: [
{
name: "Vue",
value: "vue",
},
{
name: "React",
value: "react",
},
],
},
{
name: "ts",
type: "list",
message: "是否使用ts项目",
choices: [
{
name: "YES",
value: true,
},
{
name: "NO",
value: false,
},
],
},
]);
// 是否为ts
const rep = res.type + (res.ts ? "&ts" : "");
// 拉取项目模板
const spinner = ora("正在加载项目模板...").start();
gitClone(
projectList[rep], //拉去路径
targetPath, //保存路径
//分支
{
checkout: "master",
},
//回调函数
(err) => {
if (!err) {
fs.remove(path.resolve(targetPath, ".git"));
spinner.succeed("项目模板加载完成!");
console.log("now run:");
console.log(chalk.green(`\n cd ${name}`));
console.log(chalk.green(" npm install"));
console.log(
chalk.green(` npm run ${res.type === "react" ? "start" : "dev"}\n`)
);
} else {
spinner.fail(chalk.red("项目模板加载失败,请重新获取!", err));
}
}
);
});
//解析控制台参数
program.parse(process.argv);
发布
- 注册npm账号
- 在本地登录并发布
# 登录刚注册的账号
➜ npm login
Username: 用户名
Password: 密码
Email: 注册邮箱
Enter one-time password: 一次性密码 邮箱会收到邮件
# 在我们脚手架的根目录下执行发布命令
➜ npm publish
注意:
登录和发包前一定要先查看npm的源,需要修改为
https://registry.npmjs.org/
在发布时包名不能重复,所以可以先在线上搜索下看看有没有存在的包,如果出现403错误可能是包名和线上的包重复了,修改package.json中的name即可