从零配置自己的项目脚手架

152 阅读4分钟

前提条件

在开始之前,请确保安装了 Node.js 的最新版本。使用 Node.js 最新的长期支持版本(LTS - Long Term Support),是理想的起步。使用旧版本,你可能遇到各种问题,因为它们可能缺少 webpack 功能以及/或者缺少相关 package 包。

建一个空文件夹

让我们在桌面建一个项目文件夹,名为 xp-cli ,并使用你的编辑器打开它。

打开终端,快捷键(Ctrl + ~)。

执行以下命令:

npm init -y

上面命令会在 xp-cli 的根目录生成 package.json 文件,该文件定义了这个项目所需要的各种模块,以及项目的配置信息(比如名称、版本、描述信息等数据)。npm install 命令也是根据这个配置文件,自动下载所需的模块。

package.json

{
  "name": "xp-cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
+ "bin": {
+   "xp": "index.js"
+ }
}

创建index.js

#!/usr/bin/env node

console.log('123');

index.js 最上面必须加上 #!/usr/bin/env node 这行代码,不然绑定指令之后运行会报错。

绑定指令

这样我们就可以在全局任何地方通过输入 xp 命令来运行 index.js。

npm link

解绑指令:

npm unlink

此时我们项目的文件结构如下:

project

 xp-cli
  |- index.js
  |- package-lock.json
  |- package.json

现在执行xp结果为:

原生获取命令行参数的方式

使用:process.argv 关键字。

使用 Commander 解析命令行参数

npm install commander

在 index.js 文件中输入以下代码:

#!/usr/bin/env node

const { Command } = require("commander");
const program = new Command();

program.version("0.0.1");

program
  .command("init <projectName> <tempName>")
  .description("项目初始化")
  .action((projectName, tempName) => {
    console.log(projectName, tempName);
  });

program
  .command("list")
  .alias("ls")
  .description("查看可用项目模块")
  .action(() => {
    console.log(`
        webpack5 + React
        webpack5 + Vue
    `);
  })

program.parse(process.argv);

执行xp命令如下:

安装 download-git-repo 下载模板

npm i download-git-repo

仅支持以下三个仓库源:

  • GitHub
  • GitLab
  • Bitbucket

可使用 clone-repo 下载

NPM 地址:

www.npmjs.com/package/clo…

  1. 安装

npm i clone-repo

  1. 使用

clone-repo 是国内大佬基于 download-git-repo 3.0.2 版本扩展开发的,与 download-git-repo 不同的是 clone-repo 会返回一个 Promise,且 clone-repo 支持以下四个仓库源:

  • GitHub
  • GitLab
  • Gitee
  • Bitbucket

download的配置

const download = require("download-git-repo");

const templates = {
  react: {
    downloadUrl: "xiehaisheng/webpack5_react",
    dec: "webpack5 + react CLI",
  },
  vue: {
    downloadUrl: "xiehaisheng/webpack5_vue",
    dec: "webpack5 + react CLI",
  },
  utils: {
    downloadUrl: "xiehaisheng/js-utils.git",
    dec: "TypeScript开发js函数库",
};

program
  .command("init <projectName> <tempName>")
  .description("项目初始化")
  .action((projectName, tempName) => {
    console.log(`项目名称为:${projectName}, 使用模板名:${tempName}`);
    const { downloadUrl } = templates[tempName];
    download(
      // 下载目标,格式为:用户名/仓库名字#分支
      downloadUrl,
      // 下载完成后的项目名称,也就是文件夹名
      projectName,
      // 下载结束后的回调
      (err) => {
        // 如果错误回调不存在,就表示下载成功了
        console.log(err ? `下载失败!${err}` : "下载成功!");
      }
    );
  });

运行结果:

使用 inquirerhandlebars 采集处理用户信息

npm i inquirer handlebars

命令行交互 inquirer, 能够与用户在命令行进行参数选择交互。

修改模板的 package.json

注意:这里是修改模板的配置文件,而不是当前项目的配置文件。

inquirer 和 handlebars 的使用, 将其放在 download 的下载回调中。

const inquirer = require("inquirer");
const handlebars = require("handlebars");
const fs = require("fs");

download(
      // 下载目标,格式为:用户名/仓库名字#分支
      downloadUrl,
      // 下载完成后的项目名称,也就是文件夹名
      projectName,
      // 下载结束后的回调
      (err) => {
        // 如果错误回调不存在,就表示下载成功了
        if (err) {
          return console.log(`下载失败!${err}`);
        } else {
          inquirer
            .prompt([
              {
                type: "input",
                name: "name",
                message: "请输入项目名称",
              },
              {
                type: "input",
                name: "description",
                message: "请输入项目简介",
              },
              {
                type: "input",
                name: "author",
                message: "请输入作者名称",
              },
            ])
            .then((data) => {
              const packagePath = `${projectName}/package.json`;
              const packageContent = fs.readFileSync(packagePath, "utf-8");
              var packageRes = handlebars.compile(packageContent)(data);
              fs.writeFileSync(packagePath, packageRes);
              console.log(`初始化模板成功`);
            });
        }
      }
    );

执行结果:

视觉美化

npm i ora chalk log-symbols

下载中 loading 效果

字体美化 chalk

日志符号 logSymbols

起因 ora 最新版只能使用 import 来导入,所以要设置当前项目的默认包管理为 ES6Module。

修改引入方式

import { Command } from "commander";
import download from "download-git-repo";
import inquirer from "inquirer";
import handlebars from "handlebars";
import fs from "fs";
import ora from 'ora';
const program = new Command();

解决ES6引入

为什么CommonJS和ES6可以混合使用?

分别在模板下载开始前,下载中,下载完后调用 ora 的 start()、fail()、succeed() 方法。

使用ora:

const spinner = ora('下载中...');
// 添加下载中样式,开始
loading.start();

// 调用 ora 下载失败方法,进行提示
loading.fail("下载失败:");

// 调用 ora 下载成功方法,进行提示
loading.succeed("下载成功!");

chalk 能够自定义使输出的命令行字体颜色

使用chalk:

import chalk from 'chalk';

console.log(chalk.blue('Hello world!'));

通过调用 logSymantec 的 success 和 error 输出成功与失败的日志符号。

使用logSymbols:

import logSymbols from 'log-symbols';

console.log(logSymbols.success, 'Finished successfully!');

综合使用:

.action((projectName, tempName) => {
    // 添加下载中样式,开始
    const spinner = ora(`项目名称为:${projectName}, 使用模板为:${tempName} 下载中...`);
    spinner.start();
    // console.log(`项目名称为:${projectName}, 使用模板名:${tempName}`);
    const { downloadUrl } = templates[tempName];
    download(
      // 下载目标,格式为:用户名/仓库名字#分支
      downloadUrl,
      // 下载完成后的项目名称,也就是文件夹名
      projectName,
      // 下载结束后的回调
      (err) => {
        // 如果错误回调不存在,就表示下载成功了
        if (err) {
          // 调用 ora 下载失败方法,进行提示
          spinner.fail(chalk.red('下载失败:'));
          console.log(err);
          return;
        }
        spinner.succeed('下载成功!');
        inquirer
          .prompt([
            {
              // 输入类型
              type: "input",
              // 字段名称
              name: "name",
              // 提示信息
              message: "请输入项目名称",
            },
            {
              type: "input",
              name: "description",
              message: "请输入项目简介",
            },
            {
              type: "input",
              name: "author",
              message: "请输入作者名称",
            },
          ])
          .then((data) => {
            const packagePath = `${projectName}/package.json`;
            const packageContent = fs.readFileSync(packagePath, "utf-8");
            var packageRes = handlebars.compile(packageContent)(data);
            fs.writeFileSync(packagePath, packageRes);
            console.log(logSymbols.success ,chalk.green('初始化模板成功'));
          });
      }
    );
  });

失败执行结果:

成功执行结果:

源码

#!/usr/bin/env node
import { Command } from "commander";
import download from "download-git-repo";
import inquirer from "inquirer";
import handlebars from "handlebars";
import fs from "fs";
import ora from "ora";
import chalk from "chalk";
import logSymbols from "log-symbols";
const program = new Command();

const templates = {
  react: {
    downloadUrl: "xiehaisheng/webpack5_react",
    dec: "webpack5 + react CLI",
  },
  vue: {
    downloadUrl: "xiehaisheng/webpack5_vue",
    dec: "webpack5 + react CLI",
  },
};

program.version("0.0.1");
program
  .command("init <projectName> <tempName>")
  .description("项目初始化")
  .action((projectName, tempName) => {
    // 添加下载中样式
    const spinner = ora(
      `项目名称为:${projectName}, 使用模板为:${tempName} 下载中...`
    );
    spinner.start();
    const { downloadUrl } = templates[tempName];
    download(
      // 下载目标,格式为:用户名/仓库名字#分支
      downloadUrl,
      // 下载完成后的项目名称,也就是文件夹名
      projectName,
      // 下载结束后的回调
      (err) => {
        // 如果错误回调不存在,就表示下载成功了
        if (err) {
          spinner.fail(chalk.red("下载失败:"));
          console.log(err);
          return;
        }
        spinner.succeed("下载成功!");
        inquirer
          .prompt([
            {
              type: "input",
              name: "name",
              message: "请输入项目名称",
            },
            {
              type: "input",
              name: "description",
              message: "请输入项目简介",
            },
            {
              type: "input",
              name: "author",
              message: "请输入作者名称",
            },
          ])
          .then((data) => {
            const packagePath = `${projectName}/package.json`;
            const packageContent = fs.readFileSync(packagePath, "utf-8");
            var packageRes = handlebars.compile(packageContent)(data);
            fs.writeFileSync(packagePath, packageRes);
            console.log(logSymbols.success, chalk.green("初始化模板成功"));
          });
      }
    );
  });
program
  .command("list")
  .alias("ls")
  .description("查看可用项目模块")
  .action(() => {
    for (const key in templates) {
      console.log(templates[key].dec);
    }
  });
program.parse(process.argv);

npm 发包

  1. 上 NPM 官网注册一个账号,官网:

www.npmjs.com/~achens

  1. 上 NPM 搜索看有无重名包。
  2. 把 package.json 中的 name 修改为发布到 NPM 上的包名。

这个和本地项目名称无关的。

  1. 打开项目,执行登录命令:

npm login

  1. 登陆成功以后,在项目下执行发布命令:

npm publish

  1. 进行验证并下载我们自己的包

// 取消本地连接的全局指令

npm unlink

// 安装我们自己的包

npm i item-cli

{
  "name": "item-cli",
  "version": "0.0.2",
  "description": "",
  "type": "module",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bin": {
    "xhs": "index.js"
  },
  "dependencies": {
    "chalk": "^5.0.1",
    "clone-repo": "^1.0.2",
    "commander": "^9.2.0",
    "download-git-repo": "^3.0.2",
    "handlebars": "^4.7.7",
    "inquirer": "^8.2.2",
    "log-symbols": "^5.1.0",
    "ora": "^6.1.0"
  }
}

测试

npm install -g item-cli

xhs init my-project react 
# or
xhs init my-project vue