很好奇,像create-react-app、react-native-cli是怎么样生产出来和使用的,我们是不是可以创建自己的脚手架,当一回小小的架构师。
当然不否认官方出品的这些脚手架很优秀,有时候我们会根据公司的业务需求,制定关于合适于自己的脚手架,这样我们可以快速初始化一个项目,无需自己从零开始一步步配置,有效提升开发体验。
很好奇别人是怎么开发自己的脚手架的,带着自己的好奇心开始了下面的探索
目标
- 执行
my-cli create my-project能够在本地创建一个my-project文件夹 - 文件下面有自定义的内容(例如项目的基础架构)以及已经下载好的
node_modules
让我们开始吧!!!
一、初始化项目
mkdir my-cli
cd my-cli
//下面命令二选一。
// 使用-y是yes的简写,为了省去了敲回车的步骤,生成的默认的package.json
npm init
npm init -y
完成之后,需要修改一下package.json里面的内容
{
...
// 主要增加这个配置
"bin": {
"my-cli": "./bin/index.js" // 当然这个地址是根据自己的定义位置写的,我把执行的命令放在了bin文件夹下面的index中
}
...
}
二、那么根据地址,创建相应的文件夹
下面是我的文件夹详情:
├── bin
│ └── index.js // create 命令定义处
├── template // 项目的模版内容,根据需要配置不同的模版
│ ├── index-html.js
│ ├── package-json.js
│ └── src
│ ├── app.js
│ ├── jest
│ │ ├── index.js
│ │ └── src
│ │ └── demo.js
│ └── test
│ └── tes.js
├── package.json
└── src // 主要定义 create 流程主代码
├── main.js // 入口函数
└── utils // 存放工具方法
├── create.js
└── generatorFile.js
入口是bin/index.js文件
#!/usr/bin/env node
require('../src/main.js');
加在文件顶部的 #!/usr/bin/env node,是告知这是一个可执行文件。
#!就是代表此文件可以当做脚本运行。
/usr/bin/env node 是使用node运行的脚本文件,使用usr/bin/文件里面的env环境变量里面的node去执行该文件。
三、创建软链接至全局
命令行执行 npm link ,创建软链接至全局,这样我们就可以全局使用my-cli命令了,
你可以通过命令行执行 my-cli my-project查看是否成功,可以在bin/index.js增加一句
...something
console.log(process.argv, 'porcess arguments');
然后就可以在命令行中输出:
[ '/usr/local/bin/node', '/usr/local/bin/my-cli', 'my-project' ] porcess arguments
这样则表示我们的软链接ok了。
四、解析命令行参数
我们在命令行中输入 muy-cli create my-project,其实需要对于不同的参数进行解析,然后执行不同的操作,或打印版本,或执行创建命令。
在这里,我是用的是commander 包,node的命令行解析使用较多的包。
1、下面是简单的一个简单的version命令的解析
// src/main.js
const program = require('commander');
const { version } = require('../package.json'); // 引入package.json中的版本
program.version(version, "-v, --version")
.parse(process.argv);
2、除了一些简单的展示操作,创建命令解析才是是猪脚戏。
// src/main.js
const program = require('commander');
... something
program
.command('create') // 命令的名称
.action(() => { // 动作
const createCommand = require('./utils/create')
createCommand(process.argv[3]) // 根据打印出来的arguments,确定第四个参数是执行参数
});
3、创建命令执行创建任务。为了保持main.js文件整洁,并且为更多的参数做准备,将相关的操作放在了utils不同的文件中。下面是创建的流程
在这里,我们是用mkdirp包进行文件/文件夹的创建
// src/utils/create
const path = require("path");
const mkdirp = require("mkdirp");
const generatorFile = require('./generatorFile');
module.exports = async (projectName) => {
// download template and rename project
const projectDir = path.join(process.cwd(), projectName);
try {
await mkdirp(projectDir);
console.log('创建成功')
// generate files 递归创建文件以及文件夹
generatorFile(path.join(__dirname, '../../template'), projectDir, projectName)
} catch (error) {
console.log('创建失败')
}
};
/**
* ⚠️注意
* 如果直接使用 mkdirp(path.join(process.cwd(), projectName), function (error) { do something}) 可能会报错,提示UnhandledPromiseRejectionWarning: TypeError: invalid options argument
* 因为版本的问题,不再支持 callback 的形式,需要更换为 promise 的形式,即使用 .then 或者 await 形式
*/
4、创建模版,下面是package.json的模板,使用者可以根据自己的需求创建脚手架模版,但是需要注意,模版都是js文件,template里面是生成的内容(比如jsx模版,tsx模版,不管是什么都可以的)
module.exports = function (name) {
const template = `
{
"name": "${name}",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"devDependencies": {
},
"author": "",
"license": "ISC",
"dependencies": {
"vue": "^2.6.10"
}
}
`;
return {
template,
name: "package.json"
};
};
5、递归创建文件以及文件夹
const fs = require("fs");
const path = require("path");
const mkdirp = require("mkdirp");
/**
*
* @param {string} templatePath template files path 模版文件的位置,
* @param {string} projectDir create file path 创建文件的路径
* @param {string} projectName 命令行中的项目名
* @param {string} _dirName 可选参数 - 文件名
*/
const generatorFile = async (templatePath, projectDir, projectName, _dirName = '') => {
// 读取模版文件
const fileArr = await fs.readdirSync(templatePath)
if (fileArr && fileArr.length > 0) {
fileArr.forEach(async element => {
// 根据是否文件或者文件夹走不同分支
if (element.split('.').length > 1) {
const _path = path.join(templatePath, element)
const { template, name: fileName } = require(_path)(projectName);
fs.writeFile(path.join(projectName, _dirName, fileName), template.trim(), function (error) {
// error handle
})
} else {
await mkdirp(path.join(projectDir, element));
// 递归创建文件夹中的文件
generatorFile(path.join(templatePath, element), path.join(projectDir, element), path.join(projectName, _dirName), element)
}
})
}
}
module.exports = generatorFile
上面巴拉巴拉的一通操作,简单的脚手架就弄好了,你可以试着执行你定义的命令,康康会出现什么神奇的效果!
五、发布至npm 上
npm publish # 已经发布了~~
最后
当然,这是一个入门了解如何创建脚手架的一个流程,文中有很多需要优化和补充的内容,比如更多的模版类型支持,像TS;又或者说根据不同的创建参数,使用不同创建不一样的项目;又或者是增加git hooks。。。
我将上面的创建内容都放在了 Github 库中,想要看全部的内容,请点击链接:github.com/scolleen/sc…