Skip to content

自定义命令和入口配置

首先,执行 npm init -y 生成一个 package.json 文件

接着在创建 lib/index.js

javascript
console.log("my cli code exec~");

我们希望在命令行输入 whycli 就可以执行 lib/index.js 这个文件

那么就需要找到 package.json 增加以下配置:

json
{
  ...
  "bin": {
    "whycli": "./lib/index.js"
  },
}

紧接着在 lib/index.js 头部添加

javascript
#!/usr/bin/env node

表示去环境变量找 node,让 node 运行整个文件

目前执行 whycli 会报这个命令找不到,因为我们还没有将 whycli 添加到环境变量

如果我们通过 npm publish 发布到了 npm,后面使用的时候通过 npm install 安装就会把 whycli 添加到环境变量

但是我们目前在本地,还没有发布,那么就需要执行

json
npm link

执行完之后,就会将 whycli 添加到环境变量,whycli 和./lib/index.js 会建立一个软连接

到这里,就可以在命令行执行 whycli,它就会去执行 lib/index.js 里面的代码

查看当前版本号

如果我们想要通过执行 whycli --version 或 whycli -v 查看 package.json 中的版本号,那么就需要借助一个工具叫 commander

首先,安装这个库

json
npm install commander

lib/index.js

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

// 处理--version 的操作
const version = require("../package.json").version;
program.version(version, "-v --version");

// 让program解析process.argv参数
program.parse(process.argv);

增加 options 选项和封装

lib/core/help-options.js

javascript
const { program } = require("commander");

function helpOptions() {
  // 1.处理--version的操作
  const version = require("../../package.json").version;
  program.version(version, "-v --version");

  // 2.增强其他的options的操作
  program.option("-w --why", "a why cli program~");
  // <dest>表示传进去的参数
  program.option(
    "-d --dest <dest>",
    "a destination folder, 例如: -d src/components"
  );

  program.on("--help", () => {
    console.log("");
    console.log("others:");
    console.log("  xxx");
    console.log("  yyy");
  });
}

module.exports = helpOptions;

想要获取这个额外的参数

javascript
console.log(pagram.opts().dest);

lib/index.js

javascript
...
const helpOptions = require('./core/help-options')

// 1.配置所有的options
helpOptions()

此时,我们在命令行敲 whycli --help 就可以看到以下信息

image-20240804220053210

create 命令创建项目的过程

我们现在的脚手架实现的功能非常有限,仅仅只是能查看版本号和 help 的一些信息,这样并没有什么意义

我们希望通过在命令行执行 whycli create xxx,就可以创建一个叫 xxx 的项目,但是它是从哪里来的呢?

我们可以将一个模板放到 github,然后执行 whycli create xxx 的时候就会去克隆这个模板,具体实现如下:

lib/index.js

javascript
...
const { createProjectAction } = require('./core/actions')

// 2.增加具体的一些功能操作
program
  .command("create <project> [...others]") // others表示可以跟其他一些参数
  .description("create vue project into a folder, 比如: whycli create airbnb")
  .action(createProjectAction)

lib/core/actions.js

这里需要安装 download-git-repo 这个库

json
npm install download-git-repo
javascript
const { promisify } = require("util");
const download = promisify(require("download-git-repo"));
const { VUE_REPO } = require("../config/repo");

async function createProjectAction(project) {
  try {
    // 1.从编写的项目模板中clone下来项目
    await download(VUE_REPO, project, { clone: true });
  } catch (error) {
    console.log("github连接失败, 请稍后重试");
  }
}

util 是 node 内置的一个工具库,里面的 promisify 方法可以支持使用 Promise 语法

lib/config/repo.js

javascript
const VUE_REPO = "direct:https://github.com/coderwhy/vue3_template.git#main"; // 指定从main分支克隆

module.exports = {
  VUE_REPO,
};

脚手架执行安装和运行命令

很多脚手架在执行 create 命令创建完项目之后,都会给提示

lib/core/actions.js

javascript
...

async function createProjectAction(project) {
  try {
    // 2.很多的脚手架, 都是在这里给予提示
    console.log(`cd ${project}`)
    console.log(`npm install`)
    console.log(`npm run dev`)
  } catch (error) {
    console.log("github连接失败, 请稍后重试")
  }
}

如果我们想要在脚手架内部执行安装和运行命令又要怎么实现呢?

lib/core/actions.js

javascript
const execCommand = require("../utils/exec-command");

async function createProjectAction(project) {
  try {
    // 1.从编写的项目模板中clone下来项目
    await download(VUE_REPO, project, { clone: true });

    // 2.很多的脚手架, 都是在这里给予提示
    // console.log(`cd ${project}`)
    // console.log(`npm install`)
    // console.log(`npm run dev`)

    // 3.帮助执行npm install
    console.log(process.platform); // window32的是npm.cmd
    const commandName = process.platform === "win32" ? "npm.cmd" : "npm";
    await execCommand(commandName, ["install"], { cwd: `./${project}` });

    // 4.帮助执行npm run dev
    await execCommand(commandName, ["run", "dev"], { cwd: `./${project}` });
  } catch (error) {
    console.log("github连接失败, 请稍后重试");
  }
}

lib/utils/exec-command.js

需要借助 node 的子进程

javascript
const { spawn } = require("child_process");

function execCommand(...args) {
  return new Promise((resolve) => {
    // npm install/npm run dev
    // 1.开启子进程执行命令
    const childProcess = spawn(...args);

    // 2.获取子进程的输出和错误信息
    childProcess.stdout.pipe(process.stdout);
    childProcess.stderr.pipe(process.stderr);

    // 3.监听子进程执行结束, 关闭掉
    childProcess.on("close", () => {
      resolve();
    });
  });
}

module.exports = execCommand;

脚手架创建添加组件的过程

我们希望执行 whycli addcpn xxx,就可以创建一个叫 xxx.vue 的组件到指定目录中

lib/index.js

javascript
const { addComponentAction } = require("./core/actions");

program
  .command("addcpn <cpnname> [...others]")
  .description(
    "add vue component into a folder, 比如: whycli addcpn NavBar -d src/components"
  )
  .action(addComponentAction);

lib/core/actions.js

javascript
...
const compileEjs = require('../utils/compile-ejs')
const writeFile = require('../utils/write-file')

async function addComponentAction(cpnname) {
  // 1.创建一个组件: 编写组件的模板, 根据内容给模板中填充数据
  const result = await compileEjs("component.vue.ejs", {
    name: cpnname,
    lowername: cpnname.toLowerCase()
  })

  // 2.将result写到到对应的文件夹中
  const dest = program.opts().dest || "src/components"
  await writeFile(`${dest}/${cpnname}.vue`, result)
  console.log("创建组件成功:", cpnname + ".vue")
}

module.exports = {
  addComponentAction
}

lib/utils/compile-ejs.js

需要安装 ejs 这个库

json
npm install ejs
javascript
const path = require("path");
const ejs = require("ejs");

function compileEjs(tempName, data) {
  return new Promise((resolve, reject) => {
    // 1.获取当前模板的路径
    const tempPath = `../template/${tempName}`;
    const absolutePath = path.resolve(__dirname, tempPath);

    // 2.使用ejs引擎编译模板
    ejs.renderFile(absolutePath, data, (err, result) => {
      if (err) {
        console.log("编译模板失败:", err);
        reject(err);
        return;
      }

      resolve(result);
    });
  });
}

module.exports = compileEjs;

lib/utils/write-file.js

javascript
const fs = require("fs");

function writeFile(path, content) {
  return fs.promises.writeFile(path, content);
}

module.exports = writeFile;

组件模板

template/component.vue.ejs

Vscode 搜索 EJS language support,安装这个插件,就会有提示

ejs
<template>
  <div class="<%= lowername %>">
    <h2><%= name %>: {{ message }}</h2>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const message = ref("哈哈哈")
</script>

<style>
.<%= lowername %> {
  color: red;
}
</style>

至此,我们执行 whycli addcpn TabBar,就会在 src/components 目录下创建一个 TabBar 组件

html
<template>
  <div class="tarbar">
    <h2>TarBar: {{ message }}</h2>
  </div>
</template>

<script setup>
  import { ref } from "vue";

  const message = ref("哈哈哈");
</script>

<style>
  .tarbar {
    color: red;
  }
</style>

当然如果我们也可以指定路径,比如创建一个 Banner 组件到 src/views/home/c-cpns