首先前端腳手架,也就是我們?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));
}
}


