设计自己的脚手架

news2024/11/27 0:33:57

如何设计自己的脚手架

      • 前言
      • 前置知识
      • 如何搭建一个脚手架
      • 搭建自己的脚手架
        • 初始化项目
          • 安装依赖
          • packagejson 配置
          • lint 和typescript配置
        • 加入bin字段调试
        • npm link调试
        • 核心代码实现
          • 获取所有命令
          • create实现
        • 美化项目
          • 添加logo
        • 发包
        • 源码仓库

前言

​ 在工程中,不仅是软件工程,在建筑行业,我们也经常能看到脚手架的概念。脚手架(又称为CLI,全称command-line interface),我理解是一种快速构建项目的工具,它主要提供了项目的基础结构和一些常用的配置,避免了从头开始搭建项目的繁琐工作。通过使用脚手架,开发者可以更加高效地创建和启动项目,并且保持项目结构的一致性,同时还能提供一些常用的功能和工具,例如自动化构建、代码生成、测试等。脚手架可以根据特定的需求和技术栈来定制。平时的学习和工作中我们经常会用到各种各样的脚手架,例如vue-cliCreate React Appcreate-vite等等,使用这些工具可以大大提高我们创建项目的速度。

​ 那当我们遇到重复的构建工作的时候是不是也可以考虑自己搭建一个脚手架呢。正好最近在使用vitepress搭建自己的小册网站记录自己的学习过程。由于前端涉及到的工具很多,需要很多个小册,那么我是不是可以通过搭建自己的脚手架来完成这样重复的小册构建工作呢?于是就有了这篇文章

​ 学习本文,你能收获:

  • 🌟 掌握开发脚手架的全流程
  • 🌟 学会命令行开发常用的多种第三方模块
  • 🌟 拥有一个属于自己的脚手架

前置知识

​ 在看了常见的脚手架工具的源码之后,发现大部分脚手架的构建都离不开以下工具,先对这些工具做个介绍,以便有初步的了解。这些库不一定都会用到,可以按需选择。

  • commander:最常用的命令行工具,可以通过内置的api很方便的读取命令行的命令。
    • minimist: 命令行参数解析工具,比commander要简单
  • inquirer: 交互式命令工具,给用户提供一个提问流方式,通过promise的方式返回用户选择。可以实现定制化功能
  • prompts: 交互式命令工具,inquirer的轻量级版本
  • chalk: 颜色插件,用来修改命令行输出样式,通过颜色区分info、error日志。对重要信息用不同颜色进行区分
  • ora: 用于显示加载中的效果,类似于前端页面的loading效果,像下载模版这种耗时的操作,有了loading效果,可以提示用户正在进行中,请耐心等待。在我们平时使用命令行的时候经常看到这种效果
  • fs-extra: node fs文件系统模块的增强版,可以方便我们操作本地文件。对所有的异步操作都提供了promise支持
  • cross-spawn: 是一个用于跨平台执行命令的 Node.js 模块。它解决了在不同操作系统上执行命令时可能会遇到的一些兼容性问题。
  • figlet: 可以将 text 文本转化成生成基于 ASCII 的艺术字
  • execa: 执行终端命令
  • handlebars: 可以方便执行模版替换,用用户的输入替换掉内置模版

如何搭建一个脚手架

​ 我们以vue-cli为例,看下脚手架的一般功能

  1. 提供不同的指令,执行不同的事情

    例如 --version --help --create等等

  2. 交互式用户选择

    我们的脚手架可能会有多种选择,我们需要向用户提供不同的选择。

    image-20230714162425036
  3. 用户选择完毕后,根据用户选择生成用户需求的项目文件

通过以上分析,我们可以看出脚手架的基本范式:通过命令行与用户交互的选择来生成对应的文件。

image-20230714162938722

搭建自己的脚手架

知道了大概流程之后我们就可以开始搭建自己的脚手架了

项目使用 typescript + node搭建,主要目录结构如下

zy-cli
├─ .gitignore
├─ README.md
├─ build // 打包后文件夹
├─ project-template // 初始化项目模版
├─ bin
|  ├─ bin.js // 生产环境执行文件入口
|  ├─ bin-dev.js // 本底调试执行文件入口,主要是能够动态编译ts
├─ package.json // 配置文件,具体见下
├─ src
│  └─ const // 常量包
│     ├─ index.ts
│  ├─ commands // 命令文件夹
│  │  ├─ create.ts // create命令
│  │  ├─ config.ts // config命令
│  │  ├─ package.ts // package命令
│  │  └─ utils // 公共函数
│  ├─ index.ts // 入口文件
│  └─ helpers // 公共第三方包
│     ├─ index.ts
│     ├─ logger.ts // 控制台颜色输出
│     └─ spinner.ts // 控制台loading
│  └─ utils // 工具类
│     ├─ index.ts
├─ tsconfig.json // TypeScript配置文件
└─ tslint.json // tslint配置文件

初始化项目

安装依赖

1、npm init 初始化package.json

2、npm i typescript ts-node eslint rimraf -D 安装开发依赖

3、npm i typescript chalk commander execa fs-extra globby handlebars inquirer ora pacote figlet 安装生产依赖

packagejson 配置

scripts配置clear、build、publish、lint命令

完成的package.json配置文件如下

{
  "name": "xzy-cli",
  "version": "1.0.0",
  "description": "xzy-cli",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "clear": "rimraf build",
    "build": "npm run clear && tsc",
    "publish": "npm run build && npm publish",
    "lint": "tslint ./src/**/*.ts --fix",
    "lini-fix": "tslint ./src/**/*.ts --fix"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/xzy0625/xzy-cli.git"
  },
  "bin": {
    "xzy": "./bin/bin.js",
    "xzy-dev": "./bin/bin-dev.js"
  },
  "keywords": [
    "cli",
    "node",
    "vitepress",
    "typescript"
  ],
  "author": "csuxxzy",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/xzy0625/xzy-cli/issues"
  },
  "homepage": "https://github.com/xzy0625/xzy-cli#readme",
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "eslint": "^8.45.0",
    "rimraf": "^5.0.1",
    "ts-node": "^10.9.1",
    "typescript": "^5.1.6"
  },
  "dependencies": {
    "chalk": "^4.1.2",
    "commander": "^11.0.0",
    "execa": "^7.1.1",
    "figlet": "^1.6.0",
    "fs-extra": "^11.1.1",
    "globby": "^11.0.4",
    "handlebars": "^4.7.7",
    "inquirer": "^9.2.8",
    "ora": "^6.3.1",
    "pacote": "^15.2.0"
  }
}

需关注bin字段和files字段。

  • bin字段: bin字段用于指定可执行文件的路径,是在全局或局部安装模块时可以直接运行的命令。bin字段的值可以是一个字符串或一个对象。如果bin字段的值是一个字符串,那么它表示一个可执行文件的路径。例如:
{
  "name": "my-package",
  "version": "1.0.0",
  "bin": "./dist/my-command.js"
}

​ 在安装该模块后,可以直接在命令行中运行my-command来执行./dist/my-command.js指定的文件。

​ 如果bin字段的值是一个对象,那么它的键是命令的名字,值是对应的可执行文件的路径。例如:

{
  "name": "my-package",
  "version": "1.0.0",
  "bin": {
    "my-command": "./dist/my-command.js",
    "another-command": "./dist/another-command.js"
  }
}

​ 在安装该模块后,可以直接在命令行中运行my-commandanother-command来分别执行./dist/my-command.js和./dist/another-command.js`指定的文件。

  • files字段: 即npm的白名单,也就是说发包后需要包括哪些文件,不配置的话默认发布全部文件,这样很容易暴漏我们的源码,也会导致npm的包体积过大。所以这里我们配置了"files": [ "build", "bin.js" ],发布到npm的时候只会包含build目录和bin.js文件
lint 和typescript配置
  • typescript配置

    根目录下执行tsc --init,更改配置如下

    {
      "compilerOptions": {
        "target": "es2017",
        "module": "commonjs",
        "moduleResolution": "node",
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "allowSyntheticDefaultImports": true,
        "alwaysStrict": true,
        "sourceMap": false,
        "noEmit": false,
        "noEmitHelpers": false,
        "importHelpers": false,
        "strictNullChecks": false,
        "allowUnreachableCode": true,
        "lib": ["es6"],
        "typeRoots": ["./node_modules/@types"],
        "outDir": "./build", // 重定向输出目录
        "rootDir": "./src" // 仅用来控制输出的目录结构
      },
      "exclude": [ // 不参与打包的目录
        "node_modules",
        "build",
      ],
      "esModuleInterop": true,
      "allowSyntheticDefaultImports": true,
      "compileOnSave": false,
      "buildOnSave": false
    }
    
    
  • eslint

    根目录下执行 eslint --init --format json,完整配置如下

    {
        "env": {
            "browser": true,
            "es2021": true
        },
        "extends": [
            "eslint:recommended",
            "plugin:@typescript-eslint/recommended"
        ],
        "parser": "@typescript-eslint/parser",
        "parserOptions": {
            "ecmaVersion": "latest",
            "sourceType": "module"
        },
        "plugins": [
            "@typescript-eslint"
        ],
        "rules": {
            "no-console": "warn"
        }
    }
    

    加入bin字段调试

    package.json中加入如下bin字段

      "bin": {
        "xzy": "./bin.js",
        "xzy-dev": "./bin-dev.js"
      }
    

    新建bin目录,加入bin.jsbin-dev.js(如有lint报错,在.eslintingnore文件中加入bin目录就好)

    mkdir bin && cd ./bin && touch bin.js bin-dev.js
    

    bin-dev.js

    #!/usr/bin/env node   
    require('ts-node/register')
    require('../src')
    

    bin.js

    #!/usr/bin/env node   
    require('../build')
    

    该字段是定义命令名(也就是你脚手架的名字)和关联的执行文件,行首加入一行 #!/usr/bin/env node 指定当前脚本由node.js进行解析

    由于我们使用的是typescript进行开发,如果不用ts动态编译功能。所以在bin-dev中使用了ts-node。这样不用每次改代码都build一次。后续发包之后使用bin.js,指向打包后的build目录

npm link调试

根目录下执行npm link,将我们的包link到全局的node中,这样就可以使用包中的bin命令了。 image-20230717114319819

核心代码实现

获取所有命令

通过文件即功能的方式统一获取commands目录下的所有命令,同时注册到commander中去

// 获取所有命令
  const commandsPath = await getPathList("./commands/*.*s");

  // 注册命令
  commandsPath.forEach((commandPath) => {
    const commandObj = require(`./${commandPath}`);
    const { command, description, optionList, action } = commandObj.default;
    const curp = program
      .command(command)
      .description(description)
      .action(action);

    optionList &&
      optionList.map((option: [string]) => {
        curp.option(...option);
      });
  });
create实现
  1. 检查目录是否存在

    // 检查是否已经存在相同名字工程
    export const checkProjectExist = async (targetDir) => {
      if (fs.existsSync(targetDir)) {
        const answer = await inquirer.prompt({
          type: "list",
          name: "checkExist",
          message: `\n仓库路径${targetDir}已存在同名文件,请选择是否需要覆盖原路径(删除原文件后新建)`,
          choices: ["是", "否"],
        });
        if (answer.checkExist === "是") {
          logger.warn(`已删除${targetDir}...`);
          fs.removeSync(targetDir);
          return false;
        } else {
          logger.info("您已取消创建");
          return true;
        }
      }
      return false;
    };
    
  2. 获取用户输入

    // 获取用户输入
    export const getQuestions = async (projectName): Promise<IQuestion> => {
      return await inquirer.prompt([
        {
          type: "input",
          name: "name",
          message: `package name: (${projectName})`,
          default: projectName,
        },
        {
          type: "input",
          name: "description",
          message: "description",
        },
        {
          type: "input",
          name: "author",
          message: "author",
        },
        {
          type: "input",
          name: "git",
          message: "git仓库",
        },
      ]);
    };
    
  3. 复制项目

    export const cloneProject = async (
      targetDir: string,
      projectName: string,
      template: ICmdArgs["template"],
      projectInfo: IQuestion
    ) => {
      spinner.start(`开始创建目标文件 ${chalk.cyan(targetDir)}`);
      // 复制'project-template'到目标路径下创建工程
      await fs.copy(
        path.join(__dirname, "..", "..", `project_template/${template}`),
        targetDir
      );
    
      // handlebars模版引擎解析用户输入的信息存在package.json
      const packagePath = `${targetDir}/package.json`;
      const configPath = `${targetDir}/docs/.vitepress/config.js`;
    
      // 读取文件内容
      const packageContent = fs.readFileSync(packagePath, "utf-8");
      const configContent = fs.readFileSync(configPath, "utf-8");
    
      // 覆盖模版内容
      const packageResult = handlebars.compile(packageContent)(projectInfo);
      const configResult = handlebars.compile(configContent)(projectInfo);
    
      // 写入新内容
      fs.writeFileSync(packagePath, packageResult);
      fs.writeFileSync(configPath, configResult);
    
      logger.info("开始安装项目所需依赖");
    
      try {
        // 新建工程装包
        execa.commandSync("yarn", {
          stdio: "inherit",
          cwd: targetDir,
        });
      } catch (error) {
        // 报错就用npm试下
        execa.commandSync("npm install", {
          stdio: "inherit",
          cwd: targetDir,
        });
      }
    
      if (projectInfo.git) {
        logger.info("开始关联项目到git");
        // 关联git
        gitCmds(projectInfo.git).forEach((cmd) =>
          execa.commandSync(cmd, {
            stdio: "inherit",
            cwd: targetDir,
          })
        );
      }
    
      spinner.succeed(
        `目标文件创建完成 ${chalk.yellow(projectName)}\n👉 输入以下命令开始创作吧!:`
      );
      logger.info(`$ cd ${projectName}\n$ yarn dev\n`);
    };
    

美化项目

添加logo

当使用--help时在最后添加logo展示

console.log(
  "\r\n" +
  figlet.textSync("xzy-cli", {
    font: "3D-ASCII",
    horizontalLayout: "default",
    verticalLayout: "default",
    width: 80,
    whitespaceBreak: true,
  })
);
image-20230717171158556

发包

到此,脚手架基本的搭建与开发就完成了,发布到npm

  • 1、npm run lint 校验代码,毕竟都发包了,避免出现问题
  • 2、npm run build typescript打包
  • 3、npm publish 发布到npm 发包完成后,安装检查

具体可以参考我的另一片文章:npm发布包详细流程+常见错误

源码仓库

github 地址: @csuxzy/xzy-cli

npm 仓库地址: @csuxzy/xzy-cli

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/764445.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

车载测试:CANoe中环境变量和系统变量的区别

目录 环境变量和系统变量相同点&#xff1a; 环境变量和系统变量不同点&#xff1a; 环境变量和系统变量相同点&#xff1a; 都可以作为ECU、面板和CAPL程序相连接的媒介。例如&#xff0c;在CAPL程序中&#xff0c;通过改变或监控某一环境变量的值可以触发特定的动作&#x…

Java处理doc类型的Word文档转换成html(按顺序保留格式+图片)

最新有个新需求&#xff0c;就是doc文档转换html内容倒不是很难&#xff0c;给大家分享一下&#xff0c;总体思路就是按doc转html的思路来走&#xff0c;唯一缺点是不会自动转换图片&#xff0c;图片是要手动转成base64&#xff0c;默认是有html、body、head、meta等等标签&…

基于深度学习的高精度农作物机器与行人目标检测系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度农作物机器与行人目标检测系统可用于日常生活中或野外来检测与定位农作物机器与行人目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的农作物机器与行人目标检测识别&#xff0c;另外支持结果可视化与图片或视频检测结果…

查询自己档案存在哪里

查询自己档案存在哪里 百度搜索“全国人力资源和社会保障政务服务平台” 或者点击下方的官网链接登陆后点击“个人服务”&#xff0c;选择“档案存档情况查看”就可以看到自己的存档情况了。 全国人力资源和社会保障政务服务平台 http://zwfw.mohrss.gov.cn/portal/index

【Linux】生产者消费者模型 -- RingQueue

文章目录 1. 生产者消费者模型的理解1.1 生产者消费者模型的概念1.2 生产者消费者模型的特点1.3 生产者消费者模型的优点 2. 基于BlockQueue的生产者消费者模型 1. 生产者消费者模型的理解 1.1 生产者消费者模型的概念 生产者消费者模型就是通过一个容器来解决生产者和消费者的…

从iPhone恢复已删除音视频的5种主要方法

“我需要从iPhone恢复已删除的音视频。我真的很喜欢我的音视频文件。我玩了很多封面&#xff0c;并检查听我可以改进的地方和不可以改进的地方。 iPhone是我完成这项任务的首选手机&#xff0c;因为我喜欢保持非常简单&#xff0c;我喜欢听我的iPhone。但是&#xff0c;我确实删…

4.Cesium中实体Entity的增删改查及性能优化(超详细)

前言 Cesium 作为一个功能强大的 WebGL 三维地球仪库,内置了丰富的三维地图展示能力。在 Cesium 中,我们可以通过 Entity(实体)在三维场景中添加和控制各种三维对象,如点、线、面、模型等。本文将介绍 Cesium 中实体的增删改查操作。 概述 添加到场景中的实体都保存在 viewer.…

【AI绘画】Stable-Diffusion-Webui本地部署-简单绘画图片

这里写目录标题 前言一、Stable Diffusion是什么&#xff1f;二、安装stable-diffusion-webui1. python安装2. 下载模型3. 开始安装&#xff1a;4. 汉化&#xff1a;5. 模型使用&#xff1a;6. 下载新模型&#xff1a;7. 基础玩法 三、总结 前言 本文将借助stable-diffusion-w…

【idea】的一些使用指南

一、serializable自动生成id 1.打开File菜单&#xff0c;选择Settings选项 2.打开Editor->Inspections 3.在右边的搜索框中输入serialVersionUID关键字&#xff0c;出现以下选项&#xff0c;勾选"Serializable class without serialVersionUID"&#xff0c;然后别…

攻不下dfs不参加比赛(十)

标题 为什么练dfs题目总结为什么练dfs 相信学过数据结构的朋友都知道dfs(深度优先搜索)是里面相当重要的一种搜索算法,可能直接说大家感受不到有条件的大家可以去看看一些算法比赛。这些比赛中每一届或多或少都会牵扯到dfs,可能提到dfs大家都知道但是我们为了避免眼高手低有…

非监督学习-K均值聚类-知识点扫盲

前言 在实际工作中&#xff0c;我们经常会遇到这样一类问题&#xff1a;给机器输入大量的特征数据&#xff0c;并期望机器通过学习找到数据中存在的某种共性特征或者结构&#xff0c;亦或是数据之间存在的某种关联。 例如&#xff0c;视频网站根据用户的观看行为对用户进行分组…

【MongoDB】SpringBoot整合MongoDB

【MongoDB】SpringBoot整合MongoDB 文章目录 【MongoDB】SpringBoot整合MongoDB0. 准备工作1. 集合操作1.1 创建集合1.2 删除集合 2. 相关注解3. 文档操作3.1 添加文档3.2 批量添加文档3.3 查询文档3.3.1 查询所有文档3.3.2 根据id查询3.3.3 等值查询3.3.4 范围查询3.3.5 and查…

8、gateway使用和原理

一、什么是Spring Cloud Gateway 1、网关简介 网关作为流量的入口&#xff0c;常用的功能包括路由转发&#xff0c;权限校验&#xff0c;限流等。 2、Gateway简介 Spring Cloud Gateway 是Spring Cloud官方推出的第二代网关框架&#xff0c;定位于取代 Netflix Zuul。相比 …

【iOS】编译与链接过程

前言 计算机语言分为&#xff1a;机器语言、汇编语言和高级语言。 高级语言又能分为&#xff1a;编辑语言、解释语言。 解释语言 解释语言编写的程序在每次运行时都需要通过解释器对程序进行动态解释和执行&#xff0c;即解释一条代码&#xff0c;执行一条代码。 优点&…

ADC 的初识

ADC介绍 Q: ADC是什么&#xff1f; A: 全称&#xff1a;Analog-to-Digital Converter&#xff0c;指模拟/数字转换器 ADC的性能指标 量程&#xff1a;能测量的电压范围分辨率&#xff1a;ADC能辨别的最小模拟量&#xff0c;通常以输出二进制数的位数表示&#xff0c;比如&am…

一百三十、海豚调度器——用DolphinScheduler定时调度HiveSQL任务

一、目标 用海豚调度器对Hive数仓各层数据库的SQL任务进行定时调度。比如&#xff0c;DWD层脱敏清洗表的动态插入数据、DWS层指标表的动态插入数据 二、工具版本 1、海豚调度器&#xff1a;apache-dolphinscheduler-2.0.5-bin.tar.gz 2、Hive&#xff1a;apache-hive-3.1.2…

随手笔记——Sophus的基本使用方法

随手笔记——Sophus的基本使用方法 说明CMakeLists.txt补充&#xff1a;关于 ADD_SUBDIRECTORY 的使用使用CMakeLists执行顺序 源代码 说明 Sophus 库支持SO(3) 和SE(3)&#xff0c;此外还含有二维运动 SO(2)&#xff0c;SE(2) 以及相似变换 Sim(3) 的内容。它是直接在 Eigen …

数据结构--图的存储邻接矩阵法

数据结构–图的存储邻接矩阵法 无向图&#xff1a; 有向图&#xff1a; #define MaxVerTexNum 100 //顶点数目的最大值 typedef struct {char vex[MaxVerTexNum]; //顶点表int Edge[MaxVerTexNum][MaxVerTexNum]; //邻接矩阵&#xff0c;边表int vexnum, arcnum; //图的当前顶…

最新 robot framework安装

相信大家对robot framework并不陌生&#xff0c;它是一个基于Python语言&#xff0c;用于验收测试和验收测试驱动开发&#xff08;ATDD&#xff09;的通用测试自动化框架&#xff0c;提供了一套特定的语法&#xff0c;并且有非常丰富的测试库。 ### [Python](https://www.pytho…

gogs的自定义配置

在 GOGS 下载并安装后&#xff0c;在程序目录下建立一个custom/conf/app.ini的配置文件&#xff0c;内容如下&#xff1a; APP_NAME Gogs # APP名字 RUN_USER git # 启动用户&#xff0c;设置后只能以此账号启动gogs RUN_MODE prod[database] DB_TYPE mysql HOST 1…