首先前端脚手架,也就是我们日常使用的例如create-react-app或者vite生成一个前端应用,指令式的选择一些内容,然后就可以帮我们创建一个项目。
但是这些脚手架创建的都比较基础。 没有集成路由或者状态管理等等, 我们可以自己给自己写一个脚手架,避免重复工作。
1. 初始化项目
mkdir my-cli
cd my-cli
npm init
-
当前我们的项目目录是这样
image.png - 接下来我们在index.js中编写一些测试代码,测试一下代码是否能运行。
#!/usr/bin/env node
console.log('hello world');
- 在终端中运行 node 程序,输入 node 命令
node index.js
可以正确输出 hello world ,代码顶部的 #!/usr/bin/env node 是告诉终端,这个文件要使用 node 去执行
- 一般cli脚手架都有一个特定的命令,例如create-react-app的
npx create-react-app my-app
, 所以我们也可以给自己的脚手架命名。
在 package.json 文件中添加一个字段 bin,并且声明一个命令关键字和对应执行的文件:
"bin": {
"my-cli": "index.js"
}
- 正如我上面所说,例如create-react-app当我们执行它时,我们需要全局下载它的依赖,才能直接去使用它的脚手架创建项目。例如
npm install -g create-react-app
。但是我们还没有发布到npm,如何进行测试呢?我们可以在当前项目根目录使用npm link
。此时再去执行my-cli
就可以看到我们测试代码了。
npm link
my-cli
开始编写脚本
编写代码之前需要补充说明一下,因为我们其中用了一个库叫Inquirer.js,但是查看文档最新的9.x版本只支持ESM模块,所以我们全篇代码都是用的ESM模块去进行开发。
- 首先脚手架需要执行一些复杂的命令,并且在命令行也有交互,所以我们可以使用commander这个库,来帮助我们完成这种事情。先看最终结果。我们可以直接运行
my-cli
查看帮助,也可以查看版本号,并且初始化一个新项目。
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);
- 上文也提到过Inquirer.js这个库,那这个库是干什么的呢?大家估计都见过在初始化项目时,让你选择这选择那,然后最终通过你的选项最终生成你想要的项目。这个库就是提供命令行中问答操作。效果如下
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)
});
}
- 添加命令行动画和颜色效果(chalk & ora),具体使用在下面生成模版代码中体现。
npm install chalk //改变文字颜色
npm install ora //添加loading动画效果
- 命令行交互已经准备ok了,现在就剩生成模板代码了。
网上都在使用download-git-repo这个库,但是我一直无法使用,发现了两个问题。
- 第一个是每次我运行脚本时都会出现这个警告:[DEP0040] DeprecationWarning: The punycode module is deprecated. Please use a userland alternative instead.
(Use node --trace-deprecation ... to show where the warning was created)。虽然不影响功能,但很丑。。不知道是不是因为我的npm版本原因。 - 第二个是,最终下载模板时不是报404 http error,就是报错'git clone' failed with status 128。
于是查阅了下文档,这个库本身也是使用了child_process进行了二次封装,所以我直接使用了这个,发现是ok的。child_process是node.js内置的。
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我是不想把错误或者过程输出在控制台,对于错误我也想重新处理一下。 如果你想把这个过程输出在控制台 可以使用 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));
}
}