Nodejs自定义脚手架

一、创建自定义全局指令

npm link是Node.js开发中一个非常实用的命令,它允许你在本地项目和本地npm包之间建立连接,方便在开发过程中进行模块调试,而无需反复发布到npm仓库。

简单来说,npm link通过创建符号链接(类似于快捷方式),让你正在开发的npm包能够被其他本地项目实时引用。当你对包的代码进行修改后,引用它的项目可以立即看到效果,无需重新安装。

1.1 主要使用场景

  • 开发自己的Vue/React组件库、工具库等npm包。

  • 维护内部或私有的npm包。

  • 需要频繁修改某个依赖包,并立即在项目中测试其效果。

1.2 基本用法

假设有两个项目:

  • npm包项目:my-awesome-lib

  • 使用包的项目:my-project

第一步:创建全局链接。

  1. 在终端中进入你的npm包项目目录。

    shell 复制代码
    cd path/to/my-awesome-lib
  2. 执行npm link命令。

    shell 复制代码
    npm link

    这个命令会在全局的node_modules目录下创建一个指向my-awesome-lib项目目录的符号链接。

第二步:在项目中链接本地包。

  1. 进入你想要使用该包的项目目录。

    shell 复制代码
    cd path/to/my-project
  2. 执行npm link <包名>命令。这里的<包名>是你的npm包package.json文件中name字段的值,而不是文件夹名称。

    shell 复制代码
    npm link my-awesome-lib

    这个命令会在my-projectnode_modules目录下创建一个符号链接,指向全局的那个链接,最终指向你的my-awesome-lib源码目录。

现在,你就可以在my-project中像使用普通依赖一样importrequire这个包了。你对my-awesome-lib源码的任何修改都会立即反映在my-project中。

1.3 进阶用法

如果你的npm包和使用它的项目在同一个父目录下,可以使用相对路径一步完成链接。

shell 复制代码
# 在my-project目录下执行。
cd path/to/my-project
npm link ../my-awesome-lib

1.4 特殊用法:链接命令行工具

如果你正在开发一个命令行工具(CLI),需要在package.json中配置bin字段。执行npm link后,你就可以在全局环境中直接使用该命令。

package.json 示例:

shell 复制代码
{
  "name": "my-cli-tool",
  "version": "1.0.0",
  "bin": {
    "my-cli-tool": "./cli.js"
  }
}

my-cli-tool项目目录下执行npm link后,你就可以在任何地方通过my-cli-tool命令来运行它。

1.5 如何解除链接

当你完成开发或调试后,需要解除链接。

  1. 解除项目中的链接:在使用包的项目目录下执行。

    shell 复制代码
    cd path/to/my-project
    npm unlink my-awesome-lib
  2. 解除全局链接:在npm包项目目录下执行。

    shell 复制代码
    cd path/to/my-awesome-lib
    npm unlink

通常建议按照先项目后全局的顺序来解除。

1.6 Shebang

js 复制代码
#! /usr/bin/env node

// ...

这行代码被称为Shebang(或Hashbang),它的作用是让JavaScript文件能够像一个独立的可执行程序一样,直接在命令行中运行,而无需每次都手动输入node命令。

这是开发Node.js命令行工具(CLI)或脚手架时的标准配置。

1.6.1 逐段解析

#!/usr/bin/env node拆解成三个部分来理解:

  1. #! (Shebang)

    这是一个特殊的标记,告诉操作系统(特别是Unix/Linux/macOS系统):"不要用Shell来执行这个脚本,而是用后面指定的解释器来运行它。"

  2. /usr/bin/env (环境工具)

    这是一个标准的系统命令,它的作用是在环境变量PATH中查找指定的程序。

    • 为什么用它?

      因为不同用户的电脑上,Node.js的安装路径可能不同(例如/usr/bin/node/usr/local/bin/node等)。如果直接写死路径(如#!/usr/bin/node),一旦路径不对脚本就会报错。

    • 好处。

      使用env可以让系统自动去环境变量里找node在哪里,从而实现了跨平台兼容

  3. node (解释器)

    这是env命令要查找的目标程序。意思是告诉系统:"请找到Node.js解释器,并用它来执行当前这个文件。"

1.6.2 它解决了什么问题?

如果没有这行代码,你运行一个JS文件必须这样写:

shell 复制代码
node index.js

加上这行代码并赋予执行权限后,你可以直接运行文件:

shell 复制代码
./index.js

1.6.3 如何让它生效?

仅仅在文件第一行写上它是不够的,你还需要给文件添加可执行权限

完整步骤如下:

  1. 在文件第一行添加代码。

    js 复制代码
    #! /usr/bin/env node
    console.log("我是一个可执行的脚本!");
  2. 赋予执行权限(在终端执行)。

    js 复制代码
    chmod +x index.js
  3. 直接运行。

    js 复制代码
    ./index.js

1.6.4 常见应用场景

  • 开发脚手架工具 :比如Vue CLI(vue create ...)或Create React App,它们的入口文件第一行都有这个标记。

  • npm包的bin命令 :当你在package.json中配置bin字段时,对应的文件必须包含这行代码,这样用户安装你的包后才能通过命令直接调用。

1.6.5 注意事项

  • 必须放在第一行:它必须是文件的绝对第一行,前面不能有空行或注释,否则无效。

  • Windows系统:原生Windows CMD或PowerShell不直接支持Shebang,但在Git Bash、WSL(Windows Subsystem for Linux)或通过npm全局安装时,它依然能正常工作。

二、处理命令参数

当我们按照第一章第1.4节中的方法创建好全局命令后,

2.1 process.argv命令行参数

js 复制代码
#! /usr/bin/env node
console.log(process.argv);

执行命令:

shell 复制代码
./index.js --help -p

输出结果:

shell 复制代码
[
  'C:\\Users\\CyrusCJA\\dev_kits\\nvm4w\\nodejs\\node.exe',
  'C:\\Users\\CyrusCJA\\Desktop\\workspace\\node_demo\\index.js',
  '--help',
  '-p'
]
  • 第一个参数表示的是使用了哪一个脚本来执行代码的。

  • 第二个参数表示的是执行的代码文件所在的位置。

  • 后面的参数则是执行命令时添加的参数值。

所以我们可以通过如下代码获取命令参数:

js 复制代码
if (process.argv[2] === '--help') {
  // ...
 }

参数较多时,这样写很麻烦,我们可以使用commander命令参数处理工具依赖包。

2.2 commander命令行指令设置

shell 复制代码
npm install commander
js 复制代码
#! /usr/bin/env node

import { program } from "commander";

program.option(
  "-f --framework <framework>",
  "描述(尖括号表示参数后面必须有值)",
  "默认值",
);

program.option("-p --port", "描述(尖括号表示参数后面必须有值)", "默认值");

/*
 [other...]是命令参数,表示可以有多个参数,参数名称可以自定义,参数名称可以省略。
 去掉它则只能有一个参数<project-name>。

 .alias(...)是为命令create起了一个别名,使用时既可以 ... create ...,也可以... crt ...。

 .description(...)是为命令create添加一个描述,使用时可以查看命令的描述。

 .action(...)是为命令create添加一个操作函数,使用时会执行该函数。
 该函数的参数options是命令行参数,参数args是命令行参数的数组。
*/
program
  .command("create <project-name> [other...]")
  .alias("crt")
  .description("创建项目")
  .action((options, args) => {
    // 命令的执行逻辑代码。
    console.log(options);
    console.log(args);
  });

program
  .command("deploy <environment>") // 1. 定义一个'deploy'命令,需要一个环境参数。
  .description("部署到指定环境")
  .option("-b, --branch <name>", "指定要部署的分支") // 2. 为'deploy'命令添加一个'--branch'选项。
  .option("-f, --force", "强制部署") // 3. 为'deploy'命令添加一个'--force'选项。
  .action((environment, options) => {
    // 4. 在action回调中,可以同时获取命令参数和选项。
    console.log(`正在部署到: ${environment}`);
    console.log(`options参数值: ${options}。具体内容如下:`);
    console.log(options);
    if (options.branch) {
      console.log(`使用分支: ${options.branch}`);
    }
    if (options.force) {
      console.log("强制部署模式开启");
    }
  });

// .parse(...)是解析命令行参数,必须放在最后。
program.parse(process.argv);

/*
 测试用例:

 ./index.js -f
 ./index.js -f xxx
 ./index.js -f xxx abc
 ./index.js -p
 ./index.js -p xxx
 ./index.js create
 ./index.js crt xxx
 ./index.js crt xxx abc def
 ./index.js deploy xxx
 ./index.js deploy xxx --branch xxx
 ./index.js deploy xxx --branch xxx --force
*/

2.3 inquirer命令行问答交互

shell 复制代码
npm install inquirer
js 复制代码
#! /usr/bin/env node

import { program } from "commander";
import inquirer from "inquirer";

program
  .command("create <project-name> [other...]")
  .alias("crt")
  .description("创建项目")
  .action(() => {
    // 命令的执行逻辑代码。

    inquirer
      .prompt([
        // 对象中的属性如下图所示。
        {
          // 问题类型:input,表示用户需要输入一个值。
          type: "input",
          // 问题名称,用于在answers对象中获取用户输入的值。
          name: "projectName",
          // 问题默认值,用户不输入时使用。
          default: "my-project",
          // 问题提示,用户输入时显示。
          message: "请输入项目名称",
        },
        {
          // 即可通过上下键切换选项并使用Enter键选择。也可输入相应选项号并按Enter键选择。
          type: "rawlist",
          name: "framework",
          message: "请选择要使用的框架:",
          choices: ["React", "Vue", "Angular"],
          // 此时默认值无效。
          // default: "VueVue",
        },
        {
          // 只能通过上下键切换选项并使用Enter键选择。
          type: "select",
          name: "like",
          message: "请选择你喜欢的明星:",
          choices: ["张三", "李四", "王五"],
          // 此时默认值无效。
          // default: "VueVue",
        },
      ])
      .then((answers) => {
        console.log(answers);
        console.log(answers.projectName);
      });
  });

program.parse(process.argv);

2.4 download-git-repo下载远程仓库模板代码

shell 复制代码
npm install download-git-repo
js 复制代码
#! /usr/bin/env node

import { program } from "commander";
import inquirer from "inquirer";
import downloadGitRepo from "download-git-repo";

program
  .command("create <project-name> [other...]")
  .alias("crt")
  .description("创建项目")
  .action(() => {
    // 命令的执行逻辑代码。

    inquirer
      .prompt([
        {
          type: "rawlist",
          name: "framework",
          message: "请选择要使用的框架:",
          choices: ["React", "Vue", "Angular"],
        },
      ])
      .then((answers) => {
        switch (answers.framework) {
          case "React":
            /*
             参数:
                 第一个:仓库地址,第二个:下载到哪个目录,
                 第三个:控制下载的行为,第四个:回调函数。

             第三个参数对象中的属性:
                 1、clone(类型:Boolean,默认值:false):是否使用git clone模式。
                     1.1 true: 使用git clone(适合私有仓库或需要完整git历史)。
                     1.2 false: 使用git archive或下载zip包(速度更快,不包含.git目录)。
                 2、cache(类型:Boolean,默认值:true):是否启用缓存。
                     2.1 true: 如果本地已有该仓库的缓存,则直接使用,不再重新下载。
                     2.2 false: 强制重新下载。
                 3、force(类型:Boolean,默认值:false):是否强制覆盖目标目录。
                     3.1 true: 如果目标文件夹已存在,强制删除并重新下载。
                     3.2 false: 如果目标文件夹存在,可能会报错或跳过。
                 4、head(类型:Boolean,默认值:false):是否下载HEAD版本。
                     4.1 true: 忽略指定的分支或 tag,直接下载默认分支的最新提交。
                 5、checkout(类型:String,默认值:-):指定要检出的分支或Tag。
                     5.1 例如:'main', 'dev', 'v1.0.0'。
               第四个参数:回调函数。
                 1、err:错误信息。
                 2、err:null,下载成功。

             注意:该函数是异步的。
            */
            downloadGitRepo(
              "direct:https://gitee.com/ascend/DrivingSDK.git",
              "./node_demo",
              { clone: true },
              (err) => {
                if (err) {
                  console.log("下载失败!");
                } else {
                  console.log("下载成功!");
                }
              },
            );
            break;
          case "Vue":
            console.log("使用 Vue 创建项目");
            break;
          case "Angular":
            console.log("使用 Angular 创建项目");
            break;
        }
      });
  });

program.parse(process.argv);

2.5 ora命令行任务等待工具

shell 复制代码
npm install ora
js 复制代码
#! /usr/bin/env node

import { program } from "commander";
import inquirer from "inquirer";
import downloadGitRepo from "download-git-repo";
import ora from "ora";

program
  .command("create <project-name> [other...]")
  .alias("crt")
  .description("创建项目")
  .action(() => {
    // 命令的执行逻辑代码。

    inquirer
      .prompt([
        {
          type: "rawlist",
          name: "framework",
          message: "请选择要使用的框架:",
          choices: ["React", "Vue", "Angular"],
        },
      ])
      .then((answers) => {
        switch (answers.framework) {
          case "React":
            // spinner1和spinner2两种写法等效。
            // const spinner1 = ora("downloading111...");
            const spinner2 = ora();
            // spinner2.text = "downloading222...";

            // 此时会一直显示等待中动画。哪怕其他异步代码执行完后,它也会接着显示。
            // spinner2.start();
            // 也可将spinner2.text写在start('...')中。
            spinner2.start("downloading333...");

            downloadGitRepo(
              "direct:https://gitee.com/ascend/DrivingSDK.git",
              "./node_demo",
              { clone: true },
              (err) => {
                if (err) {
                  // 中断等待动画函数。
                  // 提示字符串前面有一个对号。
                  // spinner2.succeed('执行成功!');
                  // 提示字符串前面有一个叉号。
                  spinner2.fail("执行失败!");
                  // 提示字符串前面有一个倒立的感叹号。
                  // spinner2.info('执行通知!');
                } else {
                  spinner2.succeed("执行成功!");
                }
              },
            );
            break;
          case "Vue":
            console.log("使用 Vue 创建项目");
            break;
          case "Angular":
            console.log("使用 Angular 创建项目");
            break;
        }
      });
  });

program.parse(process.argv);

2.6 chalk命令行样式渲染工具

shell 复制代码
npm install chalk
js 复制代码
#! /usr/bin/env node

import { program } from "commander";
import inquirer from "inquirer";
import downloadGitRepo from "download-git-repo";
import ora from "ora";
import chalk from "chalk";

program
  .command("create <project-name> [other...]")
  .alias("crt")
  .description("创建项目")
  .action(() => {
    // 命令的执行逻辑代码。

    inquirer
      .prompt([
        {
          type: "rawlist",
          name: "framework",
          message: "请选择要使用的框架:",
          choices: ["React", "Vue", "Angular"],
        },
      ])
      .then((answers) => {
        switch (answers.framework) {
          case "React":
            ora(
              chalk.green("正在下载中...") +
                chalk.red("请稍后...") +
                chalk.rgb(255, 165, 0)("不要急..."),
            ).start();

            downloadGitRepo(
              "direct:https://gitee.com/ascend/DrivingSDK.git",
              "./node_demo",
              { clone: true },
              (err) => {
                if (err) {
                  spinner.fail("执行失败!");
                } else {
                  spinner.succeed("执行成功!");
                }
              },
            );
            break;
          case "Vue":
            console.log("使用 Vue 创建项目");
            break;
          case "Angular":
            console.log("使用 Angular 创建项目");
            break;
        }
      });
  });

program.parse(process.argv);
相关推荐
一只小阿乐2 小时前
react中的zustand 模块化
前端·javascript·react.js·react状态管理·zustand
用户84298142418102 小时前
十二个JS混淆加密工具
javascript
久爱@勿忘2 小时前
uniapp H5 图片压缩并且转blob
前端·javascript·uni-app
Dashingl2 小时前
uni-app 页面传值 报错:TypeError: $t.setAttribute is not a function
前端·javascript·uni-app
甄心爱学习2 小时前
【项目实训】法律文书智能摘要系统2
前端·javascript·vue.js
早點睡3902 小时前
ReactNative项目OpenHarmony三方库集成实战:react-native-fs
javascript·react native·react.js
lxh01132 小时前
蜗牛排序题解
javascript·算法
一只小阿乐2 小时前
react 中的Zustand的store使用
前端·javascript·react.js·zustand
我命由我123452 小时前
Vue3 开发中,字符串中的 <br\> 标签被直接当作文本显示出来了,而不是被解析为 HTML 换行标签
开发语言·前端·javascript·vue.js·html·ecmascript·html5