首页 > 其他分享 >为什么要开发手脚架npm包

为什么要开发手脚架npm包

时间:2024-06-19 11:28:18浏览次数:11  
标签:npm node log plugin create rollup 开发 手脚

用户行为分析

找项目

脚手架目的

减少项目准备工作

重复的东西做多了是不对的 统一的代码规范 将重复的工作封装

达到标准化规范 优化开发性能 节约开发20%-30%工期 提高开发效率

降低配置难度 使团队快速启动项目DD 以便后续维护 减少项目开发内存

rollup打包工具 npm pnpm nvm

前言 

如何注册 npm 教程: npm 配置双因素身份验证_enable two-factor authentication (2fa)-CSDN博客

注意问题

rollup-plugin-node-externals 需要使用 5.1.2 的版本 不要使用最新的 6.1.2 版本

chalk 版本降级到 4.1.2 的版本

初始化项目

npm init -y

生成:typescript 配置文件 tsconfig.json

npx tsc --init

package.json 里面的依赖说明

 "devDependencies": {
    // 用于命令行交互。
    "@inquirer/prompts": "^3.2.0",
    // Rollup 相关的插件,用于模块打包
    "@rollup/plugin-commonjs": "^25.0.3", // 支持rollup打包commonjs模块
    "@rollup/plugin-json": "^6.0.1", // 支持rollup打包json文件
    "@rollup/plugin-node-resolve": "^15.1.0", // 用于帮助 Rollup 解析和处理 Node.js 模块(Node.js 的 CommonJS 模块规范)
    "@rollup/plugin-terser": "^0.4.3", // Rollup 构建过程中对生成的 JavaScript 代码进行压缩和混淆,以减小最终输出文件的体积。
    // TypeScript 的类型定义文件
    "@types/fs-extra": "^11.0.2",
    "@types/lodash": "^4.14.199",
    "@types/node": "^16.18.40",
     // 用于发起 HTTP 请求。
    "axios": "^1.5.0",
    // 在命令行中输出彩色文本。
    "chalk": "^4.1.2",
     // 命令行界面的解决方案
    "commander": "^11.0.0",
    // 扩展了标准 fs 模块的文件系统操作
    "fs-extra": "^11.1.1",
    // 一个提供实用函数的 JavaScript 库。
    "lodash": "^4.17.21",
    // 在命令行中显示日志符号。
    "log-symbols": "^4.1.0",
    // 创建可旋转的加载器
    "ora": "5",
     // 估算操作进度。
    "progress-estimator": "^0.3.1",
    // 一个特定于项目或定制的 CLI 工具
    "pure-thin-cli": "^0.1.8",

    "rollup": "^4.6.1",
    "rollup-plugin-dts": "^5.3.0", // 是一个 Rollup 插件,它的主要作用是处理 TypeScript 的声明文件(.d.ts 文件)
    "rollup-plugin-esbuild": "^5.0.0",
    "rollup-plugin-node-externals": "^5.1.2", // 使rollup自动识别外部依赖
    "rollup-plugin-typescript2": "^0.36.0", // 支持rollup打包ts文件

    // 用于 Git 命令的 Node.js 封装。
    "simple-git": "^3.19.1",
    // TypeScript 运行时库。
    "tslib": "^2.6.1",
    "typescript": "^5.2.2"
  },

基础结构

  • 我们这次的目标就是搭建一个类似于 vue-cli,create-react-app 等 cli 工具类似的工具包。要实现的核心功能就是使用命令行交互的效果去生成我们需要的 Vue 项目模板。

  • 首先把项目文件结构创建一下,一步步教大家实现。


xqy-cli/
    |- src/ # 项目资源
        |- command/  # 命令逻辑
        |- utils/   # 公共方法
        |- index.ts  # 命令入口文件

用到的依赖

命令行交互

  • commander:解析命令行指令

  • ora:终端加载动画

  • progress-estimator:终端加载条动画

  • log-symbols:终端输出符号

  • chalk:终端字体美化

  • @inquirer/prompts:终端输入交互

打包工具

  • rollup(打包工具有很多选择,webpack)

    • @rollup/plugin-node-resolve:支持 rollup 打包 node.js 模块

  • @rollup/plugin-commonjs:支持 rollup 打包 commonjs 模块

  • @rollup/plugin-json:支持 rollup 打包 json 文件

  • rollup-plugin-typescript2:支持 rollup 打包 ts 文件

  • @rollup/plugin-terser:压缩打包代码

  • rollup-plugin-node-externals:使 rollup 自动识别外部依赖 :::warning 注意这里的 rollup-plugin-node-externals 版本是 5 的版本 不是最新的 6 的版本 "rollup-plugin-node-externals": "^5.1.2", :::

配置打包命令

我们先解决打包的问题,安装好需要的依赖,然后按照下面的配置文件内容,新建一个 rollup.config.js。

pnpm add -D rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-json rollup-plugin-typescript2 @rollup/plugin-terser rollup-plugin-node-externals

import { defineConfig } from "rollup";
import nodeResolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import externals from "rollup-plugin-node-externals";
import json from "@rollup/plugin-json";
import terser from "@rollup/plugin-terser";
import typescript from "rollup-plugin-typescript2";

export default defineConfig([
  {
    input: {
      index: "src/index.ts", // 打包入口文件
    },
    output: [
      {
        dir: "dist", // 输出目标文件夹
        format: "cjs", // 输出 commonjs 文件
      },
    ],
    // 这些依赖的作用上文提到过
    plugins: [
      nodeResolve(),
      externals({
        devDeps: false, // 可以识别我们 package.json 中的依赖当作外部依赖处理 不会直接将其中引用的方法打包出来
      }),
      typescript(),
      json(),
      commonjs(),
      terser(),
    ],
  },
]);

我们还需要在 package.json 中配置一个打包命令。

  • -c 指定 rollup 配置文件,--bundleConfigAsCjs 将配置转为 commonjs 执行。

{
  // ...
  "build": "rollup -c rollup.config.js --bundleConfigAsCjs"
}

编写指令

指令交互

  • 这里带大家写一个 create 指令,在我们的入口文件 src/index.ts 编写。

  • 我们需要用到 commander,可以帮助我们解析用户在命令行输入的指令。

  • 这里会给大家讲解一些基础的用法,更详细的使用方式请查阅官方文档:commander.js

首先初始化一个 Command 对象,传入的参数作为我们的指令名称。

import { Command } from "commander";
// 这里我们用 xqy 当作我的指令名称
// 命令行中使用 xqy xxx 即可触发
const program = new Command("xqy");

接下来我们就可以配置我们需要的命令了。

  • 使用 version 可以实现最基础的查看版本的指令。

import { version } from "../package.json";
// .vesion 表示可以使用 -V --version 参数查看当前SDK版本
// 我们直接使用 package.json 中的 version 即可
program.version(version);
// 调用 version 的参数可以自定义
// .version(version, '-v --version')
  • 使用 command 与 action 实现自定义指令。

    • command 为我们需要的命令名称。

    • description 为命令添加描述。

    • action 为指令触发执行的回调。

    • argument 为我们命令需要的参数,[]包裹代表可选,<>包裹代表必填。

下面的示例就是我们编写好的指令,指令回调我们稍后实现,输入 xqy update 会打印 update command,输入 xqy create test,会打印 create test。action 回调中会将 argument 中的参数传入。

// ...

program
  .command("update")
  .description("更新 xqy 至最新版本")
  .action(async () => {
    console.log("update command");
  });

program
  .command("create")
  .description("创建一个新项目")
  .argument("[name]", "项目名称")
  .action(async (name) => {
    if (name) console.log(`create ${name}`);
    else console.log(`create command`);
  });

解析指令

// ...

// parse 会解析 process.argv 中的内容
// 也就是我们输入的指令
program.parse();

下载项目

我们先实现 create 命令,可以让用户选择下载我们预设的模板。

  • 在 src/command/create.ts 文件下编写 create 命令核心代码。

  • 导出一个可以传入项目名称的方法,如果用户直接传入了项目名称则让用户选择模板,否则需要先让用户输入项目名称。

  • 这里我们用到了@inquirer/prompts,可以帮助我们让用户在终端进行输入或选择的操作,更多使用方法请查阅官方文档:inquirer.js

import { select, input } from "@inquirer/prompts";
export default async function create(prjName?: string) {
  // 文件名称未传入需要输入
  if (!prjName) prjName = await input({ message: "请输入项目名称" });
  // 如果文件已存在需要让用户判断是否覆盖原文件
  const filePath = path.resolve(process.cwd(), prjName);
  if (fs.existsSync(filePath)) {
    const run = await isOverwrite(prjName);
    if (run) {
      await fs.remove(filePath);
    } else {
      return; // 不覆盖直接结束
    }
  }
}

在 src/command/create.ts 添加一个判断用户是否覆盖的公共方法。

import { select } from "@inquirer/prompts";
import log from "./log";

export const isOverwrite = async (fileName: string) => {
  log.warning(`${fileName} 文件已存在 !`);
  return select({
    message: "是否覆盖原文件: ",
    choices: [
      { name: "覆盖", value: true },
      { name: "取消", value: false },
    ],
  });
};
  • 然后我们就需要让用户选择我们的预设模板,在 src/command/create.ts 中添加模板信息,定义成 map 的形式是方便我们根据 key 获取项目的信息。

  • 下载模板的方式有很多种,可以将模板文件保存在 SDK 中,使用 cjs 或者其他方法动态选择生成,使用 fs 模块写入,或者存放在 git 仓库中进行 clone,我们这里把代码放到 gitee 中的代码仓库中

  • 这里我定义了 TemplateInfo 类型,可以根据自己的需求自行定义,需要存储项目名称,下载地址,描述,代码分支。

export interface TemplateInfo {
  name: string; // 项目名称
  downloadUrl: string; // 下载地址
  description: string; // 项目描述
  branch: string; // 项目分支
}
// 这里保存了我写好了咱们的之前开发的模板
export const templates: Map<string, TemplateInfo> = new Map([
  [
    "Vite4-Vue3-Typescript-template",
    {
      name: "admin-template",
      downloadUrl: "[email protected]:sohucw/admin-pro.git",
      description: "Vue3技术栈开发模板",
      branch: "dev6",
    },
  ],
]);

接下来我们就可以让用户选择需要的模板。

import { select, input } from "@inquirer/prompts";

import log from "../utils/log";

export interface TemplateInfo {
  name: string; // 项目名称
  downloadUrl: string; // 下载地址
  description: string; // 项目描述
  branch: string; // 项目分支
}
// 这里保存了我写好的预设模板
export const templates: Map<string, TemplateInfo> = new Map([
  [
    "Vite4-Vue3-Typescript-template",
    {
      name: "admin-template",
      downloadUrl: "[email protected]:sohucw/admin-pro.git",
      description: "Vue3技术栈开发模板",
      branch: "dev8",
    },
  ],
]);

export default async function create(prjName?: string) {
  // ...

  // 我们需要将我们的 map 处理成 @inquirer/prompts select 需要的形式
  // 大家也可以封装成一个方法去处理
  const templateList = [...templates.entries()].map(
    (item: [string, TemplateInfo]) => {
      const [name, info] = item;
      return {
        name,
        value: name,
        description: info.description,
      };
    }
  );

  // 选择模板
  const templateName = await select({
    message: "请选择需要初始化的模板:",
    choices: templateList,
  });

  // 下载模板
  const gitRepoInfo = templates.get(templateName);
  if (gitRepoInfo) {
    await clone(gitRepoInfo.downloadUrl, prjName, [
      "-b",
      `${gitRepoInfo.branch}`,
    ]);
  } else {
    log.error(`${templateName} 模板不存在`);
  }
}
  • 我们还需要实现我们刚刚使用过的 clone 方法,下载仓库中的模板。

  • 我们在 src/utils/clone.ts 中实现下。

    • 这里我们用到,simple-git 用于拉取 git 仓库,progress-estimator 设置预估 git clone 的时间并展示进度条。

  • 这里我就直接展示代码和注释了,思路都很简单。

import simpleGit, { SimpleGit, SimpleGitOptions } from "simple-git";
import log from "./log";
import createLogger from "progress-estimator";
import chalk from "chalk";

const logger = createLogger({
  // 初始化进度条
  spinner: {
    interval: 300, // 变换时间 ms
    frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"].map((item) =>
      chalk.blue(item)
    ), // 设置加载动画
  },
});

const gitOptions: Partial<SimpleGitOptions> = {
  baseDir: process.cwd(), // 根目录
  binary: "git",
  maxConcurrentProcesses: 6, // 最大并发进程数
};

export const clone = async (
  url: string,
  prjName: string,
  options: string[]
): Promise<any> => {
  const git: SimpleGit = simpleGit(gitOptions);
  try {
    // 开始下载代码并展示预估时间进度条
    await logger(git.clone(url, prjName, options), "代码下载中: ", {
      estimate: 8000, // 展示预估时间
    });

    // 下面就是一些相关的提示
    console.log();
    console.log(chalk.blueBright(`==================================`));
    console.log(chalk.blueBright(`=== 欢迎使用 xqy-cli 脚手架 ===`));
    console.log(chalk.blueBright(`==================================`));
    console.log();

    log.success(`项目创建成功 ${chalk.blueBright(prjName)}`);
    log.success(`执行以下命令启动项目:`);
    log.info(`cd ${chalk.blueBright(prjName)}`);
    log.info(`${chalk.yellow("pnpm")} install`);
    log.info(`${chalk.yellow("pnpm")} run dev`);
  } catch (err: any) {
    log.error("下载失败");
    log.error(String(err));
  }
};

至此,我们的 create 命令就编写完毕了,我们可以将其添加到 src/index.ts 中去调用。

// ...
program
  .command("create")
  .description("创建一个新项目")
  .argument("[name]", "项目名称")
  .action(async (dirName) => {
    // 添加create方法
    await create(dirName);
  });
// ...

检测项目更新

  • 当我们更新模板后,希望用户第一时间用到,可以在用户使用过程中添加一些更新提示。

在 src/command/create.ts 中编写方法,用于获取 npm 包的信息及版本号。

// npm 包提供了根据包名称查询包信息的接口
// 我们在这里直接使用 axios 请求调用即可
export const getNpmInfo = async (npmName: string) => { 
 const npmUrl = "https://registry.npmjs.org/" + npmName;  
let res = {}; 
 try {
    res = await axios.get(npmUrl);  
   } catch (err) {
    log.error(err as string);  
}  
return res;
};

npm 包信息中包含了该包的最新版本,我们在这里直接引用即可。

export const getNpmLatestVersion = async (npmName: string) => { 
 // data['dist-tags'].latest 为最新版本号 
 const { data } = (await getNpmInfo(npmName)) as AxiosResponse;  
 return data["dist-tags"].latest;};

然后对比版本号版本,判断是否需要更新,如需更新进行提示。

export const checkVersion = async (name: string, curVersion: string) => {
  const latestVersion = await getNpmLatestVersion(name);
  const need = lodash.gt(latestVersion, curVersion);
  if (need) {
    log.info(
      `检测到 xqy 最新版:${chalk.blueBright(
        latestVersion
      )} 当前版本:${chalk.blueBright(curVersion)} ~`
    );
    log.info(`可使用 ${chalk.yellow("pnpm")} install xqy-cli@latest 更新 ~`);
  }
  return need;
};

然后我们将这个判断更新的方法添加到 create 方法中。

export default async function create(prjName?: string) {  
// ...  
await checkVersion(name, version); // 检测版本更新  
// ...
}

当我们发布新的版本,用户可以第一时间看到。

如何调用

  • 我们已经完成了核心的代码逻辑,现在想要使用命令行去调用我们编写好的逻辑,我们可以先自己在本地执行测试,然后将其上传到 npm 就可以供他人使用了。

本地调试

  • 我们之前已经配置好了 rollup 打包的脚本,接下来就可以执行 npm run build,打包后的代码会输出到 dist/index.js 中。

  • 我们可以使用 node 在本地执行,先测试一下我们编写好的 create 命令。

node .\dist\index.js create

不出意外是可以看到我们写好的交互逻辑,如果有报错,大家可以根据对应的问题查询下

发布 npm 包

  • 本地调试没有问题后我们就可以将其发布在 npm 上。

  • npm 账号注册、登录等基础操作,这里就不过多赘述了,主要讲一下如何让发布的包能以 xqy 作为命令名调用。

  • 需要把代码提交到 github 上 如果没有 github 账号也需要 注册一下 ,具体怎么注册 登录不详细说了

需要我们修改一下 package.json 文件,下面是一些必要的配置,都加上了注释,我们需要重点关注 bin 这一项。

  • bin 中的配置是一个对象,需要有 "key" 和 "value"。

    • key 会被放置在 node_modules 的 .bin 目录中,value 是 key 对应需要执行的文件。

    • 我们使用 npx xqy 就会调用我们的 bin/index.js。

    • 当我们全局安装对应包的时候会放在全局的 node_modules 的 .bin 目录中,相当于添加了系统环境变量,这样我们就可以直接在终端中调用。

{
  "name": "xqy-cli", // 包名称
  "version": "x.x.x", // 包版本
  "description": "xqy-cli脚手架", // 包描述
  "main": "dist/index.js", // 库入口文件
  "keywords": [
    // 包查询关键词  提升SEO
    "Vite-Vue4-TypeScript-template"
  ],
  "files": [
    // npm 包需要上传的文件
    "dist",
    "bin",
    "README.md"
  ],
  "author": {
    // 作者信息
    "name": "xqy"
  },
  "bin": {
    "xqy": "bin/index.js" // npm 会在 .bin 目录中配置 xqy 执行 bin/index.js
  },
  "devDependencies": {
    "@inquirer/prompts": "^3.3.0",
    "@rollup/plugin-commonjs": "^25.0.3",
    "@rollup/plugin-json": "^6.0.1",
    "@rollup/plugin-node-resolve": "^15.1.0",
    "@rollup/plugin-terser": "^0.4.3",
    "@types/fs-extra": "^11.0.2",
    "@types/lodash": "^4.14.202",
    "@types/node": "^20.10.4",
    "axios": "^1.6.2",
    "chalk": "^4.1.2",
    "commander": "^11.1.0",
    "figlet": "^1.7.0",
    "fs-extra": "^11.1.1",
    "lodash": "^4.17.21",
    "log-symbols": "4.1.0",
    "ora": "5",
    "progress-estimator": "^0.3.1",
    "rollup": "^4.6.1",
    "rollup-plugin-node-externals": "^5.1.2",
    "rollup-plugin-typescript2": "^0.36.0",
    "simple-git": "^3.21.0",
    "tslib": "^2.6.2",
    "typescript": "^5.3.3"
  }
}

编写 bin/index.js

#!/usr/bin/env node
require('../dist'); // 执行我们打包好的 dist/index.js 文件

需要在第一行加入#!/usr/bin/env node,/usr/bin/env 就是告诉系统可以在 PATH 目录中查找,#!/usr/bin/env node 就是解决了不同的用户 node 路径不同的问题,可以让系统动态的去查找 node 来执行你的脚本文件。

开发发包

检查 npm 源,如果是淘宝源,则需要改回 npm 源

// 查看npm镜像源地址
npm config get registry

// 切换npm镜像源

// 设置npm默认源
npm config set registry https://registry.npmjs.org/
// 设置npm镜像源为淘宝镜像
npm config set registry https://registry.npmmirror.com/

npm、yarn 和 pnpm 淘宝镜像

// 设置
npm config set registry 
https://registry.npmmirror.com/yarn config set registry 
https://registry.npmmirror.com/pnpm config set registry 
https://registry.npmmirror.com/
// 查看
npm config get registry
yarn config get registry
pnpm config get registry

在终端中切换到项目目录下,运行登陆命令,之后按照终端提示输入用户名、密码等信息即可

// 登陆
npm login

// 控制台会提示输入相关信息
Log in on https://registry.npmjs.org/
Username:  // 用户名
Password: // 密码
Email: (this IS public) // 邮箱
Enter one-time password: // 如果之前做过 双因素身份验证 (2FA),需要生成一次性密钥
Logged in as xxx on https://registry.npmjs.org/.

运行发布命令

// 发布命令
npm publish

发布成功后,就可以登陆 npm 网站,查看发布包的情况了

标签:npm,node,log,plugin,create,rollup,开发,手脚
From: https://blog.csdn.net/liu198273/article/details/139773923

相关文章

  • C语言开发日志,问题记录(长期更新版本)
    一、程序存储与占用1.编译后内存分配编译后的Code(代码),RO-data(只读,譬如const),RW-data(读写,初始化非0的全局变量),存储在ROM(flash)中,ZI-data(初始化为0或者未初始化的变量),运行时ROM占用是Code+RO-data+RW-data运行时RAM占用是RO-data+RW-data+ZI-data;RW和ZI会被......
  • Libgdx游戏开发(5)——碰撞反弹的简单实践
    原文:Libgdx游戏开发(5)——碰撞反弹的简单实践-Stars-One的杂货小窝本篇简单以一个小球运动,一步步实现碰撞反弹的效果本文代码示例以kotlin为主,且需要有一定的Libgdx入门基础注:下面动态图片看着有些卡顿,是录制的问题,实际上运行时很流畅的水平滚动简单起见,我们通......
  • 开发扫码点餐小程序,让用餐更方便!
    随着餐饮行业的数字化转型,扫码点餐小程序逐渐成为餐厅提升服务效率和顾客体验的重要工具。它不仅能够简化点单流程,还能帮助餐厅实现智能化管理。那么怎么搭建一个扫码点餐小程序呢?作为餐饮商家来说,把握小程序的流量,小程序无需下载与关注,使用十分便利,投入开发后即可投入使用,对......
  • HarmonyOS开发从入门到跨平台系列:深入了解鸿蒙项目的核心结构
    前言深圳已经发了2024年关于鸿蒙软件生态的规划,如果目标达到,过几年很有可能出现iOSAndroid鸿蒙三足鼎立的情况,因此我们客户端程序员有必要储备一下鸿蒙知识。接下来我将分几篇文章介绍鸿蒙开发的入门、实战和跨平台相关知识,今天这篇文章作为开篇,主要介绍一下鸿蒙开......
  • 【鸿蒙开发教程】详解HarmonyOS Next UI开发技巧
    前言根据研究机构CounterpointResearch发布的最新数据,2024年第一季度,鸿蒙OS份额由去年一季度的8%上涨至17%,iOS份额则从20%下降至16%。这意味着,华为鸿蒙OS在中国市场的份额超越苹果iOS,已成中国第二大操作系统随着鸿蒙市场份额的不断提升,相应的岗位也会迎来一个爆发式的......
  • 【鸿蒙实战开发案例】如何构建一个HarmonyOS应用工程
    前言随着春节假期结束各行各业复产复工,一年一度的春招也持续火热起来。最近,有招聘平台发布了《2024年春招市场行情周报(第一期)》。总体来说今年的就业市场还是人才饱和的状态,竞争会比较激烈。但是,通过报告我们也能看到让人眼前一亮的信息,比如华为鸿蒙系统对应的人才市场就......
  • 【鸿蒙开发教程】HarmonyOS NEXT对于游戏类App的基础支持
    前言根据研究机构CounterpointResearch发布的最新数据,2024年第一季度,鸿蒙OS份额由去年一季度的8%上涨至17%,iOS份额则从20%下降至16%。这意味着,华为鸿蒙OS在中国市场的份额超越苹果iOS,已成中国第二大操作系统随着鸿蒙市场份额的不断提升,相应的岗位也会迎来一个爆发式的......
  • 【鸿蒙教程】华为HarmonyOS NEXT 应用开发 实现日常提醒应用
    前言根据研究机构CounterpointResearch发布的最新数据,2024年第一季度,鸿蒙OS份额由去年一季度的8%上涨至17%,iOS份额则从20%下降至16%。这意味着,华为鸿蒙OS在中国市场的份额超越苹果iOS,已成中国第二大操作系统。随着鸿蒙市场份额的不断提升,相应的岗位也会迎来一个爆发式......
  • 从局部到全局:产品开发视角的转变与系统优化策略
    一、研发背景在科技产品开发领域,每一种产品都存在着多元化的开发方案可供选择,这要求开发者不断拓展视野,进行横向对比学习,以期找到最贴合市场需求、最具竞争优势的解决方案。以往,我们的研发团队一直立足于稳固而成熟的硬件平台之上,倾注心力专攻电机控制软件开发,积累了丰富的专......
  • ARM32开发——GD32F4中断向量查询
    ......