1、为什么要做自定义脚手架
前端日常开发中有各种各样的脚手架:比如vue-cli , create-react-app , vite 还有其他很多社区的脚手架。通过这些脚手架我们可以很快的生成需要的项目模版,可以在此基础上快速的开发项目。但是这些脚手架都注重通用性 ,不太适合公司具体业务需求, 由于业务的特殊性(特有功能)和公司技术沉淀,所以,这时候就需要我们自己写一个cli脚手架工具,来更加高效的进行项目开发,减少重复操作!
2、自定义脚手架基本原理
3、核心功能
1、bin字段解析
在 package.json 中的 bin
字段,用以指定最终的命令行工具的名字,用作该 npm 包可执行文件的入口 。以 vite 为例
json
{
"bin": {
"vite": "bin/vite.js"
}
}
vite
是最终在终端执行的命令,而 ./bin/vite.js
是该命令实际执行的脚本文件。
对于最终可执行的命令行工具,node.js 项目一般倾向置文件于 bin
目录下,再如以下 Typescript 的命令行配置:
json
{
"bin": {
"tsc": "./bin/tsc",
"tsserver": "./bin/tsserver"
},
}
2、快速搭建脚手架项目
1、初始化
shell
mkdir xxx
cd xxx
npm init -y
2、全局访问
更目录新建bin/index.js
js
#! /usr/bin/env node
console.log('zkz~~~~')
- #! /usr/bin/env node 告诉操作系统用node环境 来执行index.js文件
3、配置package.json
json
"bin": {
"zkz": "./bin/index.js"
}
4、npm link 软链接 使得 zkz可以全局访问
npm link 原理简要说明:执行
npm link
,它会自动寻找当前目录的package.json
中的name
字段,并创建全局目录(~/.config/npm/link
)软链接至当前项目,然后项目中可以使用该模块
5、完毕可以访问
3、自定义命令行交互(核心)
在使用cli 脚手架的时候,通常有一些些命令,比如--version
, --help
来查看版本和帮助信息等
1、配置可执行命令commander:
js
// 解析用户输入的参数
program.parse(process.argv);
// 获取版本号
program
.version(require("../package.json").version)
.usage("<command> [options]");
js
// 同理配置 create 命令
program
.command("create <app-name>")
.description("create a new project powered by zkz-cli")
.option("-f, --force", "overwrite target directory if it exist")
.action((name, options) => {
// 调用create模块
require("../lib/create.js")(name, options);
// console.log("%c Line:12 🥖 name, options", "color:#42b983", name, options);
});
2、实现命令行交互inquirer:
当进行了步骤1、配置可执行命令 之后,我们要去解析用户输入参数: 新建lib/create.js
js
const path = require("path");
const fs = require("fs-extra");
const inquirer = require("inquirer");
const Creator = require("./Creator");
module.exports = async (projectName, options) => {
console.log("%c Line:6 🥓 options", "color:#ed9ec7", options);
// 1. 创建项目
const cwd = process.cwd(); //获取当前命令执行时的工作目录
const targetDir = path.join(cwd, projectName); // 目标目录
if (fs.existsSync(targetDir)) {
// 如果目标目录已经存在
if (options.force) {
// 如果用户传入了 --force 参数,则删除已存在的目录
await fs.remove(targetDir);
} else {
// 提示用户目标目录已存在, 是否覆盖
let { action } = await inquirer.prompt([
{
name: "action",
type: "list",
message: "Target directory already exists Pick an action:",
choices: [
{ name: "Overwrite", value: "overwrite" },
{ name: "Cancel", value: false },
],
},
]);
// 取消创建
if (!action) {
return;
} else if (action === "overwrite") {
// 移除已存在的目录
console.log(`\r\nRemoving...😊`);
await fs.remove(targetDir);
}
}
}
// 创建项目, 封装为Creator类
const creator = new Creator(projectName, targetDir);
creator.create(); // 开始创建项目
};
3、远程下载模版
接上一步开始创建项目后,这里我们选择远程下载项目模版,可分为三步
js
class Creator {
constructor(projectName, targetDir) {
this.name = projectName;
this.target = targetDir;
// promisify downloadGitRepo, 使其返回promise
this.downloadGitRepo = util.promisify(downloadGitRepo);
}
async fetchRepo() {
...
}
async fetchTag(repo) {
...
}
async download(repo, tag) {
...
}
async create() {
// 开始创建项目
// 远程获取模版github
// 1、 获取当前组织下的模版
let repo = await this.fetchRepo();
// 2、 获取模版的版本号
let tag = await this.fetchTag(repo);
// 3、 下载模版到模版目录
let downloadUrl = await this.download(repo, tag);
// 4、编译模版
}
}
- 获取当前组织下的模版
- 获取模版的版本号
- 下载模版到模版目录
涉及到的github api 官网地址
功能 | api地址 | 请求方式 | 请求参数 | 返回参数 |
---|---|---|---|---|
获取用户信息 | api.github.com/users/ | get | path路径: 用户名 | 一个用户对象 |
获取用户所有仓库 | api.github.com/users/{用户名}... | get | path路径: 用户名 | 返回一个数组 |
获取某个仓库的详细信息 | api.github.com/repos/{用户名}... | get | path路径: 用户名 和 仓库名 | 返回一个仓库对象 |
获取某个仓库里根目录文件或文件夹数组 | api.github.com/repos//{用户名... | get | path路径: 用户名 和 仓库名 | 返回一个首层文件或文件夹数组 |
获取某个仓库里子目录文件或文件夹数组 | api.github.com/repos//{用户名... | get | path路径: 用户名 和 仓库名和文件名或文件夹名 | 返回一个文件数组 |
获取某文件的原始内容(Raw) | 1. 通过上面的文件信息中提取download_url这条链接,就能获取它的原始内容了。2. 或者直接访问:raw.githubusercontent.com/{用户名}/{仓库名}... | get | path路径: 用户名 和 仓库名和文件l路径 | 返回一个文件内容的字符串 |
获取某个用户的跟随者列表 | api.github.com/users/{用户名}... | get | path路径: 用户名 | 返回一个数组 |
获取某个用户正在关注谁列表 | api.github.com/users/{用户名}... | get | path路径: 用户名 | 返回一个数组 |
获取某个用户加入的组织列表 | api.github.com/users/{用户名}... | get | path路径: 用户名 | 返回一个数组 |
repo中所有的commits列表 | api.github.com/repos/{用户名}... | get | - | - |
某一条commit详情 | api.github.com/repos/{用户名}... | get | - | - |
issues列表 | api.github.com/repos/{用户名}... | get | - | - |
某条issue详情 | api.github.com/repos/{用户名}... | get | issues都是以1,2,3这样的序列排号的 | - |
某issue中的comments列表 | api.github.com/repos/{用户名}... | get | - | - |
某comment详情 | api.github.com/repos/{用户名}... | get | 评论ID是从issues列表中获得的 | - |
4、优化+缺陷:
1、美化
- ora 加载 loading
js
const ora = require("ora");
// 睡眠函数
function sleep(n) {
return new Promise((resolve, reject) => setTimeout(resolve, n));
}
async function waitFnLoading(fn, message, ...args) {
const spinner = ora(message);
spinner.start();
try {
let result = await fn(...args);
spinner.succeed("Loaded successfully");
return result;
} catch (e) {
if (e.message.includes("download failed")) {
spinner.fail("request failed, refetch ...");
// 休息1秒
await sleep(2000);
spinner.stop();
// 重新请求
return waitFnLoading(fn, message, ...args);
} else {
spinner.fail(e.message);
throw e;
}
}
}
module.exports = {
waitFnLoading,
sleep,
};
- chalk 文本样式
js
console.log(chalk.gray(zkzCli));
console.log(`Done. Now run:\r\n`);
console.log(chalk.green(`cd ${this.name}`));
console.log(chalk.blue("npm install"));
console.log(chalk.magenta("npm run dev\r\n"));
5、小结+源码
1、换个思路,是否可以搞一个桌面端的应用,做一个前端研发平台,里面集成脚手架的功能,但是这个只是一部分,之后你可以扩展更多的功能。专注于研发提效
2、可以支持自定义配置下载模板? 比如, 其他团队, 有自己的模板, 只需要添加选项和git的下载地址, 就可以用了