在上一篇文章中,我们探讨了如何构建一个通用的脚手架框架。今天,我们将在此基础上进一步扩展脚手架的功能,赋予它下载项目模板的能力。

通常情况下,我们可以将项目模板发布到 npm 上,或者在公司内部利用私有 npm 仓库进行托管。通过交互式命令行界面,开发者可以轻松选择项目类型、项目名称以及所需的项目模板。脚手架将自动下载并生成对应的项目结构,为开发者提供便捷的项目初始化体验。

本文的内容基于上一篇文章中定义的基础脚手架。简单回顾一下目录结构:在 ice-basic-cli 项目中,存在一个 packages 文件夹,其中包含 4 个子包。cli 文件夹存放与命令行相关的内容,command 文件夹封装了 commander 类,init 文件夹包含了初始化 init 指令的实现,而 utils 文件夹则涵盖了工具函数和方法。

获取项目创建类型

首先,我们需要获取用户需要创建的项目类型。例如,用户可能需要基于 React 框架的模板,或者是 Vue 的模板;是 PC 端的项目,还是 H5 端的项目。这些信息都需要通过问答的形式让用户选择。

在命令行工具中实现问答交互,我们可以使用 inquirer 库。在 packages/utils 子包中新增 inquirer.js 文件,对 inquirer 进行封装,并导出上下键盘筛选和输入方法。

import inquirer from "inquirer";
function make({
  choices,
  defaultValue,
  message = "请选择",
  type = "list",
  require = true,
  mask = "*",
  validate,
  pageSize,
  loop,
}) {
  const options = {
    name: "name",
    default: defaultValue,
    message,
    type,
    require,
    mask,
    validate,
    pageSize,
    loop,
  };
  if (type === "list") {
    options.choices = choices;
  }
  return inquirer.prompt(options).then((answer) => answer.name);
} export function makeList(params) {
  return make({ ...params });
}

在同层级的 index.js 文件中,将需要使用的 makeList 方法导出。

import { makeList } from "./inquirer.js";
export {
  makeList,
};

接下来,我们在 packages/init/lib 文件夹下创建 createTemplate.js 文件,用于获取项目创建类型。用户可以根据需要选择是创建项目还是创建页面。

import { makList } from "@ice-basic-cli/utils";
const ADD_TYPE_PROJECT = "project";
const ADD_TYPE_PAGE = "page";
const ADD_TYPE = [
  {
    name: "项目",
    value: ADD_TYPE_PROJECT,
  },
  {
    name: "页面",
    value: ADD_TYPE_PAGE,
  },
];
function getAddType() {
  return makList({
    choices: ADD_TYPE,
    message: "请选择初始化类型",
    defaultValue: ADD_TYPE_PROJECT,
  });
}
export default async function createTemplate() {
  const addType = await getAddType();
}

接下来,我们在 init.js 文件中引入 createTemplate 方法。

import createTemplate from "./createTemplate.js";

async action() {
    await createTemplate()
}

当用户执行 npx @ice-basic-cli/cli 命令时,init 文件中的 action 方法会被执行,触发交互式问答流程,用户可以根据提示选择初始化类型(例如创建项目或页面)。

获取项目名称及项目模板

接下来,我们需要用户输入新建项目的名称。首先,在 utils/lib/inquirer.js 文件中定义 makeInput 方法并导出,用于封装输入项。

export function makeInput(params) {
  return make({
    type: "input",
    ...params,
  });
}

再次回到 packages/init/lib/createTemplate.js 文件中,定义输入项目名称的函数。如果用户在上一步选择了“项目”作为初始化类型,则进行下一步的输入项目名称操作。

import { makeList, makeInput } from "@ice-basic-cli/utils";
function getAddName() {
  return makeInput({
    message: "请输入项目名称",
    defaultValue: "",
    validate: (v) => {
      if (v.length > 0) return true;
      return "项目名称必填";
    },
  });
} export default async function createTemplate() {
  const addType = await getAddType();
  if (addType === ADD_TYPE_PROJECT) {
    const addName = await getAddName();
  }
}

完成项目名称输入后,紧接着让用户选择项目模板,比如 ReactVue 模板。这些模板应在 npm 上可查询且可下载。在使用之前,请确保已将自己的模板上传至npm 仓库。

这里以两个示例模板为例,它们是我之前上传到 npm 上的测试模板。在 packages/init/lib/createTemplate.js 中定义选择模板的代码如下:

const ADD_TEMPLATE = [
  {
    name: "vue3项目模板",
    value: "vue-template",
    npmName: "ice-ts-app",
    version: "0.0.1",
  },
  {
    name: "react项目模板",
    value: "react-template",
    npmName: "table-brush-copy",
    version: "1.0.1",
  },
]; function getAddTemplate() {
  return makeList({
    choices: ADD_TEMPLATE,
    message: "请选择项目模板",
  });
} export default async function createTemplate() {
  const addType = await getAddType();
  if (addType === ADD_TYPE_PROJECT) {
    const addName = await getAddName();
    const addTemplate = await getAddTemplate();
  }
}

当你在命令行输入 npx @ice-basic-cli/cli init 指令后,程序会依次提出预设的问题,包括选择初始化类型、输入项目名称以及选择项目模板。

npm API接入和封装

当用户完成了上述问答流程后,接下来我们可以通过 npm 来完成下载任务。首先,我们需要对 npm 的 API 进行一些封装以便于使用。

npm 提供了获取包信息的功能,通过访问 https://registry.npmjs.org/${npmName} 可以查看资源的最新版本、开发者信息、所需 Node 版本等详细信息。

例如,以 @ice-basic-cli/cli 为例,通过访问其 npm 页面可以看到最新版本号为 0.0.3。

现在,我们将这一功能封装到代码中。在 packages/utils/lib 下创建一个名为 npm.js 的文件,并定义获取 npm 包信息和获取最新版本号的方法。考虑到国内访问 https://registry.npmjs.org 可能较慢,可以将其替换为淘宝镜像 https://registry.npmmirror.com

import log from "./log.js";
import urlJoin from "url-join";
import axios from "axios"; function getNpmInfo(npmName) {
  const registry = "https://registry.npmmirror.com/";
  const url = urlJoin(registry, npmName);
  return axios.get(url).then((response) => {
    try {
      return response.data;
    } catch (err) {
      return Promise.reject(response);
    }
  });
} export function getLatestVersion(npmName) {
  return getNpmInfo(npmName).then((data) => {
    if (!data["dist-tags"] || !data["dist-tags"].latest) {
      log.error("没有 latest 版本号");
      return Promise.reject(new Error("没有 latest 版本号"));
    }
    return data["dist-tags"].latest;
  });
}

在同层级的 index.js 文件中导出 getLatestVersion 函数,以便其他模块调用。然后,在 packages/init/lib/createTemplate.js 文件中定义获取用户选择的 npm 包的最新版本号的逻辑。

import { getLatestVersion } from "@ice-basic-cli/utils";

export default async function createTemplate() {
  const addType = await getAddType();
  if (addType === ADD_TYPE_PROJECT) {
    const addName = await getAddName();
    const addTemplate = await getAddTemplate();
    const selectedTemplate = ADD_TEMPLATE.find((_) => _.value === addTemplate);
    const latestVersion = await getLatestVersion(selectedTemplate.npmName);
    selectedTemplate.version = latestVersion;
  }
}

下载项目模板

在获取到用户选择的模板之后,我们需要建立缓存目录以下载 npm 包。这可以通过编辑 packages/init/lib/createTemplate.js 文件来实现。

import { homedir } from "node:os";
import path from "node:path";
const TEMP_HOME = ".ice-cli"; function makeTargetPath() {
  return path.resolve(`${homedir()}/${TEMP_HOME}`);
} export default async function createTemplate() {
  const addType = await getAddType();
  if (addType === ADD_TYPE_PROJECT) {
    const addName = await getAddName();
    const addTemplate = await getAddTemplate();
    const selectedTemplate = ADD_TEMPLATE.find((_) => _.value === addTemplate);
    const latestVersion = await getLatestVersion(selectedTemplate.npmName);
    selectedTemplate.version = latestVersion;
    const targetPath = makeTargetPath();
    return {
      type: addType,
      name: addName,
      template: selectedTemplate,
      targetPath,
    };
  }
}

接下来,在 createTemplate.js 的同级目录下创建 downloadTemplate.js 文件,用于定义将模板下载到缓存目录的逻辑。这里有几个关键点需要注意:

  • 使用 path-exists 来判断文件是否存在,并通过 fs-extra 创建不存在的目录。
  • 通过 execa 执行如 npm install {npmName}@{npmVersion} 这样的 npm 命令。
  • 由于下载过程可能耗时较长,我们使用 ora 库展示下载进度条。
import path from "node:path";
import { pathExistsSync } from "path-exists";
import fse from "fs-extra";
import ora from "ora";
import { execa } from "execa"; function getCacheDir(targetPath) {
  return path.resolve(targetPath, "node_modules");
}
function makeCacheDir(targetPath) {
  const cacheDir = getCacheDir(targetPath);
  if (!pathExistsSync(cacheDir)) {
    fse.mkdirpSync(cacheDir);
  }
} async function downloadAddTemplate(targetPath, selectedTemplate) {
  const { npmName, version } = selectedTemplate;
  const installCommand = "cnpm";
  const installArgs = ["install", `${npmName}@${version}`];
  const cwd = targetPath;
  await execa(installCommand, installArgs, { cwd });
} export default async function downloadTemplate(selectedTemplate) {
  const { targetPath, template } = selectedTemplate;
  makeCacheDir(targetPath);
  const spinner = ora("正在下载模板...").start();
  try {
    await downloadAddTemplate(targetPath, template);
    spinner.stop();
  } catch (e) {
    spinner.stop();
  }
}

拷贝项目模板

完成模板下载后,下一步是将其从缓存目录复制到用户希望创建的项目位置。这涉及到以下逻辑:

  • 获取用户输入的文件夹名称并检查该名称是否已存在。如果存在且指定了 --force 参数,则移除原有文件夹并创建新文件夹;否则提示错误信息。
  • 将缓存目录下的内容复制到用户新建的项目文件夹内。
import fse from "fs-extra";
import { pathExistsSync } from "path-exists";
import { log } from "@ice-basic-cli/utils";
import ora from "ora";
import path from "path"; export default function installTemplate(selectedTemplate, opts = {}) {
  const { force = false } = opts;
  const { targetPath, template, name } = selectedTemplate;
  const rootDir = process.cwd();
  fse.ensureDirSync(targetPath);
  const installDir = path.resolve(`${rootDir}/${name}`);   if (pathExistsSync(installDir)) {
    if (!force) {
      log.error(`当前目录下已存在 ${installDir} 文件夹`);
    } else {
      fse.removeSync(installDir);
      fse.ensureDirSync();
    }
  } else {
    fse.ensureDirSync(installDir);
  }
  copyFile(targetPath, template, installDir);
} function getCacheFilePath(targetPath, template) {
  return path.resolve(targetPath, "node_modules", template.npmName);
} function copyFile(targetPath, template, installDir) {
  const originFile = getCacheFilePath(targetPath, template);
  const fileList = fse.readdirSync(originFile);
  const spinner = ora("正在拷贝文件").start();
  fileList.map((file) => {
    fse.copySync(`${originFile}/${file}`, `${installDir}/${file}`);
  });
  spinner.stop();
  log.success("模板拷贝成功");
}

再次回到 init.js 文件中,导入 downloadTemplate.js 以及其他必要的模块,确保整个流程能够顺利运行。

import createTemplate from "./createTemplate.js";
import downloadTemplate from "./downloadTemplate.js";
import installTemplate from "./installTemplate.js"; class InitCommand extends Command {
async action(name, opts) {
    const selectedTemplate = await createTemplate(name, opts);
    await downloadTemplate(selectedTemplate);
   // 安装项目模板至项目目录
    await installTemplate(selectedTemplate, opts);
  }
}

到这里,一个交互式命令行下载模板的流程已经构建完成。

非交互式项目创建

为了提供更大的灵活性,我们还需要支持非交互式的命令行创建项目方法,因为单元测试时需要通过命令而非交互方式来创建项目。

首先,在 init.js 文件中增加接收非交互式命令的指令集。

 get options() {
    return [
      ["-f, --force", "是否强制更新", false],
      ["-t, --type <type>", "项目类型(值:project/page)"],
      ["-p, --template <template>", "模板名称"],
    ];
  }

然后,在 createTemplate.js 文件中处理这些参数,并根据不同的输入参数执行相应的流程。

export default async function createTemplate(name, opts) {
  const { type = null, template = null } = opts;
  let addType;
  let addName;
  let selectedTemplate;
  if (type) {
    addType = type;
  } else {
    addType = await getAddType();
  }
  if (addType === ADD_TYPE_PROJECT) {
    if (name) {
      addName = name;
    } else {
      addName = await getAddName();
    }
    if (template) {
      selectedTemplate = ADD_TEMPLATE.find((tp) => tp.value === template);
      if (!selectedTemplate) {
        throw new Error(`项目模板 ${template} 不存在!`);
      }
    } else {
      const addTemplate = await getAddTemplate();
      selectedTemplate = ADD_TEMPLATE.find((_) => _.value === addTemplate);
    }
    const latestVersion = await getLatestVersion(selectedTemplate.npmName);
    selectedTemplate.version = latestVersion;
    const targetPath = makeTargetPath();
    return {
      type: addType,
      name: addName,
      template: selectedTemplate,
      targetPath,
    };
  } else {
    throw new Error(`创建的项目类型不支持`);
  }
}

现在,在命令行工具中输入 npx @ice-basic-cli/cli init testProject --type project --template vue-template --force --debug 即可直接下载模板到 testProject 文件夹中。

通过上述步骤,我们已经详细介绍了如何使用 Node.js 实现一个从模板下载到项目创建的完整流程。这个过程不仅涵盖了交互式的命令行工具开发,还考虑到了非交互式的应用场景,确保了在各种情况下都能灵活高效地创建项目。

如果你对前端工程化有兴趣,或者想了解更多相关的内容,欢迎查看我的其他文章,这些内容将持续更新,希望能给你带来更多的灵感和技术分享~

使用Node.js打造交互式脚手架,简化模板下载与项目创建的更多相关文章

  1. NodeJs>------->>第二章:Node.js中交互式运行环境--------REL

    第二章:Node.js中交互式运行环境--------REL 一:REPL运行环境概述 C:\Users\junliu>node > foo = 'bar' ; 'bar' > 二: ...

  2. Node.js REPL(交互式解析器)

    Node.js REPL(交互式解释器) Node 自带了交互式解释器,可以执行以下任务: 读取 - 读取用户输入,解析输入了Javascript 数据结构并存储在内存中. 执行 - 执行输入的数据结 ...

  3. 你要是还学不会,请提刀来见 Typora+PicGo+Gitee + node.js 打造个人高效稳定优雅图床

    你要是还学不会,请提刀来见 Typora+PicGo+Gitee + node.js 打造个人高效稳定优雅图床 经过前面两弹的介绍,相信大家对图床都不陌生了吧, 但是小魔童觉得这样做法还是不方便,使用 ...

  4. Node.js 入门到干活,10 个优质项目就够了!

    Node.js 在很多大公司都有不错的实践,比如:淘宝.天猫 Web 版,很多页面都是在 Node 服务器上渲染的.还有各种脚手架.前端打包发布工具.构建生态的小工具,也基本都是 Node.js 编写 ...

  5. Node.js 从零开发 web server博客项目[express重构博客项目]

    web server博客项目 Node.js 从零开发 web server博客项目[项目介绍] Node.js 从零开发 web server博客项目[接口] Node.js 从零开发 web se ...

  6. Node.js 从零开发 web server博客项目[数据存储]

    web server博客项目 Node.js 从零开发 web server博客项目[项目介绍] Node.js 从零开发 web server博客项目[接口] Node.js 从零开发 web se ...

  7. Node.js 从零开发 web server博客项目[koa2重构博客项目]

    web server博客项目 Node.js 从零开发 web server博客项目[项目介绍] Node.js 从零开发 web server博客项目[接口] Node.js 从零开发 web se ...

  8. Node.js 从零开发 web server博客项目[安全]

    web server博客项目 Node.js 从零开发 web server博客项目[项目介绍] Node.js 从零开发 web server博客项目[接口] Node.js 从零开发 web se ...

  9. Node.js 从零开发 web server博客项目[日志]

    web server博客项目 Node.js 从零开发 web server博客项目[项目介绍] Node.js 从零开发 web server博客项目[接口] Node.js 从零开发 web se ...

  10. Node.js 从零开发 web server博客项目[登录]

    web server博客项目 Node.js 从零开发 web server博客项目[项目介绍] Node.js 从零开发 web server博客项目[接口] Node.js 从零开发 web se ...

随机推荐

  1. 【前端】HTML编码效提升:快速生成HTML标签

    目录 1.生成多级标签 2.生成同级标签 3.生成注释 4.生成多个相同标签 5.生成带class标签 6生成带id标签. 7.生成带内容标签1 8.生成带内容标签2 9.生成带属性标签 GIF演示: ...

  2. Android信任证书,把用户级别放入系统级别

    三.操作步骤 1.在Windows安装openssl,用来把证书转成 .pem 格式 1)下载和安装 下载其他人做的便捷版安装包:http://slproweb.com/products/Win32O ...

  3. kubernetes上报Pod已用内存不准问题分析

    1.问题描述: 经常有业务反馈在使用容器云平台过程中监控展示的业务使用内存不准,分析了下kubernetes采集Pod内存使用的实现原理以及相应的解决思路 2.问题分析: 2.1 问题排查: 监控数据 ...

  4. (default-compile) on project app: Fatal error compiling: 无效的标记: --release -> [Help 1]

    <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <versio ...

  5. Linux 虚拟机重启找不到IP解决方案

    @ 目录 前言 简介 Linux 操作系统查看不到IP地址 问题描述: 第一步 :修改配置 第二步 :查看ip 第三步 :查看网卡 第四步 :重启网络 ‌Linux 网络服务重启失败解决办法 问题描述 ...

  6. Qt音视频开发43-采集屏幕桌面并推流(支持分辨率/矩形区域/帧率等设置/实时性极高)

    一.前言 采集电脑屏幕桌面并推流一般是用来做共享桌面.远程协助.投屏之类的应用,最简单入门的做法可能会采用开个定时器或者线程抓图,将整个屏幕截图下来,然后将图片传出去,这种方式很简单但是性能要低不少, ...

  7. vue 控件的淡入淡出

    页面代码. 1.首先要用transition 包裹一下,设置name或者不设置都可以,其次transition 下面要有一个div设置v-if来触发移入移出 <transition name=& ...

  8. 如何在众多Ubuntu版本中挑选出最适配自身需求的系统版本?用德承工控机GM-1100来深度剖析其中的门道

    Ubuntu是一款基于Debian GNU/Linux,支持x86.amd64(x64)和ppc架构,以桌面应用为主的Linux操作系统.其名称来自非洲南部的语言"ubuntu"( ...

  9. Java中hashCode() 和 equals()

    该文章为转载(原文链接在结尾),虽然篇幅偏长,但是却能使你真正理解hashCode和queals各自的作用以及之间的联系,尤其是第四部分,读完肯定会让你有所收获. 第1部分 equals() 的作用 ...

  10. MySQL-扩展

    1.行转列 源数据: 目标数据: 数据准备 -- 建表插入数据 drop table if exists time_temp; create table if not exists time_temp ...