自定义脚手架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的下载地址, 就可以用了

相关推荐
zhougl99635 分钟前
html处理Base文件流
linux·前端·html
花花鱼39 分钟前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_42 分钟前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo2 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
杉之4 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端4 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡4 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木5 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!5 小时前
优选算法系列(5.位运算)
java·前端·c++·算法
難釋懷6 小时前
JavaScript基础-移动端常见特效
开发语言·前端·javascript