开发一个前端脚手架,立马增加摸鱼时间!🐠🐠🐠

746 阅读5分钟

在开发过程中,我们会通过官方的脚手架搭建项目,这没问题。在我们把路由,工具函数,layout,网络封装以及页面都写好时,这个时候你要开发一个新项目,难道又重新来一次吗?

粘贴复制是麻烦的,那可以开发一款符合公司的脚手架,基于脚手架一键生成基本demo,将上述的layout、utils、page、router等集成进去,其他的时间就可以摸鱼了🐠

需求分析

脚手架三要素分别是创建项目命令,版本号和选项,以下的工作都将围绕着这三要素进行

bin

例如vue/cli create-react-app创建vue项目和react项目,而创建一个脚手架的第一步就是package.json中的bin字段。

当在package.json中添加这个字段时,会创建一个脚本到/user/local/bin/cli-create的符号连接,将运行该脚本。

本地开发时先执行npm link将该包链接到npm目录下 ,这个目录可以直接访问,完成之后会生成cli-create命令

{
  "bin": {
    "cli-create": "index.js"
  },
}

然后在index.js文件中首行添加 \#!/usr/bin/env node表示在node环境下执行

image-20250123151128704

image-20250123151201270

至此,第一步脚手架全局命令就成功了,接着就是利用Commander添加具体的命令和选项,如cli-create create vue-app

Commander

nodejs命令行的解决方案

安装

npm install commander

脚手架版本

const { Command } = require("commander");
const program = new Command();

program.version("cli-create@" + require("./package.json").version); // 定义脚手架版本

image-20250123152915537

脚手架选项

Commander 使用.opton()方法来定义选项,同时可以附加选项的简介。

解析后的选项可以通过Command对象上的.opts()方法获取,同时会被传递给命令处理函数。

#!/usr/bin/env node
const { Command } = require("commander");
const program = new Command();

program.version("cli-create@" + require("./package.json").version); // 定义脚手架版本

/**
 * 定义选项
 *  需要注意两个点
 *  1是不能将--name和<value>写在一起,如--name<value>,必须有一个空格,--name <value>
 *  2是option要单独写,不能将其写在program.command后面,不然读不出来
 */
program.option("-n, --name <value>", "this is cli-create's name","cli-create"); // 定义脚手架选项

const options = program.opts(); // 获取脚手架所有的选项
console.log(options);
program.parse(process.argv);

image-20250123155420788

image-20250123155457797

脚手架命令

通过.command()配置命令。

.command()的第一个参数为命令名称。命令参数可以跟在名称后面,也可以用.argument()单独指定。参数可为必选的(尖括号表示)、可选的(方括号表示)或变长参数(点号表示,如果使用,只能是最后一个参数)。

#!/usr/bin/env node
const { Command } = require("commander");
const program = new Command();

program
  .command("create <project-name> [second]")
  .description("create a new project by cli-create")
  .action((name, second) => {
    console.log(name, second); // 打印 create命令后面的参数
  });
program.parse(process.argv);

image-20250123170406130

image-20250123170452228

到这里,我们已经创建了最基础的命令,接下来就是在 action 这个方法中处理

action使用

通过命令create 创建项目,需要一个代码仓库模版通过类似于git clone url方式拉取下来,再进入到文件夹中通过 npm install 下载依赖,最后执行 npm run serve 。到此一个最简单的脚手架就创建完成了。

download-git-repo

一个 Node.js 模块,它允许你直接从 Git 仓库下载项目,支持github,gitlab,不支持码云

npm install download-git-repo

参数详情如下:

  • 第一个是模版仓库地址,默认master分支,通过在末尾添加#main切换到main分支
    • 建议通过direct和clone的方式,不然需要将完整的url传递给zip文件
  • 第二个是要下载的文件路径
  • 第三个是选项,alone:true表示通过git clone的方式下载
#!/usr/bin/env node
const { Command } = require("commander");
const download = require("download-git-repo");
const program = new Command();

program
  .command("create <project-name> [repositoryUrl]")
  .description("create a new project by cli-create")
  .action((name, second) => {
    console.log("开始下载脚手架模版~");
    download(
      "direct:https://github.com/wczy-ao/project-vue2-template.git#main",
      name,
      {
        clone: true,
      },
      function (err) {
        if (!err) {
          console.log("下载成功~");
        }
      }
    );
  });
program.parse(process.argv);

image-20250124095908766

image-20250124100250295

安装模版依赖

上面已经把模版代码下载下来了,但是没有通过npm install / yarn / pnpm i添加依赖,这一步可做可不做,因为不知道当前用户习惯用哪个,如果用的不适合则有可能会出现混乱。

在这里我们先使用pnpm安装,后面优化通过命令来实现。

pnpm需要nodejs版本至少 18.12,所以我们需要针对这个判断一下,我们改一下代码

const pnpmVersion = 18.12;
program
  .command("create <project-name> [repositoryUrl]")
  .description("create a new project by cli-create")
  .action((name, second) => {
    console.log("开始下载脚手架模版~");
    const nodeVersionArr = process.version.substring(1).split(".");
    const nodeVersion = nodeVersionArr[0] + "." + nodeVersionArr[1];

    download(
      "direct:https://github.com/wczy-ao/project-vue2-template.git#main",
      name,
      {
        clone: true,
      },
      function (err) {
        if (!err) {
          console.log(
            "code repository dowload success, start download node_modules by pnpm, please wait"
          );
          if (pnpmVersion - Number(nodeVersion) > 0) {
            console.log(
              "pnpm need nodejs version at least V18.12,please upgrade node version"
            );
          } else {
            // 开始安装 node_modules
          }
        }
      }
    );
  });

安装node_modules通过child_processspawn来实现

  • 第一个参数是要执行的命令(字符串)
  • 第二个参数是一个数组,包含了传递给命令的所有参数。
  • 事件监听
    • stdout:当子进程产生标准输出时触发。
    • stderr:当子进程产生错误输出时触发。
    • close:当子进程退出时触发,提供退出代码。

spawn默认不缓冲I/O数据,而是以流的形式实时读取子进程的标准输出和错误输出,因此更适合处理大量数据或长时间运行的任务

const { spawn } = require("child_process");
program.  .action((name, second) => {
    console.log("start download project template~");
    download(
      "direct:https://github.com/wczy-ao/project-vue2-template.git#main",
      name,
      {
        clone: true,
      },
      function (err) {
        if (!err) {
          console.log(
            "code repository dowload success, start download node_modules by pnpm, please wait"
          );
          if (pnpmVersion - Number(nodeVersion) > 0) {
            console.log(
              "pnpm need nodejs version at least V18.12,please upgrade node version"
            );
          } else {
            // 开始安装 node_modules
            const result = spawn("pnpm", ["i"], {
              cwd: `./${name}`,
            });
          }
        }
      }
    );
  });

image-20250124174625037.png spawn是异步的,我们这里通过promise优化一下

// lib/handleSpawn.js
const { spawn } = require("child_process");

function handleSpawn(...args) {
  return new Promise((resolve, reject) => {
    const result = spawn(...args);
    result.stdout.pipe(process.stdout); // 成功后将子进程的标准输出通过管道传输到当前进程的标准输出
    result.stderr.pipe(process.stderr); // 同样可以将子进程的标准错误也重定向到当前进程的标准错误
    result.on("close", () => {
      resolve(); // 结束之后再resolve出去
    });
  });
}

module.exports = {
  handleSpawn,
};

// index.js

program
  .command("create <project-name> [repositoryUrl]")
  .description("create a new project by cli-create")
  .action((name, second) => {
    console.log("start download project template~");
    download(
      "direct:https://github.com/wczy-ao/project-vue2-template.git#main",
      name,
      {
        clone: true,
      },
      async function (err) {
        if (!err) {
          console.log(
            "code repository dowload success, start download node_modules by pnpm, please wait"
          );
          if (pnpmVersion - Number(nodeVersion) > 0) {
            console.log(
              "pnpm need nodejs version at least V18.12,please upgrade node version"
            );
          } else {
            // 开始安装 node_modules
            await handleSpawn("pnpm", ["i"], {
              cwd: `./${name}`,
            });

            await handleSpawn("pnpm", ["run", "start"], {
              cwd: `./${name}`,
            });
          }
        }
      }
    );
  });

image-20250124192036159

至此,一个简单的脚手架就开发完成了。但在pnpm i时候会有很长的间隔,可以针对这些进行一定的优化

优化

代码优化

上面代码回调过多,这里整改一下

// index.js
#!/usr/bin/env node
const { Command } = require("commander");

const { createCommander } = require("./lib/createCommanders");
const program = new Command();

program.version("cli-create@" + require("./package.json").version); // 定义脚手架版本
program.option("-n, --name <value>", "this is cli-create's name", "cli-create");

createCommander(program);

program.parse(process.argv);
// lib/handleLoading.js
const loadResult = () => {
  return new Promise((resolve) => {
    import("ora").then((res) => {
      const spinner = res.default("loading").start();
      spinner.color = "yellow";
      spinner.text = "Loading...";
      resolve(spinner);
    });
  });
};

module.exports = {
  loadResult,
};
// lib/handleChalk.js
const chalkResult = () => {
  return new Promise((resolve) => {
    import("chalk").then((res) => {
      resolve(res.default);
    });
  });
};

module.exports = {
  chalkResult,
};
// handleSpawn.js
const { spawn } = require("child_process");
function handleSpawn(...args) {
  return new Promise((resolve, reject) => {
    const result = spawn(...args);
    result.stdout.pipe(process.stdout);
    result.stderr.pipe(process.stderr);
    result.on("close", () => {
      resolve();
    });
  });
}

module.exports = {
  handleSpawn,
};

// createCommanders.js

const download = require("download-git-repo");
const { loadResult } = require("./handleLoading");
const { chalkResult } = require("./handleChalk");
const { handleSpawn } = require("./handleSpawn");

const createCommander = (program) => {
  const pnpmVersion = 18.12;
  const nodeVersionArr = process.version.substring(1).split(".");
  const nodeVersion = nodeVersionArr[0] + "." + nodeVersionArr[1];

  program
    .command("create <project-name> [repositoryUrl]")
    .description("create a new project by cli-create")
    .action(async (name, second) => {
      const chalk = await chalkResult();
      const spinner = await loadResult();
      download(
        "direct:https://github.com/wczy-ao/project-vue2-template.git#main",
        name,
        {
          clone: true,
        },
        async function (err) {
          if (err) {
            spinner.fail("download error");
          }
          if (!err) {
            console.log(
              "code repository dowload success, start download node_modules by pnpm, please wait"
            );
            if (pnpmVersion - Number(nodeVersion) > 0) {
              console.log(
                "pnpm need nodejs version at least V18.12,please upgrade node version"
              );
            } else {
              // 开始安装 node_modules
              await handleSpawn("pnpm", ["i"], {
                cwd: `./${name}`,
              });

              spinner.succeed("download success");

              // await handleSpawn("pnpm", ["run", "start"], {
              //   cwd: `./${name}`,
              // });
            }
          }
        }
      );
    });
};

module.exports = {
  createCommander,
};

依赖下载增加 loading

ora一个用于终端环境的美观、简洁的加载动画和进度反馈工具。开发者经常使用 ora 来增强命令行应用的用户体验,比如显示加载状态、任务完成情况或是错误提示等。

pnpm add ora

ora默认是esmodule,所以这里需要动态导入一下

const download = require("download-git-repo");
const { loadResult } = require("./handleLoading");
const { handleSpawn } = require("./handleSpawn");

const createCommander = (program) => {
  const pnpmVersion = 18.12;
  const nodeVersionArr = process.version.substring(1).split(".");
  const nodeVersion = nodeVersionArr[0] + "." + nodeVersionArr[1];

  program
    .command("create <project-name> [repositoryUrl]")
    .description("create a new project by cli-create")
    .action(async (name, second) => {
      const spinner = await loadResult();
      download(
        "direct:https://github.com/wczy-ao/project-vue2-template.git#main",
        name,
        {
          clone: true,
        },
        async function (err) {
          if (err) {
            spinner.fail("download error");
          }
          if (!err) {
            console.log(
              "code repository dowload success, start download node_modules by pnpm, please wait"
            )

            if (pnpmVersion - Number(nodeVersion) > 0) {
              spinner.fail("pnpm need nodejs version at least V18.12,please upgrade node version");
            } else {
              // 开始安装 node_modules
              await handleSpawn("pnpm", ["i"], {
                cwd: `./${name}`,
              });

              spinner.succeed("download success");

              // await handleSpawn("pnpm", ["run", "start"], {
              //   cwd: `./${name}`,
              // });
            }
          }
        }
      );
    });
};

module.exports = {
  createCommander,
};

image-20250126105653441.png

打印提供状态颜色

我们针对不同的信息也需要用颜色标注出来,比如错误红色,成功绿色等

chalk 允许你在终端(命令行界面)中使用颜色和样式来输出文本。

pnpm add chalk
const download = require("download-git-repo");
const { loadResult } = require("./handleLoading");
const { chalkResult } = require("./handleChalk");
const { handleSpawn } = require("./handleSpawn");

const createCommander = (program) => {
  const pnpmVersion = 18.12;
  const nodeVersionArr = process.version.substring(1).split(".");
  const nodeVersion = nodeVersionArr[0] + "." + nodeVersionArr[1];

  program
    .command("create <project-name> [repositoryUrl]")
    .description("create a new project by cli-create")
    .action(async (name, second) => {
      const chalk = await chalkResult();
      const spinner = await loadResult();
      download(
        "direct:https://github.com/wczy-ao/project-vue2-template.git#main",
        name,
        {
          clone: true,
        },
        async function (err) {
          if (err) {
            spinner.fail("download error");
          }
          if (!err) {
            console.log(chalk.green(
              "code repository dowload success, start download node_modules by pnpm, please wait"
            ))

            if (pnpmVersion - Number(nodeVersion) > 0) {
              spinner.fail("pnpm need nodejs version at least V18.12,please upgrade node version");
            } else {
              // 开始安装 node_modules
              await handleSpawn("pnpm", ["i"], {
                cwd: `./${name}`,
              });

              spinner.succeed("download success");

              // await handleSpawn("pnpm", ["run", "start"], {
              //   cwd: `./${name}`,
              // });
            }
          }
        }
      );
    });
};

module.exports = {
  createCommander,
};

image-20250126135135082.png

增加代码仓库地址选项

上面的代码模版是我的,对于开发者来说,肯定是不试用的,那么我这里提供第二选项,传入相应的代码仓库地址,同样可以下载下来。

cli-create create myApp 'https://github@1sss:xxxxx#main.git'
  program
    .command("create <project-name> [repositoryUrl]")
    .description("create a new project by cli-create")
    .action(async (name, projectUrl) => {
      const chalk = await chalkResult();
      const spinner = await loadResult();
      const gitUrl =
        projectUrl ||
        "https://github.com/wczy-ao/project-vue2-template.git#main";
      download(
        `direct:${gitUrl}`,
        name,
        {
          clone: true,
        },
        async function (err) {
          
        }
      );
    });

image-20250126140200369

最后

如果此文对你有帮助欢迎大家点赞收藏加关注!如有不对之处,望各位大佬不吝赐教。 三连加关注,更新不迷路!