从0开始搭建优雅的前端脚手架工具

2022-12-05 15:42:15 浏览数 (1)

前言

在日常开发中,我们经常会使用到各种脚手架工具(cli): vue-create-app,ng 包括 npm。它们极大简化了开发人员对于项目结构和文件创建的工作,让我们可以把精力专心在业务实现上。 对于某些项目而言 cli还可以封装一些脚本,用来处理项目中的一些特殊场景。

开发cli的好处:

  • 简化项目初始化流程,对于部门内新立项的项目能快速的生成项目骨架。
  • 集成项目通用指令,例如:本地化发布上传,subtree 拉取配置等,部门内所有项目统一升级等。
  • 减少重复工作,跟据用户输入还可以进行差异化配置

原理

cli 不论有多少功能基本原理就是利用 nodejs 来进行脚本和文件的各种操作,比如 init 等初始化的指令就是在指定的模板代码仓库中拉取相应的代码,再跟据用户输入进行模板替换。 再比如 npm run serve这类的指令就是利用node 的环境运行一些 shell脚本达到相应的功能。

准备工作

编写cli能使用到的工具库有很多,并且各有各的优点,最为常见的配套按 使用顺序 如下:

commander.js,解析用户命令(init, create, -v, help 等)

inquirer,执行命令后的用户交互集合有多种可供支持的交互类型(input, checkbox, list 等)

download-git-repo,下载指定仓库指定分支下的代码,作为项目模板

handlebars,模板引擎,用户根据用户输入替换项目模板中的内容实现自定义与差异化

除开以上功能性的模块,还有许多可以用来提高用户体验的包:

ora, 执行命令时的动画效果,用来标识cli的运行进度。

chalk,给通过终端console命令输出的文本添加字体颜色。

log-symbols,给通过终端console命令输出的文本添加符号标识(info, success,warning,error四种)

步骤

想直接查看源码的请拖到页面底部。

初始化项目

创建一个空目录,根据你的喜好命名(示例项目为d-cli),然后进入项目执行 npm init 生成 package.json文件。 接着安装上面提到的依赖包

npm install commander inquirer download-git-repo handlebars ora chalk log-symbols

接着在package.json 中加上 bin的内容:

代码语言:txt复制
{
   "name": "d-cli",
   "version": "1.0.0",
   "bin": {
      "d-cli": "index.js"
   },
   ...
}

然后在根目录创建 index.js 文件,加载所有需要用到的依赖

代码语言:JavaScript复制
const fs = require("fs");
const program = require("commander");
const download = require("download-git-repo");
const handlebars = require("handlebars");
const inquirer = require("inquirer");
const ora = require("ora");
const chalk = require("chalk");
const symbols = require("log-symbols");
const exec = require("child_process").exec;
const path = require("path");

开始编写我们的第一个方法

代码语言:JavaScript复制
function startCommand() {
  return new Promise((resolve) => {
    program
      .version(
        require(path.resolve(__dirname, "./package")).version,
        "-v --version"
      )
      .command("init <name>")
      .action((name) => {
        if (!fs.existsSync(name)) {
          inquirer
            .prompt([
              {
                type: 'input',
                name: 'name',
                message: 'Please input project name'
              },
              {
                type: 'input',
                name: 'author',
                message: 'Please input project author'
              },
              {
                type: 'list',
                name: 'description',
                choices: ['This project is for learn cli',
                          'How about do a amazing job?'],
                default: 0,
                message: 'Please input project description'
              }
            ])
            .then((answers) => {
              resolve({
                name,
                answers,
              });
            });
      } else {
        console.log(symbols.error, chalk.red("Project already exists!"));
      }
    });
    program.parse(process.argv);
    if (!program.args.length) {
      program.help();
    }
  });
}

startCommand 为开启cli 命令的函数,在内部我们通过commander 包定义cli 的 version 信息(版本号与package.json 保持同步)之后就可以通过 command方法 定义我们需要的功能,<name> 字段为用户在命令关键字后输入的内容,可以在action回调中获取

代码语言:JavaScript复制
program
      .version(
        require(path.resolve(__dirname, "./package")).version,
        "-v --version"
      )
      .command("init <name>")
      .action((name) => {})

接下来我们使用 inquirer 包的 prompt 方法来与用户进行交互,数组中的内容会按顺序来提示用户输入,对象中的 name 字段会汇总起来构成回调函数中 answers 的 key。 最后通过 Promise 的 resolve 返回我们收集到的信息。

代码语言:JavaScript复制
resolve({
  name,
  answers,
});

此外我们还通过 fs.existsSync 判断了用户输入的 name 是否在当前目录下存在重名。 最后通过 program.parse(process.argv) 启动命令配置。

代码语言:JavaScript复制
if (!program.args.length) {
  program.help();
}

最后判断,当用户未输入参数时给予 help 提示帮助

下载模板

代码语言:JavaScript复制
function downloadProject(name) {
  return new Promise((resolve) => {
    const spinner = ora("Downloading template...");
    spinner.start();
    download(
      "https://github.com:Derrys/testing-case#cli-template",
      name,
      { clone: true },
      (err) => {
        if (err) {
          spinner.fail();
          console.log(symbols.error, chalk.red(err));
        } else {
          spinner.succeed();
          resolve();
        }
      }
    );
  });
}

downloadProject 方法接收一个 name 参数(上一步返回的 name)指定下载到的文件夹名,通过 download-git-repo库的 download 方法下载对应仓库地址的代码。需要注意的是 download 的第一个参数 https://github.com:Derrys/testing-case#cli-template 是原本正常仓库地址 (https://github.com/Derrys/testing-case.git) 的变形写法在域名后的 url 开始位置用 : 替代 /; 地址最后的 .git 后缀名需要删除; #cli-template 后面为仓库对应的 branch 名称。

Error: 'git clone' failed with status 128 这个报错,大多是地址配置有误

渲染模板

在被clone 的仓库中我们可以使用 handlebars 的语法定义需要被替换的值

代码语言:JSON复制
// https://github.com/Derrys/testing-case.git/package.json
{
  "name": "{{ name }}",
  "version": "0.1.0",
  "description": "{{ description }}",
  "author": "{{ author }}",
  "private": true,
}

然后定义所有需要被替换的文件路径集合

代码语言:Javascript复制
const TEMPLATEFILES = [
  // Here you can list all the file directories that need to be replaced
  'package.json'
]

接着定义模板替换方法

代码语言:Javascript复制
function setTemplate(fileName, meta) {
  return new Promise((resolve, reject) => {
    if (fs.existsSync(fileName)) {
      const content = fs.readFileSync(fileName).toString();
      const result = handlebars.compile(content)(meta);
      fs.writeFileSync(fileName, result);
      resolve();
    } else {
      reject();
    }
  });
}

最后我们就可以使用 Promise.all 等方法进行批量替换了

代码语言:javascript复制
const spinner = ora("Set template files...");
spinner.start();
await Promise.all(TEMPLATEFILES.map((i) => setTemplate(path.join(__dirname, name, i), answers))).then(() => {
  spinner.succeed();
}).catch(e => {
  console.log(symbols.error, chalk.red(e));
  spinner.fail();
})

执行Shell

在模板替换完毕之后,我们还可以进行很多 shell 脚本操作来简化项目的运行步骤。

代码语言:javascript复制
function doShellJob(shell, tips) {
  return new Promise((resolve, reject) => {
    const spinner = ora(tips);
    spinner.start();
    exec(shell, (error, stdout, stderr) => {
      if (error) {
        spinner.fail();
        console.log(symbols.error, chalk.red(err));
        reject();
        process.exit();
      }
      spinner.succeed();
      resolve();
    });
  });
}

通过

代码语言:javascript复制
await doShellJob(`cd ${name} && git init`, "Git initializing...")
await doShellJob(`cd ${name} && npm install`, "Downloading node modules...")

就可以让脚本按顺序依次执行。

最后记得当所有步骤都完成之后给用户一个友好而又显著的提示,并结束当前的 process。

代码语言:javascript复制
console.log(
  symbols.success,
  chalk.green("Project initialization completed, enjoy you coding now!")
);
process.exit();

现在我们的脚手架工具已经搭建好了,大家可以一起来尝试下了!

全部源码

d-cli

0 人点赞