从零构建自己的脚手架

从零构建自己的脚手架

简介

什么是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

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue CLI 是一款官方提供的脚手架工具,可以帮助我们快速创建 Vue 项目。如果您想要自己制作 Vue 的脚手架,可以参考以下步骤: 1. 创建一个空的项目文件夹,例如 my-vue-cli。 2. 在项目文件夹中创建一个 bin 文件夹,并在 bin 文件夹中创建一个名为 my-vue 的可执行文件(注意没有扩展名)。 3. 在 my-vue 文件中添加如下代码: ``` #!/usr/bin/env node console.log('Hello Vue CLI!'); ``` 这段代码的作用是,当我们在终端中执行 my-vue 命令时,会输出一句话“Hello Vue CLI!”。 4. 在终端中执行 `chmod +x ./bin/my-vue` 命令,给 my-vue 文件添加执行权限。 5. 在终端中执行 `npm init` 命令,初始化项目。 6. 在项目中安装 commander 模块,用于解析命令行参数。执行 `npm install commander` 命令安装。 7. 在 my-vue 文件中添加如下代码,用于解析命令行参数: ``` #!/usr/bin/env node const program = require('commander'); program .version('0.1.0') .command('create <app-name>') .action((name) => { console.log(`Creating a new Vue app with name ${name}...`); }); program.parse(process.argv); ``` 这段代码的作用是,当我们在终端中执行 `my-vue create my-app` 命令时,会输出一句话“Creating a new Vue app with name my-app...”。 8. 在 my-vue 文件中添加如下代码,用于检查命令行参数是否正确: ``` #!/usr/bin/env node const program = require('commander'); program .version('0.1.0') .command('create <app-name>') .action((name) => { if (!name) { console.error('Please specify the app name.'); process.exit(1); } console.log(`Creating a new Vue app with name ${name}...`); }); program.parse(process.argv); ``` 这段代码的作用是,当我们在终端中执行 `my-vue create` 命令时,会输出一句话“Please specify the app name.”。 9. 在 my-vue 文件中添加如下代码,用于执行真正的创建 Vue 项目的逻辑: ``` #!/usr/bin/env node const program = require('commander'); const execa = require('execa'); program .version('0.1.0') .command('create <app-name>') .action((name) => { if (!name) { console.error('Please specify the app name.'); process.exit(1); } console.log(`Creating a new Vue app with name ${name}...`); execa('vue', ['create', name]) .then(() => console.log('Done!')) .catch((err) => console.error(err.message)); }); program.parse(process.argv); ``` 这段代码的作用是,当我们在终端中执行 `my-vue create my-app` 命令时,会使用 execa 模块执行 `vue create my-app` 命令,从而创建一个新的 Vue 项目。 10. 在终端中执行 `npm link` 命令,将 my-vue 命令链接到全局命令行。 现在,当我们在终端中执行 `my-vue create my-app` 命令时,就可以快速创建一个新的 Vue 项目了。当然,这只是一个简单的例子,实际上自己制作脚手架还需要考虑很多其他的因素。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值