前端工程化之—— CLI 脚手架(两步掌握核心)

1,270 阅读6分钟

前言

脚手架是前端工程化中不可缺少的一环,主要为了快速搭建新项目,统一项目约定规范,通过自动化和智能化流程,规避 copy 带来的繁琐和易出错,提升开发效率和体验。

举个实际点的例子:首先项目目录结构就可以达到统一,其次 package.json 内的依赖,常用的一些库,都可以提前内置。

总而言之就是:目前脚手架无法满足你搭建完后直接开发,还需要进行额外的许多操作时,是时候开始自己动手来一个脚手架了。

配置约定

项目目录结构:

目录结构.png

package.json

  "type": "module",
  "bin": {
    "cli": "bin/main.js"
  },

"type": "module" 是为了使用 ESM 模块规范 ,而不再使用 CommonJs 语法 require。

这个细节很重要,现在很多库最新版本都不支持 commonjs 了(比如下面用到的就有:inquirer,ora,chalk 等等)。我想大概是因为直接使用 CommonJs 的 require 加载 ESM 模块会报错,而 ESM 可以使用 import 命令加载 CommonJS 吧。

bin 字段是用来指定内部命令对应可执行文件的位置,由于 node_modules/.bin/ 目录会在运行时加入系统的 PATH 变量,因此在运行 npm 时,就可以不带路径,直接通过命令来调用这些脚本。 (通俗点就是你用 npm 全局安装后,可直接执行 cli 命令。如果你用 yarn 全局安装后却无法执行命令,记得查看是否已经配置了 yarn 的环境变量。)

bin/main.js 用来作为一个入口文件:

#! /usr/bin/env node

头部添加 #!/usr/bin/env node 的作用,是为了解决不同用户 node 路径不同的问题,可以让系统动态的去查找 node 来执行你的脚本文件(#!是一个标识,代表此文件可以当做脚本运行)

src 内写对应的逻辑。

脚手架核心

试着安装 Vue 官方的项目脚手架工具。发现可以分成核心两步:

  • 命令交互:开始询问创建的项目名,然后是一些可选功能提示。
  • 下载模板:根据用户的选择,拉取对应的模板创建项目。

image.png

命令行交互

inquirerenquirerprompts:可以处理复杂的用户输入,完成命令行输入交互。(下面以 inquirer 为例)

src/inquiry.js

import inquirer from "inquirer";

export default inquirer.prompt([
  {
    type: "input",
    name: "projectName",
    message: "请输入项目名:",
    default: "react-project",
  },
]);

下载模板

download-git-repo:下载并提取一个 git 仓库

不管你的 git 仓库是 gitlab 还是 github,操作都一样,下面以 github 为例:

// url
https://github.com/owner/name 


// Clone width HTTPS
https://github.com/owner/name.git


// Clone width SSH
git@github.com:owner/name.git

src/download.js

import path from "path";
import download from "download-git-repo";

export default (dir) => {
  download("owner/name", path.join(process.cwd(), dir), (err) => {
    console.log(err ? "Error" : "Success");
  });
};

默认拉取master,如果要指定分支,末尾加#xxx分支

如何拉取私有仓库的模板

如果你是私有仓库,仅仅上面的做法是找不到的,会报下面的错误!

GotError [HTTPError]: Response code 404 (Not Found)

那要怎么做呢?

首先很重要的一点是把项目访问权限更改为 public,然后更改为下面的写法。(注意中间/更改为:)

export default (dir) => {
  download(
    "http://11.168.1.123:owner/name",
    path.join(process.cwd(), dir),
    { clone: true },
    (err) => {
      console.log(err ? "Error" : "Success");
    }
  );
};

如何拉取指定文件

download-git-repo 源码 可以知道,download-git-repo 是对 git-clonedownload 的封装。(download-git-repo 是通过 clone 属性决定采用哪种下载方式的)

看其对应的文档可以知道,git-clone 没有可以克隆指定文件的 API,而 downloadfilter 可以过滤文件。

所以我们要换一种写法,不用克隆的下载方法,也就是不传 clone ,然后通过 filter 在提取之前过滤掉不需要的文件。

export default (dir) => {
  download(
    "github:http://11.168.1.123:owner/name",
    path.join(process.cwd(), dir),
    { filter: (file) => path.extname(file.path) === ".tsx" },
    (err) => {
      console.log(err ? "Error" : "Success");
    }
  );
};

完成基础脚手架

最后让我们来写下 main.js 的执行逻辑。

main.js

#! /usr/bin/env node
import inquiry from "../src/inquiry.js";
import download from "../src/download.js";

inquiry().then(({ projectName }) => {
  download(projectName);
});

命令行中运行下面的指令:

node bin/main.js

在被安装到项目中后,可以执行以下命令查看帮助(cli 是 package.json 内 bin 内的命名):

yarn run cli -h

至此,脚手架最核心的功能部分已经介绍完毕了,都掌握了吧,是不是突然感觉脚手架很简单了🥳。

下面让我们给脚手架提升下用户体验。

提升脚手架体验

下载中的等待

ora :可以让命令行出现好看的 Spinners。

chalkkleur:使终端可以输出彩色信息文案。

在下载等待的过程中,让控制台增加 Spinners。 然后更改一些信息的颜色,让输出不那么单调。

改写下 download.js 文件:

import path from "path";
import download from "download-git-repo";
import ora from "ora";
import chalk from "chalk";

export default async (dir) => {
  const loading = ora(chalk.cyan(`downloading...`)).start();

  await new Promise((resolve, reject) => {
    download("owner/name", path.join(process.cwd(), dir), (err) => {
      err ? reject(err) : resolve();
    });
  })
    .then(() => {
      loading.succeed(chalk.green.bold("download successfully!"));
      
      console.log("Done. Now run:");
      console.log(chalk.green(`cd ${dir}`));
      console.log(chalk.green("yarn"));
      console.log(chalk.green("yarn start"));
    })
    .catch((err) => {
      console.error(err);
      loading.fail(chalk.red.bold("download failed!"));
    });
};

相同目录的情况

fs-extra:系统 fs 模块的扩展,可以更方便的操作系统中的文件。

在拉取模板前,增加一段逻辑,判断是否已存在相同目录,防止不小心覆盖已有目录。

改写下 main.js 文件:

#! /usr/bin/env node
import inquiry from "../src/inquiry.js";
import download from "../src/download.js";
import fs from "fs-extra";
import path from "path";
import inquirer from "inquirer";

inquiry().then(({ projectName }) => {
  let directory = path.join(process.cwd(), projectName);
  if (fs.existsSync(directory)) {
    inquirer
      .prompt([
        {
          type: "list",
          name: "cover",
          message: "已存在相同目录,是否覆盖?",
          choices: ["Yes", "No"],
        },
      ])
      .then(({ cover }) => {
        if (cover === "Yes") {
          fs.remove(directory).then(() => {
            download(projectName);
          });
        }
      });
  } else {
    download(projectName);
  }
});

自动安装依赖

execa:对 child_process 的方法进行了一些改进。

有 yarn 用 yarn 安装依赖,没有则用 npm 安装依赖。

install.js

import { execa } from "execa";

export default async (directory) => {
  try {
    await execa("yarn", ["install"], {
      cwd: directory,
    });
  } catch (e) {
    await execa("npm", ["install"], {
      cwd: directory,
    });
  }
};

改写下 download.js 文件:

import install from "./install.js";

/**...... */
.then(() => {
  loading.text = chalk.magenta("Installation dependencies in Progress...");
  let directory = path.join(process.cwd(), dir);
  install(directory).then(() => {
    loading.succeed(`Scaffolding project in ${directory}...`);
    console.log("Done. Now run:");
    console.log(chalk.green(`cd ${dir}`));
    console.log(chalk.green("yarn"));
    console.log(chalk.green("yarn start"));
  });
});
/**...... */

另外的工具库

下面列出一些你在写脚手架中,可能会需要用到的其他库。

commanderyargs:可以进行更加复杂的命令行参数解析。

meowarg:可以进行基础的命令行参数解析。

listr:可以在命令行中画出进度列表。

easy-table:可以在命令行中输出表格。

figlet:可以在命令行中输出 ASCII 的艺术字体。

boxen:可以在命令行中画出 Boxes 区块。

脚手架的使用

对比 vue-cli 和 create-vue

可以看到 vue-cli 脚手架的使用,先全局安装,再执行命令。

create-vue 是直接一行命令初始化项目。

npm init 文档可知:

npm init vue@next === npx create-vue@next

npm 负责安装不负责执行,npx 负责执行不负责安装。 正是 npx 无需安装即可运行命令的特性(npm 的 5.2 版本,发布于 2017 年 7 月),才使得可以如此简单优雅。

优雅的使用脚手架

我们的 package.jsonname 命名为 create-<name>,例:create-chestnut。

使用:

npm init chestnut
// or
npm create chestnut
//or
yarn create chestnut

当然你也可以直接执行 npx (package.json).name。

最后

我们只要发布到 npm 上就大功告成啦,这里有一份 打包 JavaScript 库的现代化指南 可以帮你更好的配置 package.json

目前上面只是属于你的脚手架的雏形,还需要不断打磨和优化,不断扩展命令和模板,不断的增强可操作性。

如果想更加扩展性的学习,看看 vue-clicreate-vue 源码(也可以看看 这篇文章 学习下原理)。

最后,这个算是个起点,掌握这个之后,想写一些其他 cli 也就不难。比如前端是通过 docker 部署代码,接口是通过 swagger-typescript-api 自动生成,也可以写入 cli 进行简化.....