自定义脚手架cli

1、为什么要做自定义脚手架

前端日常开发中有各种各样的脚手架:比如vue-cli , create-react-app , vite 还有其他很多社区的脚手架。通过这些脚手架我们可以很快的生成需要的项目模版,可以在此基础上快速的开发项目。但是这些脚手架都注重通用性 ,不太适合公司具体业务需求, 由于业务的特殊性(特有功能)和公司技术沉淀,所以,这时候就需要我们自己写一个cli脚手架工具,来更加高效的进行项目开发,减少重复操作!

2、自定义脚手架基本原理

3、核心功能

1、bin字段解析

官网package.json中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"
  },
}

再比如vue-cli

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(); // 开始创建项目
};

引用:fs-extrainquirer

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的下载地址, 就可以用了

相关推荐
╰つ゛木槿27 分钟前
深入了解 React:从入门到高级应用
前端·react.js·前端框架
m0_748241231 小时前
ElasticPDF-新国产 PDF 编辑器开发框架(基于 pdf.js Web PDF批注开发,实现高亮多边形橡皮擦历史记录保存注释文字)
前端·pdf·编辑器
huapiaoy1 小时前
JavaSE---String(含一些源码)
java·linux·前端
rkmhr_sef1 小时前
frp内网穿透云服务器。云服务器映射多个家庭局域网内网端口。家庭Windows主机内网运行多个web程序
服务器·前端·windows
带多刺的玫瑰1 小时前
Leecode刷题C语言之考场就座
c语言·前端·javascript
麦子爱种地1 小时前
前端学习DAY26(华为平板页面)
服务器·前端·javascript
m0_748248021 小时前
HTML5前端实现毛玻璃效果的可拖拽登录框
前端·html·html5
rainoway1 小时前
CRDT宝典 - Multi-Value-Register
前端·分布式·算法
Loong_DQX1 小时前
【前端js】 indexedDB Nosql的使用方法
前端·javascript·nosql·indexeddb