前言
这篇文章会讲解如何搭建一个最简单的脚手架,即通过命令生成已设定好的模板
实现效果展示
创建项目
我们先创建一个node-cli结构的文件
fishfan-cli
├─ bin
│ └─ cli.js # 启动文件
└─ package.json
// package.json
{
"name": "fishfan-cli",
"version": "0.0.1",
"description": "a good cli",
"main": "index.js",
"bin": {
"fishcli": "./bin/cli.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "fishfan",
"license": "ISC"
}
如上图所示,我们在终端执行命令的时候,统一走bin下面的cli.js文件(这里并不是一定要在bin目录下面,而是根据package.json文件的bin字段所确定)
编辑cli.js文件
#!/usr/bin/env node
console.log('hello,fishfan')
为了方便在本地进行调试,使用npm link链接到全局,mac下需要使用管理员权限
sudo npm link
友情提示:必须在你所创建的脚手架的文件下进行该命令操作,不然会出现下面的错误
我们随后可以随意创建一个文件,在该文件下执行下fishcli
看到成功打印的hello,fishfan,算是第一步成功
终端命令交互
我们需要做的是让cli.js文件能够读懂我们的终端命令,比如fishcli create创建项目,为了能够在终端流利地操作命令行,我们引入commander模块
commander更多用法 👉 中文文档
安装依赖
npm install commander --save
编辑cli.js文件
#!/usr/bin/env node
const program = require('commander');
program
.command('create')
.description('create a project')
.action(() => {
console.log('欢迎使用fishcli脚手架')
});
// 解析用户执行命令传入参数
program.parse(process.argv);
在终端输入fishcli create查看命令是否创建成功
这时候我们的create命令已经创建成功,但是我们想实现和终端的交互功能,这就是要node的inquirer模块,同时我们发现很多的脚手架的文字都是彩色的,而我们的脚手架打印的都是默认颜色,我们引入chalk工具库
inquirer文档👉
命令交互
安装依赖
npm install inquirer chalk --save
inquirer的基础用法
const inquirer = require('inquirer');
inquirer
.prompt([
/* 把你的问题传过来 */
])
.then(answers => {
/* 反馈用户内容 */
})
.catch(error => {
/* 出现错误 */
});
假设我们这里做的是一个React脚手架,我们和用户设定的问题为
- 是否创建新的项目(是/否)
- 请输入项目名称(文本输入)
- 请输入作者(文本输入)
- 请选择公告管理状态(单选)
上述的prompt第一参数和我们所设置的问题进行结合,得到的question配置大致是这样
const question = [
{
name:'conf', /* key */
type:'confirm', /* 确认 */
message:'是否创建新的项目?' /* 提示 */
},{
name:'name',
message:'请输入项目名称',
when: res => Boolean(res.conf) /* 是否进行 */
},{
name:'author',
message:'请输入作者',
when: res => Boolean(res.conf)
},{
type: 'list', /* 选择框 */
message: '请选择公共管理状态?',
name: 'state',
choices: ['mobx','redux'], /* 选项*/
filter: function(val) { /* 过滤 */
return val.toLowerCase()
},
when: res => Boolean(res.conf)
}
]
我们接着完善cli.js的代码
#!/usr/bin/env node
const program = require('commander');
const chalk = require('chalk')
const inquirer = require('inquirer')
const question = [
{
name:'conf', /* key */
type:'confirm', /* 确认 */
message:'是否创建新的项目?' /* 提示 */
},{
name:'name',
message:'请输入项目名称?',
when: res => Boolean(res.conf) /* 是否进行 */
},{
name:'author',
message:'请输入作者?',
when: res => Boolean(res.conf)
},{
type: 'list', /* 选择框 */
message: '请选择公共管理状态?',
name: 'state',
choices: ['mobx','redux'], /* 选项*/
filter: function(val) { /* 过滤 */
return val.toLowerCase()
},
when: res => Boolean(res.conf)
}
]
program
.command('create')
.description('create a project')
.action(() => {
console.log(chalk.green('欢迎使用fishcli,轻松构建react ts项目~🎉🎉🎉'))
inquirer.prompt(question).then(answer=>{
console.log('answer=', answer )
})
});
// 解析用户执行命令传入参数
program.parse(process.argv);
效果如下
这时候我们可以拿到用户所输入的信息,然后我们就可以根据用户输入的信息,来选择适合用户输入的模板进行创建,这里的作者名字和项目名称都是动态的,因此我们需要动态去修改模板里面的文件,将用户输入的内容进行替换,本文暂时不讲解。
拷贝文件
在最外层我们创建template文件夹,这里模板文件就是供用户下载的文件,如果你的脚手架有多种选择比如mobx或者redux,那么这里的模板文件就不止一个。这里的template文件我使用的是webpack5搭建的React项目,代码地址,关于如何使用webpack5搭建react项目可以查看该文章链接
由于template项目模板,有可能是深层的文件结构,我们需要深拷贝项目文件,需要node的fs模块
我们在src下创建create.js文件
src/craete.js
const fs = require('fs');
const chalk = require('chalk')
const { Buffer } = require('buffer');
/* 三变量判断异步操作 */
let fileCount = 0; /* 文件数量 */
let dirCount = 0; /* 文件夹数量 */
let flat = 0; /* readir数量 */
module.exports = function (res) {
/* 创建文件 */
console.log(chalk.green('------开始构建-------'));
const sourcePath = __dirname.slice(0, -3) + 'template';
console.log(chalk.blue('当前路径:' + process.cwd()));
/* 修改package.json*/
revisePackageJson(res, sourcePath).then(() => {
copy(sourcePath, process.cwd());
});
};
const copy = (sourcePath, currentPath)=> {
flat++;
fs.readdir(sourcePath, (err, paths) => {
flat--;
if (err) {
throw err;
}
paths.forEach((path) => {
if (path !== '.git' && path !== 'package.json') fileCount++;
const newSoucePath = sourcePath + '/' + path;
const newCurrentPath = currentPath + '/' + path;
fs.stat(newSoucePath, (err, stat) => {
if (err) {
throw err;
}
if (stat.isFile() && path !== 'package.json') {
const readSteam = fs.createReadStream(newSoucePath);
const writeSteam = fs.createWriteStream(newCurrentPath);
readSteam.pipe(writeSteam);
console.log(chalk.green('创建文件:' + newCurrentPath));
fileCount--;
} else if (stat.isDirectory()) {
if (path !== '.git' && path !== 'package.json') {
dirCount++;
dirExist(newSoucePath, newCurrentPath, copy);
}
}
});
});
});
}
const dirExist =(sourcePath, currentPath, copyCallback)=> {
fs.exists(currentPath,(exist) => {
if (exist) {
copyCallback(sourcePath, currentPath);
} else {
fs.mkdir(currentPath, () => {
fileCount--;
dirCount--;
copyCallback(sourcePath, currentPath);
console.log(chalk.yellow('创建文件夹:' + currentPath));
});
}
});
}
const revisePackageJson = (res, sourcePath) => new Promise((resolve) => {
fs.readFile(sourcePath + '/package.json', (err, data) => {
if (err) throw err;
const { author, name } = res;
let json = data.toString();
json = json.replace(/demoname/g, name.trim());
json = json.replace(/demoAuthor/g, author.trim());
const path = process.cwd() + '/package.json';
const data1 = new Uint8Array(Buffer.from(json));
fs.writeFile(path, data1, () => {
console.log(chalk.green('创建文件:' + path));
resolve();
});
});
});
在cli.js引入我们创建的create文件的方法
const create = require('../src/create');
program
.command('create')
.description('create a project')
.action(() => {
console.log(chalk.green('欢迎使用fishcli,轻松构建react ts项目~🎉🎉🎉'))
inquirer.prompt(question).then(answer=>{
if (answer.conf) {
create(answer)
} else {
console.log(chalk.yellow('没关系,希望以后有合作机会!'))
}
})
});
这时候我们fishcli create命令已经完善完毕,可以利用该命令来生成自己的文件
至此我们最基本的脚手架已经构建完成。
结束语
关于文件拷贝部分涉及node的fs知识,这里建议大家抽时间看看这里的内容,我这里也是直接使用了他人写的方法,自己着实没想到啥好的方法来。这篇文章其实介绍功能就是拷贝文件生成文件,终端等命令其实都是工具而已,不过所涉及的工具是大家学习终端操作所避不开的东西。
本文大部分内容借鉴以下俩篇文章,如果大家想深入了解脚手架知识,可以参考下面两篇文章,后续我会继续完善该脚手架,并对文章进行完善,做进一步的了解。