簡(jiǎn)單實(shí)現(xiàn)一個(gè)React前端腳手架

首先前端腳手架,也就是我們?nèi)粘J褂玫睦鏲reate-react-app或者vite生成一個(gè)前端應(yīng)用,指令式的選擇一些內(nèi)容,然后就可以幫我們創(chuàng)建一個(gè)項(xiàng)目。

但是這些腳手架創(chuàng)建的都比較基礎(chǔ)。 沒(méi)有集成路由或者狀態(tài)管理等等, 我們可以自己給自己寫(xiě)一個(gè)腳手架,避免重復(fù)工作。

1. 初始化項(xiàng)目

mkdir my-cli
cd my-cli
npm init
  • 當(dāng)前我們的項(xiàng)目目錄是這樣


    image.png
  • 接下來(lái)我們?cè)趇ndex.js中編寫(xiě)一些測(cè)試代碼,測(cè)試一下代碼是否能運(yùn)行。
#!/usr/bin/env node
console.log('hello world');
  • 在終端中運(yùn)行 node 程序,輸入 node 命令
node index.js

可以正確輸出 hello world ,代碼頂部的 #!/usr/bin/env node 是告訴終端,這個(gè)文件要使用 node 去執(zhí)行

  • 一般cli腳手架都有一個(gè)特定的命令,例如create-react-app的npx create-react-app my-app, 所以我們也可以給自己的腳手架命名。
    在 package.json 文件中添加一個(gè)字段 bin,并且聲明一個(gè)命令關(guān)鍵字和對(duì)應(yīng)執(zhí)行的文件:
"bin": {
    "my-cli": "index.js"
}
  • 正如我上面所說(shuō),例如create-react-app當(dāng)我們執(zhí)行它時(shí),我們需要全局下載它的依賴,才能直接去使用它的腳手架創(chuàng)建項(xiàng)目。例如npm install -g create-react-app。但是我們還沒(méi)有發(fā)布到npm,如何進(jìn)行測(cè)試呢?我們可以在當(dāng)前項(xiàng)目根目錄使用npm link。此時(shí)再去執(zhí)行my-cli就可以看到我們測(cè)試代碼了。
npm link
my-cli

開(kāi)始編寫(xiě)腳本

編寫(xiě)代碼之前需要補(bǔ)充說(shuō)明一下,因?yàn)槲覀兤渲杏昧艘粋€(gè)庫(kù)叫Inquirer.js,但是查看文檔最新的9.x版本只支持ESM模塊,所以我們?nèi)a都是用的ESM模塊去進(jìn)行開(kāi)發(fā)。

  • 首先腳手架需要執(zhí)行一些復(fù)雜的命令,并且在命令行也有交互,所以我們可以使用commander這個(gè)庫(kù),來(lái)幫助我們完成這種事情。先看最終結(jié)果。我們可以直接運(yùn)行my-cli查看幫助,也可以查看版本號(hào),并且初始化一個(gè)新項(xiàng)目。
    image.png
npm i commander --save

import { Command } from "commander";
import fs from "fs";
import path from "path";

//ESM不支持__dirname,所以我們用到了process.cwd()
function getPackageJSON() {
  const packageJsonPath = path.join(process.cwd(), "package.json");
  const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
  return JSON.parse(packageJsonContent);
}

const Mypackage = getPackageJSON();
const program = new Command();
program.version(Mypackage.version);
program
  .command("init")
  .description("Create a new project")
  .action(() => {
    initProject();
  });
program.parse(process.argv);
  • 上文也提到過(guò)Inquirer.js這個(gè)庫(kù),那這個(gè)庫(kù)是干什么的呢?大家估計(jì)都見(jiàn)過(guò)在初始化項(xiàng)目時(shí),讓你選擇這選擇那,然后最終通過(guò)你的選項(xiàng)最終生成你想要的項(xiàng)目。這個(gè)庫(kù)就是提供命令行中問(wèn)答操作。效果如下
    image.png
npm install --save inquirer
import inquirer from "inquirer";

function initProject() {
  inquirer
    .prompt([
      {
        type: "input",
        message: "Please enter the name of the project:",
        name: "name",
        default: "my-app",
      },
      {
        type: "list",
        message: "Please select the status of the emoji:",
        name: "status",
        choices: ["??", "??", "??"],
      },
    ])
    .then(async (answers) => {
         console.log("answers",answers)
    });
}
  • 添加命令行動(dòng)畫(huà)和顏色效果(chalk & ora),具體使用在下面生成模版代碼中體現(xiàn)。
npm install chalk  //改變文字顏色
npm install ora     //添加loading動(dòng)畫(huà)效果
  • 命令行交互已經(jīng)準(zhǔn)備ok了,現(xiàn)在就剩生成模板代碼了。

網(wǎng)上都在使用download-git-repo這個(gè)庫(kù),但是我一直無(wú)法使用,發(fā)現(xiàn)了兩個(gè)問(wèn)題。

  • 第一個(gè)是每次我運(yùn)行腳本時(shí)都會(huì)出現(xiàn)這個(gè)警告:[DEP0040] DeprecationWarning: The punycode module is deprecated. Please use a userland alternative instead.
    (Use node --trace-deprecation ... to show where the warning was created)。雖然不影響功能,但很丑。。不知道是不是因?yàn)槲业膎pm版本原因。
  • 第二個(gè)是,最終下載模板時(shí)不是報(bào)404 http error,就是報(bào)錯(cuò)'git clone' failed with status 128。
    于是查閱了下文檔,這個(gè)庫(kù)本身也是使用了child_process進(jìn)行了二次封裝,所以我直接使用了這個(gè),發(fā)現(xiàn)是ok的。child_process是node.js內(nèi)置的。
     if (answers.status === "??") {
        console.log(chalk.yellow("don't be angry, be happy!"));
        return;
      }
      if (answers.status === "??") {
        console.log(chalk.yellow("don't cry, be happy!"));
        return;
      }

      console.log(chalk.yellowBright("Please wait a moment..."));
      const localPath = path.join(process.cwd(), answers.name);
      const remote = `git 地址`;
      const spinner = ora("download template......").start();
      const result = await spawnSync("git", ["clone", remote, localPath], {
        stdio: "pipe",   //pipe我是不想把錯(cuò)誤或者過(guò)程輸出在控制臺(tái),對(duì)于錯(cuò)誤我也想重新處理一下。  如果你想把這個(gè)過(guò)程輸出在控制臺(tái) 可以使用 inherit
      });

      if (result.status !== 0) {
        spinner.fail(
          chalk.red("download template failed:" + result.stderr.toString())
        );
        return;
      }
      updatePackageJsonName(localPath, answers.name);
      spinner.succeed(chalk.green("download template success"));
  • 修改package.json文件的Name和用戶輸入的name一致
function updatePackageJsonName(localPath, newName) {
  const packageJsonPath = path.join(localPath, "package.json");
  try {
    const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
    packageJson.name = newName;
    fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
  } catch (err) {
    console.error(chalk.red("update package.json failed:" + err));
  }
}

3. 完整代碼

#!/usr/bin/env node
import inquirer from "inquirer";
import { Command } from "commander";
import fs from "fs";
import path from "path";
import ora from "ora";
import chalk from "chalk";
import { spawnSync } from "child_process";

function getPackageJSON() {
  const packageJsonPath = path.join(process.cwd(), "package.json");
  const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
  return JSON.parse(packageJsonContent);
}

const Mypackage = getPackageJSON();
const program = new Command();
program.version(Mypackage.version);
program
  .command("init")
  .description("Create a new project")
  .action(() => {
    initProject();
  });
program.parse(process.argv);

function initProject() {
  inquirer
    .prompt([
      {
        type: "input",
        message: "Please enter the name of the project:",
        name: "name",
        default: "my-app",
      },
      {
        type: "list",
        message: "Please select the status of the emoji:",
        name: "status",
        choices: ["??", "??", "??"],
      },
    ])
    .then(async (answers) => {
      if (answers.status === "??") {
        console.log(chalk.yellow("don't be angry, be happy!"));
        return;
      }
      if (answers.status === "??") {
        console.log(chalk.yellow("don't cry, be happy!"));
        return;
      }

      console.log(chalk.yellowBright("Please wait a moment..."));
      const localPath = path.join(process.cwd(), answers.name);
      const remote = `https://github.com/li-yu-tfs/vite-react-template.git`;
      const spinner = ora("download template......").start();
      const result = await spawnSync("git", ["clone", remote, localPath], {
        stdio: "pipe",
      });

      if (result.status !== 0) {
        spinner.fail(
          chalk.red("download template failed:" + result.stderr.toString())
        );
        return;
      }
      spinner.succeed(chalk.green("download template success"));
      updatePackageJsonName(localPath, answers.name);
      spinner.succeed(chalk.green("update package.json success"));
    });
}

function updatePackageJsonName(localPath, newName) {
  const packageJsonPath = path.join(localPath, "package.json");
  try {
    const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
    packageJson.name = newName;
    fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
  } catch (err) {
    console.error(chalk.red("update package.json failed:" + err));
  }
}


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容