从零开始搭建前端脚手架
一、功能设计
每个前端小组都会有自己的独特的业务场景,从这些业务场景从提取公共部分,并打造一个前端项目模版,是非常有必要的
为了能够基于这个项目模版快速创建一个新项目,就需要脚手架工具登场
所以这里至少有两个项目仓库:前端模版项目、脚手架工具
而对于脚手架工具,它应当具备这样的功能:输入一个命令和项目名称,创建对应的项目目录,其内容就是模版项目
my-cli create my-project
基于此,脚手架工具的内部逻辑也就很清晰了:
创建一个 create 命令,其行为是拉取模版项目的代码(用 git clone 即可实现)
为了实现这个命令,我们需要借助一个神奇的工具:commander
二、Commander
Commander.js 是完整的 node.js 命令行解决方案
可以通过它创建一个 program 对象,作为程序的主体
const { Command } = require('commander');
const program = new Command();
program.version('0.0.1');
commander 常用的功能有创建选项和命令
选项 option 需要基于短线 " - " 声明,常见的 -h 、 -V 都是选项
每个选项可以定义一个短选项名称(" - " 后面接单个字符)和一个长选项名称(" -- "后面接一个或多个单词)
解析后的选项可以通过 .opts() 方法获取,从而执行对应的操作
比如创建一个 -d 选项:
program
.option('-d, --debug', 'output extra debugging')
program.parse(process.argv);
const options = program.opts();
if (options.debug) console.log(options);
还可以对选项定义参数及默认值:
program
.option('-d, --debug <type>', 'output extra debugging', 'hard')
这里就对 debug 选项定义了 type 参数,并设置其默认值为 hard
用尖括号 <> 定义的参数为必填,用中括号 [] 创建的参数为选填
命令 command 可以通过 .command() 创建,并通过链式调用 .description() 和 .action() 定义该命令的描述和具体行为
program
.command('create <name> [type]')
.description('create a new project')
.action((name, type) => {
console.info('project', name);
});
和选项类似,命令也可以通过 <> 和 [] 定义参数
除了像上面那样通过链式调用创建命令行为(description、action)之外,还可以用单文件的形式描述命令,不过我没有跑通
commander 还有更多更强大的功能,这里就不逐一介绍了,详情可以参考中文文档
三、正式开始
学习了 commander 之后,就可以着手脚手架的开发了
首先创建脚手架工具的目录,如 my-cli,然后在目录下创建一个 .gitignore 文件
# Dependency directories
node_modules/
接着通过 npm init 创建 package.json
创建之后可以删除其中的 main 和 scripts,然后安装 commander
npm install commander --save
然后创建 bin 目录及 bin/cli.js 文件:
#!/usr/bin/env node
const { Command } = require('commander');
const { name, version } = require('../package.json');
const program = new Command();
program.name(name).version(version);
program
.command("create <project-name>")
.description("create a new project")
.action((name) => {
console.info('project', name);
});
program.parse();
顶部的 #!/usr/bin/env node 是告诉操作系统,用 /usr/bin 下的 node 来执行这个脚本
一个简单的 cli 命令就创建好了,可以回到根目录,用 node 执行 cli.js 试试:
node bin/cli.js create my-project
可以看到 create 命令正常执行
但直接通过 node + 路径 的形式运行代码还是太死板了,可以通过 npm link 将项目挂到全局,就能像正常的脚手架工具那样用了
首先需要在 package.json 里添加 bin 命令:
{
...
"bin": {
"my-cli": "bin/cli.js"
}
}
这里的 my-cli 是包的名称,后面的路径需要写全后缀
然后在根目录执行 npm link 就能将 my-cli 链接到全局
npm link 是一个很好的本地测试的手段,调试完成后可以通过 npm unlink 卸载
四、完善 create 命令
据文档介绍, commander 支持将命令拆成单文件进行维护,但我没有搞出来,最后只好加了一层 map 来拆文件
在根目录下新建一个 command 目录,并创建 command/create.js 和 command/index.js 两个文件:
然后将 create 命令的相关逻辑移到 command/create.js 中
// create.js
function createAction(name) {
console.log('project', name);
}
const create = {
alias: 'c',
params: '<project-name>',
description: 'create a new project',
action: createAction,
}
module.exports = create;
并在 command/index.js 中导出
// index.js
const create = require('./create.js');
module.exports = {
create,
};
最后来改造 bin/cli.js
#!/usr/bin/env node
const { Command } = require('commander');
const { name, version } = require('../package.json');
const commands = require('../command/index.js');
const program = new Command();
program.name(name).version(version);
// 创建命令
Reflect.ownKeys(commands).map((name) => {
const { params, alias, action, description } = commands[name] || {};
program.command(`${name} ${params || ''}`)
.alias(alias)
.description(description)
.action((...args) => {
typeof action === 'function' && action(...args);
})
});
program.parse(process.argv);
项目的结构基本成型,接下来完善 create 命令
在文章的开头就已经分析过了,create 命令只需要做一个事情,就是将项目 clone 到当前目录
const { exec } = require("child_process");
function createAction(name) {
// 这是模板项目的仓库地址
const url = "http://github.com/xxx/template.git";
// 克隆项目
exec(`git clone ${url} ${name}`, (error, stdout, stderr) => {
if (error) {
console.log(error);
process.exit();
}
console.log("Success");
process.exit();
});
}
如果模板项目是 github 上的项目,应该没什么大问题
而如果模板项目是小组内部的 gitlab 项目,就需要放开模板项目的访问权限,至少让小组人员都能够访问
五、优化体验
在完成了 create 命令之后,我们的脚手架工具就可以算是开发完成
但为了更好的体验,可以借助这些工具加以改造:chalk、inquirer
1. chalk
它可以在命令行打印彩色文字:
import chalk from 'chalk';
const error = chalk.bold.red;
const warning = chalk.hex('#FFA500');
console.log(chalk.blue('Hello world!'));
console.log(error('Error!'));
console.log(warning('Warning!'));
2. inquirer
这是一个让用户与命令行交互的工具
它提供了很多 api,让用户可以在程序运行的过程中输入内容,从而影响程序运行的结果
const inquirer = require('inquirer');
inquirer
.prompt([
/* Pass your questions in here */
])
.then((answers) => {
console.log('success', answers);
})
.catch((error) => {
console.log('error');
});
在上面的 prompt 中配置需要用户输入/选择的(表单)内容
比如让用户输入项目名称:
.prompt([
{
type: "input",
message: "项目名称:",
name: "projectName",
validate: (val) => {
// 对输入的值做判断
if (!val || !val.trim()) {
return chalk.red("项目名不能为空,请重新输入");
} else if (val.includes(" ")) {
return chalk.red("项目名不能包含空格,请重新输入");
}
return true;
},
},
])
除了这里的 input 类型外,inquirer 还提供了很多交互类型,如单选列表 list、多选 checkbox、确认项 confirm 等
在了解了 chalk 和 inquirer 之后,我们就可以进一步改造 create 命令
npm install inquirer chalk --save
// create.js
const inquirer = require("inquirer");
const chalk = require("chalk");
const { exec } = require("child_process");
function createProject(name) {
// 这是模板项目的仓库地址
const url = "git@github.com:wisewrong/chart-admin.git";
exec(`git clone ${url} ${name}`, (error, stdout, stderr) => {
if (error) {
console.log(chalk.red(error));
process.exit();
}
console.log(chalk.green("Success"));
process.exit();
});
}
const create = {
alias: "c",
params: "[project-name]",
description: "create a new project",
action: (project) => {
project
? createProject(project)
: inquirer
.prompt([
{
type: "input",
message: "项目名称:",
name: "projectName",
validate: (val) => {
// 对输入的值做判断
if (!val || !val.trim()) {
return chalk.red("项目名不能为空,请重新输入");
} else if (val.includes(" ")) {
return chalk.red("项目名不能包含空格,请重新输入");
}
return true;
},
},
])
.then((answer) => {
createProject(answer.projectName);
});
},
};
module.exports = create;
麻雀虽小五脏俱全,这样一个简单的脚手架就开发完了
如果需要发布到 npm,可以参考我之前的文章《vue-cli 3.x 开发插件并发布到 npm》
后面我也会继续完善这个脚手架,添加更多的交互,以及展示进度条(专业挖坑,从来不填...)