一个简单的脚手架

作者: 幻城之雪 | 来源:发表于2023-04-26 21:32 被阅读0次

    背景

    像我们熟悉的 vue-cli,create-react-app 等脚手架,只需要输入简单的命令 vue init webpack project,即可快速帮我们生成一个初始项目。在实际工作中,我们可以定制一个属于自己的脚手架,来提高自己的工作效率。

    目的如下:

    减少重复性的工作,不再需要复制其他项目再删除无关代码,或者从零创建一个项目和文件。

    根据交互动态生成项目结构和配置文件等。

    多人协作更为方便,不需要把文件传来传去。

    工作原理

    通过命令行交互询问用户问题;

    根据用户回答的结果生成文件;

    开发流程

    可借鉴vue-cli的开发思路, vue-cli是将项目模板放在git上,运行的时候再根据用户交互下载不同的模板,经过模板引擎渲染出来,生成项目。这样将模板和脚手架分离,就可以各自维护,即使模板有变动,只需要上传最新的模板即可,而不需要用户去更新脚手架就可以生成最新的项目。那么就可以按照这个思路来进行开发了。

    开发工具

    commander.js,可以自动的解析命令和参数,用于处理用户输入的命令。

    download-git-repo,下载并提取 git 仓库,用于下载项目模板。

    Inquirer.js,通用的命令行用户界面集合,用于和用户进行交互。

    handlebars.js,模板引擎,将用户提交的信息动态填充到文件中。

    ora,下载过程久的话,可以用于显示下载中的动画效果。

    chalk,可以给终端的字体加上颜色。

    log-symbols,可以在终端上显示出 √ 或 × 等的图标。

    初始化项目

    新建新项目,添加一个index.js文件

    npm init 生成一个package.json文件

    安装上述开发工具依赖

    命令行处理

    node.js 内置了对命令行操作的支持,在 package.json 中的 bin 字段可以定义模块的入口文件,所以现在 package.json 中加上 bin 的内容:

    {
    
      "name": "suporka-parcel-vue",
    
      "version": "1.0.0",
    
      "description": "a vue cli which use parcel to package object",
    
      "bin": {
    
        "suporka-parcel-vue": "index.js"
    
      },
    
      ...
    
    }
    

    然后在 index.js 中来定义 init 命令:

    #!/usr/bin/env node
    
    const program = require('commander');
    
    program.version('1.0.0', '-v, --version')
    
        .command('init <name>')
    
        .action((name) => {
    
            console.log(name);
    
        });
    
    program.parse(process.argv);
    

    (1)需要特别注意的一点是:我们要在index.js文件头部需要添加#!/usr/bin/env node,用于指定该文件需要使用node来运行,让系统动态的去查找node,以解决不同机器不同用户设置不一致问题;

    (2)调用 version('1.0.0', '-v, --version') 会将 -v 和 --version 添加到命令中,可以通过这些选项打印出版本号。 调用 command('init ') 定义 init 命令,name 则是必传的参数,为项目名。 action() 则是执行 init 命令会发生的行为,要生成项目的过程就是在这里面执行的,这里暂时只打印出 name。 其实到这里,已经可以执行 init 命令了。我们来测试一下,在同级目录下执行:

    node index.js init HelloWorld
    

    (3)可以看到命令行工具也打印出了 HelloWorld,那么很清楚, action((name) => {}) 这里的参数 name,就是我们执行 init 命令时输入的项目名称;

    (4)在终端中通过npm link将我们的包映射到全局,这样就可以通过运行模块名来运行我们的模块,以方便我们在本地测试

    命令已经完成,接下来就要下载模板生成项目结构了。

    下载模板

    download-git-repo 支持从 Github、Gitlab 和 Bitbucket 下载仓库,各自的具体用法可以参考官方文档。

    命令行交互

    命令行交互功能可以在用户执行 init 命令后,向用户提出问题,接收用户的输入并作出相应的处理。这里使用 inquirer.js 来实现。

    const inquirer = require('inquirer');
    
    inquirer.prompt([
    
        {
    
            name: 'description',
    
            message: 'Input the object description'
    
        },
    
        {
    
            name: 'author',
    
            message: 'Input the object author'
    
        }
    
        ]).then((answers) => {
    
        console.log(answers.author);
    
    })
    

    通过这里例子可以看出,问题就放在 prompt() 中,问题的类型为 input 就是输入类型,name 就是作为答案对象中的 key,message 就是问题了,用户输入的答案就在 answers 中,使用起来就是这么简单。更多的参数设置可以参考官方文档。

    通过命令行交互,获得用户的输入,从而可以把答案渲染到模板中。

    渲染模板

    这里用 handlebars 的语法对模板中的 package.json 文件做一些修改

    {
    
      "name": "{{name}}",
    
      "version": "1.0.0",
    
      "description": "{{description}}",
    
      "scripts": {
    
        "test": "echo \"Error: no test specified\" && exit 1"
    
      },
    
      "author": "{{author}}",
    
      "license": "ISC"
    
    }
    

    并在下载模板完成之后将用户输入的答案渲染到 package.json 中

    视觉美化

    在用户输入答案之后,开始下载模板,这时候使用 ora 来提示用户正在下载中。

    const ora = require('ora');
    
    // 开始下载
    
    const spinner = ora('正在下载模板...');
    
    spinner.start();
    
    // 下载失败调用
    
    spinner.fail();
    
    // 下载成功调用
    
    spinner.succeed();
    

    然后通过 chalk 来为打印信息加上样式,比如成功信息为绿色,失败信息为红色,这样子会让用户更加容易分辨,同时也让终端的显示更加的好看。

    const chalk = require('chalk');
    
    console.log(chalk.green('项目创建成功'));
    
    console.log(chalk.red('项目创建失败'));
    

    除了给打印信息加上颜色之外,还可以使用 log-symbols 在信息前面加上 √ 或 × 等的图标

    const chalk = require('chalk');
    
    const symbols = require('log-symbols');
    
    console.log(symbols.success, chalk.green('项目创建成功'));
    
    console.log(symbols.error, chalk.red('项目创建失败'));
    

    完整示例

    #!/usr/bin/env node
    
    // 处理用户输入的命令
    
    const program = require('commander');
    
    // 下载模板
    
    const download = require('download-git-repo');
    
    // 问题交互
    
    const inquirer = require('inquirer');
    
    // node 文件模块
    
    const fs = require('fs');
    
    // 填充信息至文件
    
    const handlebars = require('handlebars');
    
    // 动画效果
    
    const ora = require('ora');
    
    // 字体加颜色
    
    const chalk = require('chalk');
    
    // 显示提示图标
    
    const symbols = require('log-symbols');
    
    // 命令行操作
    
    var shell = require("shelljs");
    
    program.version('1.0.1', '-v, --version')
    
      .command('init <name>')
    
      .action((name) => {
    
        if (!fs.existsSync(name)) {
    
          inquirer.prompt([
    
            {
    
              name: 'description',
    
              message: 'Input the object description'
    
            },
    
            {
    
              name: 'author',
    
              message: 'Input the object author'
    
            }
    
          ]).then((answers) => {
    
            const spinner = ora('Downloading...');
    
            spinner.start();
    
            download('zxpsuper/suporka-parcel-vue', name, (err) => {
    
              if (err) {
    
                spinner.fail();
    
                console.log(symbols.error, chalk.red(err));
    
              } else {
    
                spinner.succeed();
    
                const fileName = `${name}/package.json`;
    
                const meta = {
    
                  name,
    
                  description: answers.description,
    
                  author: answers.author
    
                }
    
                if (fs.existsSync(fileName)) {
    
                  const content = fs.readFileSync(fileName).toString();
    
                  const result = handlebars.compile(content)(meta);
    
                  fs.writeFileSync(fileName, result);
    
                }
    
                console.log(symbols.success, chalk.green('The vue object has downloaded successfully!'));
    
                inquirer.prompt([
    
                  {
    
                    type: 'confirm',
    
                    name: 'ifInstall',
    
                    message: 'Are you want to install dependence now?',
    
                    default: true
    
                  }
    
                ]).then((answers) => {
    
                  if (answers.ifInstall) {
    
                    inquirer.prompt([
    
                      {
    
                        type: 'list',
    
                        name: 'installWay',
    
                        message: 'Choose the tool to install',
    
                        choices: [
    
                          'npm', 'cnpm'
    
                        ]
    
                      }
    
                    ]).then(ans => {
    
                      if (ans.installWay === 'npm') {
    
                        let spinner = ora('Installing...');
    
                        spinner.start();
    
                        // 命令行操作安装依赖
    
                        shell.exec("cd " + name + " && npm i", function (err, stdout, stderr) {
    
                          if (err) {
    
                            spinner.fail();
    
                            console.log(symbols.error, chalk.red(err));
    
                          }
    
                          else {
    
                            spinner.succeed();
    
                            console.log(symbols.success, chalk.green('The object has installed dependence successfully!'));
    
                          }
    
                        });
    
                      } else {
    
                        let spinner = ora('Installing...');
    
                        spinner.start();
    
                        shell.exec("cd " + name + " && cnpm i", function (err, stdout, stderr) {
    
                          if (err) {
    
                            spinner.fail();
    
                            console.log(symbols.error, chalk.red(err));
    
                          }
    
                          else {
    
                            spinner.succeed();
    
                            console.log(symbols.success, chalk.green('The object has installed dependence successfully!'));
    
                          }
    
                        })
    
                      }
    
                    })
    
                  } else {
    
                    console.log(symbols.success, chalk.green('You should install the dependence by yourself!'));
    
                  }
    
                })
    
              }
    
            })
    
          })
    
        } else {
    
          // 错误提示项目已存在,避免覆盖原有项目
    
          console.log(symbols.error, chalk.red('The object has exist'));
    
        }
    
      });
    
    program.parse(process.argv);
    

    相关文章

      网友评论

        本文标题:一个简单的脚手架

        本文链接:https://www.haomeiwen.com/subject/hsxbjdtx.html