从零开始搭建自己的脚手架工具(保姆级)

182 阅读5分钟

搭建自己的脚手架工具

了解真相,你才能获得真正的自由

首先要明确一下什么脚手架?

相信对 vue/cli 和 create-react-app 等一些脚手架工具多多少少也是有使用过,我们可以通过这些脚手架工具可以快速的创建出一个项目的雏形,所以简单的来说,脚手架就是为我们快速创建项目的一种工具,我们不需要从零搭建一个项目,为我们的开发在一定的程度上提高效率。


知道脚手架是什么了,那么接下来我们就是明确一下我们的脚手架的工作流程了

脚手架的基本工作流程

  1. 首先是对命令行的命令进行解析(稍后会在代码中体现,暂时只需要知道第一步是这个操作)
  2. 根据你的命令克隆指定的仓库代码
  3. 安装依赖
  4. 启动项目
  5. 打开浏览器(可在项目模板中配置好)

接下来我们就按照我们梳理的工作流程进行代码的编写

在正式写代码之前,我们先创建一个空文件夹,然后再到终端进入到你所创建的文件夹,执行

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": "",

}

下图就是我对目录结构的划分,也可以根据自己的喜好来进行安排(在下面编写代码的时候再来对文件进行说明)

目录结构.png

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,问答界面效果如下

inquirer.png

当我们选择框架之后,就是获取对应的框架配置了,也就是 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 项目模板中已经配置了打开浏览器,所以我在代码中将打开浏览器操作给注释了

到这里基本的工作流程就已经差不多了,那么最后来对文件的划分来进行一个说明

  1. /lib/config/frameworkCondig.js -> 导出仓库的基本配置
  2. /lib/config/frameworkTypes.js -> 导出框架的类型

当然,还可以配置更多的一些东西,比如更多的一些 问答询问配置

  1. /lib/core/hllper.js -> 配置 program 的 options (在本文中未介绍)

其实program还有其他很多的一些功能,感兴趣可以上GitHub学习

  1. /lib/core/action .js -> 执行对指令的 action 函数
  2. /lib/core/create.js -> 给 program 创建命令
  3. /index.js -> 入口文件

本文代码对应的 GitHub链接npm链接


早睡早起