搭建自己的脚手架工具
了解真相,你才能获得真正的自由
首先要明确一下什么脚手架?
相信对 vue/cli 和 create-react-app 等一些脚手架工具多多少少也是有使用过,我们可以通过这些脚手架工具可以快速的创建出一个项目的雏形,所以简单的来说,脚手架就是为我们快速创建项目的一种工具,我们不需要从零搭建一个项目,为我们的开发在一定的程度上提高效率。
知道脚手架是什么了,那么接下来我们就是明确一下我们的脚手架的工作流程了
脚手架的基本工作流程
- 首先是对命令行的命令进行解析(稍后会在代码中体现,暂时只需要知道第一步是这个操作)
- 根据你的命令克隆指定的仓库代码
- 安装依赖
- 启动项目
- 打开浏览器(可在项目模板中配置好)
接下来我们就按照我们梳理的工作流程进行代码的编写
在正式写代码之前,我们先创建一个空文件夹,然后再到终端进入到你所创建的文件夹,执行
npm init -y
会生成 package.json 文件,并且添加 bin 字段,如下代码片段,其中 tjh 是我想在命令行中执行的命令,你也可以用你想用的命令名,index.js是这个命令的执行入口,当然这个也是你可以自定义的
package.json
{
"name": "tjh-cli",
"version": "1.2.4",
"description": "",
"main": "index.js",
"scripts": {
},
"bin": {
"tjh": "index.js"
},
"keywords": [],
"author": "",
}
下图就是我对目录结构的划分,也可以根据自己的喜好来进行安排(在下面编写代码的时候再来对文件进行说明)
index.js
#!/usr/bin/env node
const { Command } = require('commander')
const { helper } = require('./lib/core/helper')
const { createCommands } = require('./lib/core/create')
// 创建处理程序
const program = new Command()
// -V --version 查看版本号
program.version(require('./package.json').version)
// 增加 options
helper(program)
// 增加 commands
createCommands(program)
// 解析指令
program.parse(process.argv)
在当前目录下(tjh-cli)执行 npm link,我的入口文件(index.js)的顶部写了一行 #!/usr/bin/env node,当我执行 tjh xxx 时(tjh 是之前在 package.json 中的 bin 配置好的),会执行当前入口文件,然后就可以在 process.argv 中拿到参数,接下来就是对参数的解析啦。
1. 参数解析
这个事情我们是不需要从零开始的,我们可以站在巨人的肩膀上前进,那就是我在 index.js 中引入的commander 这个包。首先我们从 commander 中导入 Command ,创建一个处理程序并命名为 program,后续我们需要他来进行参数的解析,也可以为他配置一些 option 和 command ,如 -v 和 create。program.parse(process.argv)就是执行解析命令了,为什么不是先解析呢,因为我们还要先为 program 配置一下必要的 command 和 option 。
2. 克隆项目模板代码
当我们知道命令的解析是可以交给 commander 为我们处理,那么我们接下来就是要为其配置对应的命令的执行操作了,我打算就是执行 tjh create projectName 就能为我创建一个项目,那么我就要为我的处理程序 program 配置命令了;回到 index.js 中,我导入了一个叫做 createCommands 的函数,并且将我的 program 传入执行,接下来就看看 createCommands 干了些什么吧。
create.js(该文件是用来为 program 配置 command 和 options 的)
const { createProjectAction } = require('./actions')
const createCommands = (program) => {
program.command('create <project>')
.description('clone a project')
.action(createProjectAction)
}
module.exports = {
createCommands
}
为 program 配置一个 create 命令(program.command('create <project>'),后面跟着的 project 就是项目名称了,关于一些写法规则可以参考其 GitHub,后面的 description 就是该 command 的描述了,最后的 action 就是执行该指令对应的操作了,那么接下来就看看 createProjectAction 的内容了
action.js
const { promisify } = require('util') // 可以将回调形式转化为 promise 形式
const download = promisify(require('download-git-repo')) // clone 项目的包
const open = require('open') // 打开浏览器
const inquirer = require('inquirer') // 交互界面
const { commandSpawn } = require('../utils/terminal') // 开始子
const configs = require('../config/frameworkConfig') // 项目模板配置
const frameworkTypes = require('../config/frameworkTypes') // 项目的类型
const createProjectAction = async (project) => {
// 传进来的参数 project 就是 create 后面跟的项目名字
const prompt = inquirer.createPromptModule(); // 创建问答
const { framework } = await prompt({
type: 'list', // 问答类型
name: 'framework', // 这个名字是到时候用来获取用户选择的值的
choices: frameworkTypes // 一个数组,我这里是框架名字
})
let { type, repo, command, install, run, url} = configs[framework] // 获取框架配置
console.log(`正在为您创建 ${framework} 项目·······`)
// 处理 windows 兼容性,在 windows 中 需要加.cmd
command = process.platform === 'win32' ? command + '.cmd' : command
// 下载模板
try {
await download(repo, project, { clone: true })
} catch(e) {
// 可以对出错做一些处理
// console.log('---------', e)
console.log('git clone 的时候出错了,请重新创建~~~~~')
return
}
// 执行 install
try {
await commandSpawn(command, install, { cwd: `./${project}`})
} catch (e) {
console.log('npm install 的时候出错了,请手动执行 npm install~~~~~~~')
return
}
// 将项目 run 起来
commandSpawn(command, run, { cwd: `./${project}`})
// 打开浏览器
// if (type === 'client') {
// open(url)
// }
}
module.exports = {
createProjectAction
}
frameworkTypes.js
module.exports = [
'Vue',
'React',
'Koa',
'Express',
'Nest'
]
当我们执行 tjh create demo 之后,后续就应该创建项目了,但是我们是不是应该先选择技术栈,或者说是框架;那么关于问答对话方面,我这里使用的是一个叫做 inquirer 的库,我这里就只是使用了 list 的形式(具体可看代码中的逐行注释),更多形式可以参考其GitHub,问答界面效果如下
当我们选择框架之后,就是获取对应的框架配置了,也就是 action.js 中的
let { type, repo, command, install, run, url} = configs[framework] // 获取框架配置
我的 frameworkConfig.js(暂时只配置了Vue)
module.exports = {
Vue: {
type: 'client', // 项目的端
repo: 'direct:https://github.com/AbaAba01/vue-temp.git',
install: ['install'],
run: ['run', 'dev'],
command: 'npm',
url: 'http://localhost:8080/'
}
}
我们已经获取到了项目的配置了,那么就可以进行克隆了,这里我使用到了 一个叫做 download-git-repo 的库,更多细节可以点击访问,作者是使用的回调形式,然后我通过 promisify 将其 promise 化了,进而使用了 async 和 await;使用 download 传递的参数 repo 为项目模板仓库地址,project 即为项目名,配置 { clone: true },然后这个包会帮你把对应的 repo 项目克隆下来。
3. 安装依赖
项目克隆下来之后,我们是需要进行安装依赖的,因为我们通常不会将 node_modules 传的自己的仓库中,其实我们在框架的配置文件中就配置了需要执行的指令(这里就不管你是 yarn 还是 npm 了),那么这里我们也可以对一个统一的动作进行封装了。可以看到,我在处理安装依赖和运行项目用的都是同一个接口(commandSpawn),那么接下来看一下 commandSpawn 的实现
terminal.js
const { spawn } = require('child_process')
const commandSpawn = (...args) => {
return new Promise((resolve, reject) => {
const childProcess = spawn(...args)
// 打印信息
childProcess.stdout.pipe(process.stdout)
childProcess.stderr.pipe(process.stderr)
// 监听结束
childProcess.on('close', () => {
resolve()
})
})
}
module.exports = {
commandSpawn
}
里面也就是对 node 原生模块的一个小封装,更多细节可在 官网文档 查看,简单的说,这个就是先传入一个命令,如 npm ,后续可以传入一个数组,执行一个或多个名,如 ['run', 'dev'],配置项可以配置所要执行的目录,如 { cwd: `./${project}` },就是在当前你创建的目录中执行。当当前子进程执行结束之后,也就是触发 close 回调的时候,那么就可以执行 resolve 了,因为我在 action.js 中都是使用的 await ,所以当我回调中执行 resolve 之后才会继续往后执行, 安装依赖那么就是直接执行
await commandSpawn(command, install, { cwd: `./${project}`})
4. 启动项目
基于上面我们封装的 commandSpawn 函数,那么启动项目就是直接执行
commandSpawn(command, run, { cwd: `./${project}`})
5. 打开浏览器
如果项目没有配置打开浏览器的话,那么可以使用一个叫做 open 的包,执行
//打开浏览器
if (type === 'client') {
open(url)
}
我在我的 Vue 项目模板中已经配置了打开浏览器,所以我在代码中将打开浏览器操作给注释了
到这里基本的工作流程就已经差不多了,那么最后来对文件的划分来进行一个说明
- /lib/config/frameworkCondig.js -> 导出仓库的基本配置
- /lib/config/frameworkTypes.js -> 导出框架的类型
当然,还可以配置更多的一些东西,比如更多的一些 问答询问配置
- /lib/core/hllper.js -> 配置 program 的 options (在本文中未介绍)
其实program还有其他很多的一些功能,感兴趣可以上GitHub学习
- /lib/core/action .js -> 执行对指令的 action 函数
- /lib/core/create.js -> 给 program 创建命令
- /index.js -> 入口文件
早睡早起