从零构建自己的脚手架

news2024/9/23 15:25:37

从零构建自己的脚手架

简介

什么是CLI

CLI 全称是 Command Line Interface,是一类通过命令行交互的终端工具。日常工作中常用的脚手架有 vue-cli、create-react-app、angular-cli 等,都是通过简单的初始化命令,完成内容的快速构建。

为什么需要CLI

CLI 可以帮助我们更高效地操作计算机系统,我们可以将有规律可循的、重复的、繁琐的、模板化的工作,集成到CLI工具中。

  • GUI:更侧重易用性,用户通过点击图形界面,完成相关配置

  • CLI:更侧重操作效率,通过命令组合自动化操作、批量操作等

脚手架的简单雏形

脚手架就是在启动的时候询问一些简单的问题,并且通过用户回答的结果去渲染对应的模板文件,例如我们在使用 vue-cli创建一个 vue 项目时的时候 👇

step1:运行创建命令

vue create hello-world

step2:询问用户问题

image

image

step3:生成符合用户需求的项目文件

image

参考上面的流程我们可以自己来 搭建一个简单的脚手架雏形。

1. 在命令行启动 cli

目标: 实现在命令行执行 my-node-cli 来启动我们的脚手架

1.1 新建项目目录 my-node-cli

mkdir my-node-cli 
cd my-node-cli 
npm init

1.2 新建程序入口文件 cli.js

$ touch cli.js # 新建 cli.js 文件

在 package.json 文件中指定入口文件为 cli.js 👇

{
  "name": "my-node-cli",
  "version": "1.0.0",
  "description": "",
  "main": "cli.js",
  "bin": "cli.js", // 手动添加入口文件为 cli.js
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

此时项目目录结构:

my-node-cli      
├─ cli.js        
└─ package.json  

打开 cli.js 进行编辑

文件头部必须有 #!/usr/bin/env node,这是头部声明代码,用来告诉系统使用 NodeJS 执行脚本;如不声明,默认按shell去解析执行。

#! /usr/bin/env node

console.log('my-node-cli working~')

1.3 npm link 链接到全局

npm link

执行完成 ✅

image

我们就可以来测试了,在命令行中输入 my-node-cli 执行一下

image

这里我们就看到命令行中打印了:【my-node-cli working~】,此时最简单的一个demo就完成了👏

2. 询问用户信息

实现与询问用户信息的功能需要引入 inquirer.js 👉 文档看这里

npm install inquirer@8.2.5 --dev // 版本要低于9,否则报语法错误,详见【遇到的问题】

接着我们在 cli.js 来设置我们的问题

#! /usr/bin/env node

// #! 用于指定脚本的解释程序,Node CLI 应用入口文件必须要有这样的文件头

const inquirer = require('inquirer');

inquirer.prompt([
  {
    type: 'input', //type: input, number, confirm, list, checkbox ... 
    name: 'name', // key 名
    message: 'Your name', // 提示信息
    default: 'my-node-cli' // 默认值
  }
]).then(answers => {
  console.log(answers); // 打印互动的输入结果
})

在命令行输入 my-node-cli 看一下执行结果

image

这里我们就拿到了用户输入的项目名称 { name: ‘hello-ranran’ }, 👌

3. 生成对应的文件

3.1 新建模版文件夹

mkdir templates # 创建模版文件夹 

3.2 新建 index.html 和 common.css 两个简单的示例文件

// index.html 
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>
        <!-- ejs 语法 -->
        <%= name %>
    </title>
  </head>
  <body>
    <h1><%= name %></h1>
  </body>
</html>

/* common.css */
body {
    margin: 20px auto;
    background-color: azure;
}

此时的代码结构:

my-node-cli           
├─ templates          
│  ├─ common.css      
│  └─ index.html      
├─ cli.js             
├─ package-lock.json  
└─ package.json       

3.3 接着完善文件生成逻辑

这里借助 ejs 模版引擎将用户输入的数据渲染到模版文件上

npm install ejs --save

完善后到 cli.js 👇

#! /usr/bin/env node

// #! 用于指定脚本的解释程序,Node CLI 应用入口文件必须要有这样的文件头

const inquirer = require('inquirer');
const path = require('path');
const fs = require('fs');
const ejs = require('ejs');

inquirer
  .prompt([
    {
      type: 'input', //type: input, number, confirm, list, checkbox ...
      name: 'name', // key 名
      message: 'Your name', // 提示信息
      default: 'my-node-cli' // 默认值
    }
  ])
  .then((answers) => {
    const destUrl = path.join(__dirname, 'templates'); // 模版文件目录
    const cwdUrl = process.cwd(); // 生成文件目录,process.cwd() 对应控制台所在目录
    // 从模版目录中读取文件
    fs.readdir(destUrl, (err, files) => {
      if (err) throw err;
      // 使用 ejs 渲染对应的模版文件
      files.forEach((file) => {
        // renderFile(模版文件地址,传入渲染数据)
        ejs.renderFile(path.join(destUrl, file), answers).then((data) => {
          // 生成 ejs 处理后的模版文件
          fs.writeFileSync(path.join(cwdUrl, file), data);
        });
      });
    });
  });

同样,在控制台执行一下 my-node-cli ,此时 index.html、common.css 已经成功创建 ✔

image

我们打印一下当前的目录结构 👇

my-node-cli           
├─ templates          
│  ├─ common.css      
│  └─ index.html      
├─ cli.js             
├─ common.css .................... 生成对应的 common.css 文件        
├─ index.html .................... 生成对应的 index.html 文件        
├─ package-lock.json  
└─ package.json    

打开生成的 index.html 文件看一下

image

用户输入的 { name: ‘my-app’ } 已经添加到了生成的文件中了 ✌️

热门脚手架工具库

实际生产中搭建一个脚手架或者阅读其他脚手架源码的时候需要了解下面这些工具库 👇

名称简介
commander命令行自定义指令
inquirer命令行询问用户问题,记录回答结果
chalk控制台输出内容样式美化
ora控制台 loading 样式
figlet控制台打印 logo
cli-table3控制台输出表格
download-git-repo下载远程模版
fs-extra系统fs模块的扩展,提供了更多便利的 API,并继承了fs模块的 API
cross-spawn支持跨平台调用系统上的命令

重点介绍下面这些,其他工具可以查看说明文档

1. commander 自定义命令行指令(在线文档)

安装依赖

npm install commander

完善cli.js代码

#! /usr/bin/env node

const program = require('commander');

program
  .version('0.1.0')
  .command('create <name>')
  .description('create a new project')
  .action((name) => {
    // 打印命令行输入的值
    console.log('project name is ' + name);
  });

program.parse();

npm link 链接到全局

  • 执行 npm link 将应用 my-node-cli 链接到全局

  • 完成之后,在命令行中执行 my-node-cli

看下命令行的输出内容

image

这个时候就有了 my-node-cli命令使用的说明信息

image

image

2. chalk 命令行美化工具(在线文档)

安装依赖

npm install chalk@4.1.2

完善cli.js代码

#! /usr/bin/env node

const program = require('commander');
const chalk = require('chalk');

program
  .version('0.1.0')
  .command('create <name>')
  .description('create a new project')
  .action((name) => {
    // 打印命令行输入的值

    // 文本样式
    console.log('project name is ' + chalk.bold(name));

    // 颜色
    console.log('project name is ' + chalk.cyan(name));
    console.log('project name is ' + chalk.green(name));

    // 背景色
    console.log('project name is ' + chalk.bgRed(name));

    // 使用RGB颜色输出
    console.log('project name is ' + chalk.rgb(4, 156, 219).underline(name));
    console.log('project name is ' + chalk.hex('#049CDB').bold(name));
    console.log('project name is ' + chalk.bgHex('#049CDB').bold(name));
  });

program.parse();

npm link 链接到全局

  • 执行 npm link 将应用 my-node-cli 链接到全局

  • 完成之后,在命令行中执行 my-node-cli create my-app

看下命令行的输出内容

image

具体的样式对照表如下

image

3. inquirer 命令行交互工具(在线文档)

inquirer 在脚手架工具中的使用频率是非常高的,在上文脚手架的简单雏形中,我们已经使用到了,这里就不过多介绍了。

4. ora 命令行 loading 动效(在线文档)

安装依赖

npm install ora@5.x

完善cli.js代码

#! /usr/bin/env node
const ora = require('ora');
// 自定义文本信息
const message = 'Loading...';
// 初始化
const spinner = ora(message);
// 开始加载动画
spinner.start();
setTimeout(() => {
  // 修改动画样式

  // Type: string
  // Default: 'cyan'
  // Values: 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'gray'
  spinner.color = 'red';
  spinner.text = 'Loading rainbows';

  setTimeout(() => {
    // 加载状态修改
    spinner.stop(); // 停止
    spinner.succeed('Loading succeed'); // 成功 ✔
    // spinner.fail(text?);  失败 ✖
    // spinner.warn(text?);  提示 ⚠
    // spinner.info(text?);  信息 ℹ
  }, 2000);
}, 2000);

npm link 链接到全局

  • 执行 npm link 将应用 my-node-cli 链接到全局

  • 完成之后,在命令行中执行 my-node-cli

看下命令行的输出内容

请至钉钉文档查看附件《未命名_副本.mov》

5. cross-spawn 跨平台shell工具(在线文档

安装依赖

npm install cross-spawn

完善cli.js代码

#! /usr/bin/env node

const spawn = require('cross-spawn');
const chalk = require('chalk');

// 定义需要按照的依赖
const dependencies = ['vue', 'vuex', 'vue-router'];

// 执行安装
const child = spawn('npm', ['install', '-D'].concat(dependencies), {
  stdio: 'inherit'
});

// 监听执行结果
child.on('close', function (code) {
  // 执行失败
  if (code !== 0) {
    console.log(chalk.red('Error occurred while installing dependencies!'));
    process.exit(1);
  }
  // 执行成功
  else {
    console.log(chalk.cyan('Install finished'));
  }
});

看下命令行的输出内容

image

成功安装 👍

搭建自己的脚手架

需要实现的功能

  1. 通过 ranran-cli create 命令启动项目

  2. 如果重名则询问用户是否进行覆盖

  3. 远程拉取模板文件

搭建步骤拆解

  1. 创建项目

  2. 创建脚手架启动命令(使用 commander)

  3. 如果重名则询问用户是否进行覆盖

  4. 下载远程模板(使用 download-git-repo)

代码实现

目录结构

image

package.json

{
  "name": "ranran-cli",
  "version": "1.0.0",
  "description": "simple vue cli",
  "main": "index.js",
  "bin": {
    "ranran-cli": "./bin/cli.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "files": [
    "bin",
    "lib"
  ],
  "author": "ranran",
  "keywords": [
    "ranran-cli",
    "脚手架"
  ],
  "license": "ISC",
  "dependencies": {
    "chalk": "^4.1.2",
    "commander": "^10.0.1",
    "cross-spawn": "^7.0.3",
    "download-git-repo": "^3.0.2",
    "ejs": "^3.1.9",
    "figlet": "^1.6.0",
    "fs-extra": "^11.1.1",
    "inquirer": "^8.2.5",
    "ora": "^5.4.1"
  }
}

脚手架启动文件****cli.js

#! /usr/bin/env node

const program = require('commander');
const figlet = require('figlet');

program
  .command('create <app-name>')
  .description('create a new project')
  .option('-f, --force', 'overwrite target directory if it exist') // 是否强制创建,当文件夹已经存在
  .action((name, options) => {
    // 在 create.js 中执行创建任务
    require('../lib/create.js')(name, options);
  });

program.on('--help', () => {
  console.log(
    '\r\n' +
      figlet.textSync('ranran', {
        font: 'Ghost',
        horizontalLayout: 'default',
        verticalLayout: 'default',
        width: 80,
        whitespaceBreak: true
      })
  );
});

// 解析用户执行命令传入参数
program.parse(process.argv);

创建 lib 文件夹并在文件夹下创建 create.js

const path = require('path');

// fs-extra 是对 fs 模块的扩展,支持 promise 语法
const fs = require('fs-extra');
const inquirer = require('inquirer');
const Generator = require('./utils');

module.exports = async function (name, options) {
  // 当前命令行选择的目录
  const cwd = process.cwd();
  // 需要创建的目录地址
  const targetAir = path.join(cwd, name);

  // 目录是否已经存在?
  if (fs.existsSync(targetAir)) {
    // 是否为强制创建?
    if (options.force) {
      await fs.remove(targetAir);
    } else {
      // 询问用户是否确定要覆盖
      let { action } = await inquirer.prompt([
        {
          name: 'action',
          type: 'list',
          message: 'Target directory already exists Pick an action:',
          choices: [
            {
              name: 'Overwrite',
              value: 'overwrite'
            },
            {
              name: 'Cancel',
              value: false
            }
          ]
        }
      ]);

      if (!action) {
        return;
      } else if (action === 'overwrite') {
        // 移除已存在的目录
        console.log(`\r\nRemoving...`);
        await fs.remove(targetAir);
      }
    }
  }

  // 创建项目
  const generator = new Generator(name);

  // 开始创建项目
  generator.create();
};

创建 lib 文件夹并在文件夹下创建 utils.js

const downloadGitRepo = require('download-git-repo');
const ora = require('ora');
const chalk = require('chalk');

const bsInitOriginUrl = '@git/xx.xxx.x.x:xx/xx.git';
const bsInitUrl = 'gitlab:xx.xxx.x.x:xx/xx#dev';

// 项目模板远程下载
const downloadTemplate = async (ProjectName, api) => {
  return new Promise((resolve, reject) => {
    downloadGitRepo(api, ProjectName, { clone: true }, (err) => {
      if (err) {
        reject(err);
      } else {
        resolve();
      }
    });
  });
};

class Generator {
  constructor(name) {
    // 目录名称
    this.name = name;
  }

  async download() {
    let loading = ora().start(`Start cloning template... ${chalk.yellow(bsInitOriginUrl)}`);
    await downloadTemplate(this.name, bsInitUrl);
    setTimeout(() => {
      loading.succeed(`\r\nSuccessfully created project ${chalk.cyan(this.name)}`);
      console.log(`\r\n  cd ${chalk.cyan(this.name)}`);
      console.log('  npm run dev\r\n');
    }, 2000);
  }

  // 下载模板到模板目录
  async create() {
    // 下载模板到模板目录
    await this.download();
  }
}

module.exports = Generator;

遇到的问题

使用my-node-cli报错

image

原因:发现安装的是看了一下【inquirer】的版本号是9以上的

解决方法:降【inquirer】的版本到8.2.5

npm i inquirer@8.2.5

npm link只需要执行一次

代码更改后,不需要重新执行npm link

如果是修改了执行命令的别名,则需要重新执行npm link

image

删除软链接

更改入口文件后重新进行npm link报错,已修改入口文件地址。

image

使用npm unlink、npm link --force均无效。

image

image

解决方法:找到npm软链的目录,删除相应的文件,有两处都需要删干净。

image

重新 npm link后生效。

image

参考文档

https://juejin.cn/post/6966119324478079007#heading-38

https://juejin.cn/post/7178666619135066170#heading-21

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

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

相关文章

Unity Audio -- (3)创建3D音效

本节会添加场景中小瀑布的音效。小瀑布的音效会有一个作用范围&#xff0c;也会根据角色所处的位置不同&#xff0c;产生不同的效果。 添加小瀑布的声音 1. 在Hierarchy中&#xff0c;点击右键&#xff0c;选择Audio -> Create Audio Source&#xff0c;将这个新的Audio So…

HEVC学习之去方块滤波

一、概要 视频编码为视频带来的压缩伪影呈现出的效果各有不同&#xff0c;但其原因总结起来为高频信息的失真以及基于块的编码消除了块与块之间的相似性。 为了弥补基于块的编码带来的影响&#xff0c;HEVC中引入了去方块滤波。 AVC中采取对44块的边界进行去方块滤波&#xf…

通过自定义域名 + SSL 的方式访问 Amazon MQ for RabbitMQ

引言&#xff1a; 一般为了解决应用解耦&#xff0c;异步处理&#xff0c;流量削峰等问题&#xff0c;实现高性能&#xff0c;高可用&#xff0c;可伸缩和最终一致性的架构&#xff1b;我们会引入消息队列中间件来完善架构设计。 对于需要消息传递协议的应用程序&#xff0c;…

CCFCSP 201409-2 画图

思路上很容易想到即使用一个标记数组对上过色的模块进行标记&#xff0c;最后遍历该数组得到被标记的模块数即可 #include<iostream>using namespace std;int mapp[105][105]; int ans0;int main(){int n;cin>>n;for(int i0;i<n;i){int x1,y1,x2,y2;cin>>…

【P15】JMeter 正则表达式提取器(Regular Expression Extractor)

文章目录 一、准备工作二、测试计划设计 一、准备工作 慕慕生鲜&#xff1a; http://111.231.103.117/#/login 进入网页后&#xff0c;登录&#xff0c;页面提供了账户和密码 搜索框输入“虾” 右键检查或按F12&#xff0c;打开调试工具&#xff0c;点击搜索 二、测试计划设…

【Java项目】SpringCloud项目注册到Nacos中心时显示的是内网IP导致不同服务器之间无法互相调用接口的解决并发

微服务项目地址—动动你发财的小手点一个stars吧 出现这个问题是之前我也就遇到过的&#xff0c;这个问题的情况就是&#xff1a; 我们知道微服务项目是可以把不同的项目部署在不同的服务器上从而减少某一台服务器的压力&#xff0c;我们只需要为每一个服务配置一个注册中心即…

计算机基础--计算机存储单位

一、介绍 计算机中表示文件大小、数据载体的存储容量或进程的数据消耗的信息单位。在计算机内部&#xff0c;信息都是釆用二进制的形式进行存储、运算、处理和传输的。信息存储单位有位、字节和字等几种。各种存储设备存储容量单位有KB、MB、GB和TB等几种。 二、基本存储单元…

【进阶知识】显示管理器,窗口管理器,桌面环境/桌面管理器,显示服务器

文章目录 一、显示管理器&#xff08;Display Manager&#xff09;1.1 什么是 Linux 中的显示管理器&#xff1f;1.2 不同的显示管理器1.3 其他控制台显示管理器图形界面显示管理器 二、窗口管理器&#xff08;Window Manager&#xff09;三、桌面环境/桌面管理器&#xff08;D…

AD9680之JESD204B接口2路、4路、8路的14bit 500MSPS/1GSPS/1.25GSPS采样率子卡的中文版本设计及调试经验资料分享

板卡概述&#xff1a; 【FMC155】 FMC155 是一款基于 VITA57.1 标准的&#xff0c;实现 2 路 14-bit、500MSPS/1GSPS/1.25GSPS 直流耦合 ADC 同步采集 FMC 子卡模 块。 该模块遵循 VITA57.1 规范&#xff0c;可直接与 FPGA 载卡配合使用&#xff0c;板 卡 ADC 器件采用 ADI 的…

MySQL基础篇补充 | 单行函数(数值函数、字符串函数、日期函数、流程控制函数、加密与解密函数、MySQL信息函数)

目录 一&#xff1a;单行函数 1. 数值函数 &#xff08;1&#xff09;基本函数 &#xff08;2&#xff09;角度与弧度互换函数 &#xff08;3&#xff09;三角函数 &#xff08;4&#xff09;指数与对数 &#xff08;5&#xff09;进制间的转换 2. 字符串函数 3. 日期和…

Nginx配置浏览器缓存,页面展示更快一步

1.简介 缓存能够存储请求的响应结果&#xff0c;可以很方便的再次访问&#xff0c;使用缓存的优点是很明显的。 加速内容的访问&#xff0c;降低响应时间减少服务器的负载 Nginx不仅仅是一个web服务器&#xff0c;它也是一个web缓存服务器。通过Nginx缓存&#xff0c;我们对…

Java引用类型(强引用,软引用,弱引用,虚引用)

从Java SE2开始&#xff0c;就提供了四种类型的引用&#xff1a;强引用、软引用、弱引用和虚引用。Java中提供这四种引用类型主要有两个目的&#xff1a;第一是可以让程序员通过代码的方式决定某些对象的生命周期&#xff1b;第二是有利于JVM进行垃圾回收。 强引用&#xff08…

【Linux环境】Linux常用命令记录汇总

Linux常用命令记录汇总 一、传输命令二、打包命令三、创建文件夹或文件命令四、切换用户五、vim相关命令六、权限更改命令 一、传输命令 命令格式&#xff1a; rz 点击回车 sz 文件名 点击回车命令解释&#xff1a; rz&#xff08;receive Zmodem缩写&#xff09;上传文件&am…

淘宝时光机入口在哪里怎么打开回溯20年淘宝历史账单?

淘宝时光机入口在哪里怎么打开&#xff1f; 打开淘宝时光机入口&#xff1a;https://www.caochai.com/article-4208.html &#xff0c;开始回溯20年淘宝历史账单&#xff1b; 淘宝大额内部隐藏优惠券怎么领取&#xff1f; 1、打开淘宝优惠券查询领取入口&#xff1a;https://…

上班摸鱼逛博客,逮到一个字节8年测试开发,聊过之后羞愧难当......

老话说的好&#xff0c;这人呐&#xff0c;一旦在某个领域鲜有敌手了&#xff0c;就会闲得某疼。前几天我在上班摸鱼刷博客的时候认识了一位字节测试开发大佬&#xff0c;在字节工作了8年&#xff0c;因为本人天赋比较高&#xff0c;平时工作也兢兢业业&#xff0c;现在企业内有…

好家伙,一天约了6场面试,又被吊打了....

好兄弟一天约了6场面试&#xff0c;又被吊打了 面试感受 先说一个字 是真的 “ 累 ” 安排的太满的后果可能就是一天只吃一顿饭&#xff0c;一直奔波在路上 不扯这个了&#xff0c;给大家说说面试吧&#xff0c;我工作大概两年多的时间&#xff0c;大家可以参考下 在整个面试…

mysql 连表查询

文章目录 一、内连接二、外连接2.1 左外连接2.2 右外连接 三、总结 写在前面 在MySQL中join操作被称为连接&#xff0c;作用是能连接多个表的数据&#xff08;通过连接条件&#xff09;&#xff0c;从多个表中获取数据合并在一起作为结果集返回给客户端。即使用连接查询一条SQL…

Redis 主从 + 哨兵模式搭建

前言&#xff1a;以Linux环境为示例 一、整体架构&#xff08;1主 2从 3哨兵&#xff09; 二、redis 安装 1、安装路径&#xff1a;/usr/local/redis/redis-6382&#xff08;可自行指定&#xff09; 2、解压安装包&#xff1a; 直接解压&#xff1a;tar -axvf redis-5.0.…

国内可以使用的chatgpt站点,有多种工具可用

看到很多人在寻找国内可以使用的chatgpt站点&#xff0c;忍不住来给大家分享一波&#xff0c;这个相对而言还是挺好用的&#xff0c;不限制字数&#xff0c;每天都能白女票使用。看下面正文。 chatgpt的使用方法 在浏览器或者打开我的电脑&#xff0c;顶部车俞入 人工智能聊…

C++好难(5):内存管理

这一节学完&#xff0c;我们 C嘎嘎 就算是正式入门了&#xff0c;但是之后的课还会更上一阶d(ŐдŐ๑) 继续坚持&#xff01; 【本节目标】 1. C/C内存分布 2. C语言中动态内存管理方式 3. C中动态内存管理 4. operator new与operator delete函数 5. new和delete的实现原…