设计自己的脚手架

这篇具有很好参考价值的文章主要介绍了设计自己的脚手架。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

​ 在工程中,不仅是软件工程,在建筑行业,我们也经常能看到脚手架的概念。脚手架(又称为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. 交互式用户选择

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

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

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

搭建自己的脚手架

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

项目使用 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命令了。

核心代码实现
获取所有命令

通过文件即功能的方式统一获取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,
  })
);
发包

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

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

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

源码仓库

github 地址: @csuxzy/xzy-cli

npm 仓库地址: @csuxzy/xzy-cli文章来源地址https://www.toymoban.com/news/detail-576051.html

到了这里,关于设计自己的脚手架的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 前端架构: 脚手架命令行交互核心实现之inquirer和readline的应用教程

    命令行交互核心实现 核心目标:实现命令行行交互,如List 命令行的交互呢比命令行的渲难度要更大,因为它涉及的技术点会会更多 它涉及以下技术点 键盘输入的一个监听 (这里通过 readline 来实现) 计算命令行窗口的尺寸 清屏 光标的移动 输出流的静默 (我们输出的内容, 不

    2024年04月23日
    浏览(43)
  • 前端架构: 实现脚手架终端UI样式之ANSI escape code, Chalk, Ora介绍

    在脚手架当中实现命令行的UI显示 1 )概述 在命令行中,如果想实现除传统的常规文本以外的内容 比如想对字体进行加粗斜体下划线,包括对它改变颜色改变前景色改变后景色等等 需要借助一个叫做 ANSI escape code 这样的一个概念 它其实是一个标准,它可以用来控制光标的位

    2024年02月22日
    浏览(37)
  • [npm]脚手架本地全局安装1

    该文章是你的脚手架已经开发完成的前提下,你想要本地全局安装该脚手架,便于本地使用脚手架的命令的情况 如果本地开发的项目是个脚手架,只是个人使用,也并不需要上传到 npm 或者私库,如何安装本地的项目到包的全局位置,以便全局使用该脚手架? 可以在项目的根

    2024年02月07日
    浏览(54)
  • 安装Nodejs、NPM、Vue脚手架详细教程

    查看自己电脑是否安装nodejs 我这里已经下载过了,没有下载过的会提示该命令不存在 可以到官网下载一下 https://nodejs.org/en/download/ 不要安装在中文路径下 如果你安装了nodejs–默认会安装NPM. 使用vue --version 查看当前是否安装vue脚手架 没有安装会提示不是内部命令 在命令行输

    2024年02月16日
    浏览(59)
  • 从 0 到 1 搭建自己的脚手架(java 后端)

    脚手架是一种基础设施工具,用于快速生成项目的框架代码和文件结构。它是一种标准化的开发工具,使开发人员能够在项目的早期阶段快速搭建出一个具备基本功能和结构的系统。 主流的微服务架构体系下很多公司会将原有的单体架构或者繁重的微服务进行拆分。这个时候

    2024年02月08日
    浏览(46)
  • 实现一个简单的前端脚手架

    前端脚手架概念 实现前端脚手架 随着前端工程化的概念越来越深入人心,脚手架应运而生。简单来说,「前端脚手架」就是指通过选择几个选项快速搭建项目基础代码的工具 前端脚手架可帮我们做什么? 可以帮助我们快速生成项目的基础代码 脚手架工具的项目模板经过了

    2024年02月03日
    浏览(47)
  • 和chatgpt学架构01-搭建项目脚手架

    今年3月份以来,chatgpt就热度不减。有了这种聊天机器人,就可以很方便的帮助我们提高。无论是我们独立创业还是做项目外包,拥有一套自己可以把握的脚手架还是必备的能力。 过去如果靠自己摸索,组装这么一套脚手架还是费事费力的。一个是涉及技术比较多,既要架构

    2024年02月16日
    浏览(54)
  • 前端如何搭建脚手架并在本地运行

    在开始搭建前,确保本机安装了node,为避免奇奇怪怪的问题 建议node版本16以上 使用过vue ,react,angular的同学都知道 ,应该对脚手架有一定的理解,比如vue-cli的 vue create myApp ,其中vue 就是vue-cli声明的一个命令,下来我们创建一个项目并声明自己的命令。 创建一个空的文件夹

    2024年02月20日
    浏览(45)
  • Vue的架构以及基于脚手架环境开发vue项目

    M:model 模型层(业务逻辑层),主要包含 JS 代码,用于管理业务逻辑的实现。 V:View 视图层,主要包括 HTML / CSS代码,用于管理 UI 的展示。 VM:viewModel (视图模型层),用于将data与视图层的 DOM 进行动态绑定。 基于脚手架环境开发Vue项目 制作web 从小作坊状态转向工程化开

    2024年02月01日
    浏览(54)
  • 【前端】Vue2 脚手架模块化开发 -快速入门

    🎄欢迎来到@边境矢梦°的csdn博文🎄  🎄本文主要梳理Vue2 脚手架模块化开发 🎄 🌈我是边境矢梦°,一个正在为秋招和算法竞赛做准备的学生🌈 🎆喜欢的朋友可以关注一下 🫰🫰🫰 ,下次更新不迷路🎆 Ps: 月亮越亮说明知识点越重要 (重要性或者难度越大)🌑🌒🌓🌔🌕

    2024年02月10日
    浏览(82)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包