前言
脚手架是前端工程化中不可缺少的一环,主要为了快速搭建新项目,统一项目约定规范,通过自动化和智能化流程,规避 copy 带来的繁琐和易出错,提升开发效率和体验。
举个实际点的例子:首先项目目录结构就可以达到统一,其次 package.json 内的依赖,常用的一些库,都可以提前内置。
总而言之就是:目前脚手架无法满足你搭建完后直接开发,还需要进行额外的许多操作时,是时候开始自己动手来一个脚手架了。
配置约定
项目目录结构:
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 官方的项目脚手架工具。发现可以分成核心两步:
- 命令交互:开始询问创建的项目名,然后是一些可选功能提示。
- 下载模板:根据用户的选择,拉取对应的模板创建项目。
命令行交互
inquirer 、enquirer、prompts:可以处理复杂的用户输入,完成命令行输入交互。(下面以 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-clone 和 download 的封装。(download-git-repo 是通过 clone 属性决定采用哪种下载方式的)
看其对应的文档可以知道,git-clone 没有可以克隆指定文件的 API,而 download 有 filter
可以过滤文件。
所以我们要换一种写法,不用克隆的下载方法,也就是不传 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。
在下载等待的过程中,让控制台增加 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"));
});
});
/**...... */
另外的工具库
下面列出一些你在写脚手架中,可能会需要用到的其他库。
commander、yargs:可以进行更加复杂的命令行参数解析。
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.json
的 name
命名为 create-<name>
,例:create-chestnut。
使用:
npm init chestnut
// or
npm create chestnut
//or
yarn create chestnut
当然你也可以直接执行 npx (package.json).name。
最后
我们只要发布到 npm 上就大功告成啦,这里有一份 打包 JavaScript 库的现代化指南 可以帮你更好的配置 package.json
。
目前上面只是属于你的脚手架的雏形,还需要不断打磨和优化,不断扩展命令和模板,不断的增强可操作性。
如果想更加扩展性的学习,看看 vue-cli 或 create-vue 源码(也可以看看 这篇文章 学习下原理)。
最后,这个算是个起点,掌握这个之后,想写一些其他 cli 也就不难。比如前端是通过 docker 部署代码,接口是通过 swagger-typescript-api 自动生成,也可以写入 cli 进行简化.....